- Explore some more advanced, real-world usages of generators
- Understand how to modify existing source code with generators
Generate another generator called update-scope-schema
. Use it to set the defaultProject
to movies-app
in our nx.json
file.
🐳 Hint
- Use the updateJson utility
- update the generator schema such that no
name
property is required - Try it first before you head over to the solution
🐳 Solution
import { formatFiles, Tree, updateJson } from '@nx/devkit';
export default async function (tree: Tree) {
updateJson(tree, 'nx.json', (json) => ({
...json,
defaultProject: 'movies-app',
}));
await formatFiles(tree);
}
Now that we had some practice with the updateJson
util - Let's build something even more useful:
- When large teams work in the same workspace, they will occasionally be adding new projects and hence, new scope tags
- We want to make sure that scope tags specified in our
util-lib
generator are up to date and take into account all these new scopes that teams have been adding - We want to check if there is a new scope tag in any of our
project.json
files and update our generator schema - We can use the
getProjects
util to read all the projects at once. ✨ BONUS:
Modify your generator so it fetches list of scopes from all theproject.json
files and updates the schema inutil-lib
with any new ones
🐳 Hint: Function to extract all scopes
function getScopes(projectMap: Map<string, ProjectConfiguration>) {
const allScopes: string[] = Array.from(projectMap.values())
.map((project) => {
if (project.tags) {
const scopes = project.tags.filter((tag: string) => tag.startsWith('scope:'));
return scopes;
}
return [];
})
.reduce((acc, tags) => [...acc, ...tags], [])
.map((scope: string) => scope.slice(6));
// remove duplicates
return Array.from(new Set(allScopes));
}
Use updateJson
function from @nx/devkit
to update the schema.json
file
🐳 Hint: Schema replacement
updateJson(
tree,
'libs/internal-plugin/src/generators/util-lib/schema.json',
(schemaJson) => {
schemaJson.properties.directory['x-prompt'].items = scopes.map(
(scope) => ({
value: scope,
label: scope,
})
);
schemaJson.properties.directory.enums = scopes;
return schemaJson;
}
);
formatFiles
async function at the end of your generator.
Our index.ts
also has a Schema
interface that should be updated. Although it's recommended to use ASTs for more complex code replacement cases, in this case we will use simple tree.read(path)
and tree.write(path, content)
methods.
🐳 Hint: Replacing scopes
function replaceScopes(content: string, scopes: string[]): string {
const joinScopes = scopes.map((s) => `'${s}'`).join(' | ');
const PATTERN = /interface UtilLibGeneratorSchema \{\n.*\n.*\n\}/gm;
return content.replace(
PATTERN,
`interface UtilLibGeneratorSchema {
name: string;
directory: ${joinScopes};
}`
);
}
🐳 Solution
import {
Tree,
updateJson,
formatFiles,
ProjectConfiguration,
getProjects,
} from '@nx/devkit';
export default async function (tree: Tree) {
const scopes = getScopes(getProjects(tree));
updateSchemaJson(tree, scopes);
updateSchemaInterface(tree, scopes);
await formatFiles(tree);
}
function getScopes(projectMap: Map<string, ProjectConfiguration>) {
const projects: any[] = Array.from(projectMap.values());
const allScopes: string[] = projects
.map((project) =>
project.tags.filter((tag: string) => tag.startsWith('scope:'))
)
.reduce((acc, tags) => [...acc, ...tags], [])
.map((scope: string) => scope.slice(6));
return Array.from(new Set(allScopes));
}
function updateSchemaJson(tree: Tree, scopes: string[]) {
updateJson(
tree,
'libs/internal-plugin/src/generators/util-lib/schema.json',
(schemaJson) => {
schemaJson.properties.directory['x-prompt'].items = scopes.map(
(scope) => ({
value: scope,
label: scope,
})
);
schemaJson.properties.directory.enums = scopes;
return schemaJson;
}
);
}
function updateSchemaInterface(tree: Tree, scopes: string[]) {
const joinScopes = scopes.map((s) => `'${s}'`).join(' | ');
const interfaceDefinitionFilePath =
'libs/internal-plugin/src/generators/util-lib/schema.d.ts';
const newContent = `export interface UtilLibGeneratorSchema {
name: string;
directory: ${joinScopes};
}`;
tree.write(interfaceDefinitionFilePath, newContent);
}
Create a new app and define a brand new scope for it. Run your generator and notice the resulting changes. Commit them so you start fresh on your next lab.
🐳 Hint
nx generate app video-games --tags=scope:video-games
Our internal-plugin
doesn't have tags either. Let's add some:
{
// ...
"tags": ["type:util", "scope:internal"]
}
Run generator again.
Use a tool like Husky to run your generator automatically before each commit. This will ensure developers never forget to add their scope files.
🐳 Solution
{
"scripts": {
"postinstall": "husky install",
"pre-commit": "npx nx g @nx-workshop/internal-plugin:update-scope-schema"
}
}
Create a test to automate verification of this generator in libs/internal-plugin/src/generators/update-scope-schema/generator.spec.ts
. This will be particularly difficult, as you'll need to create a project with the actual source code of your util-lib
generator as part of the setup for this test.
⚠️ Check the solution if you get stuck!
🐳 Solution
import { readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { libraryGenerator } from '@nx/js/generators';
import { generatorGenerator, pluginGenerator } from '@nx/plugin/generators';
import { readFileSync } from 'fs';
import { join } from 'path';
import { Linter } from '@nx/eslint';
import generator from './generator';
describe('update-scope-schema generator', () => {
let appTree: Tree;
beforeEach(async () => {
appTree = createTreeWithEmptyWorkspace();
await addUtilLibProject(appTree);
await libraryGenerator(appTree, { name: 'foo', tags: 'scope:foo' });
await libraryGenerator(appTree, { name: 'bar', tags: 'scope:bar' });
});
it('should adjust the util-lib generator based on existing projects', async () => {
await generator(appTree);
const schemaJson = readJson(
appTree,
'libs/internal-plugin/src/generators/util-lib/schema.json'
);
expect(schemaJson.properties.directory['x-prompt'].items).toEqual([
{
value: 'foo',
label: 'foo',
},
{
value: 'bar',
label: 'bar',
},
]);
const schemaInterface = appTree.read(
'libs/internal-plugin/src/generators/util-lib/schema.d.ts',
'utf-8'
);
expect(schemaInterface).toContain(`export interface Schema {
name: string;
directory: 'foo' | 'bar';
}`);
});
});
async function addUtilLibProject(tree: Tree) {
await pluginGenerator(tree, {
name: 'internal-plugin',
directory: 'libs/internal-plugin'
skipTsConfig: false,
unitTestRunner: 'jest',
linter: Linter.EsLint,
compiler: 'tsc',
skipFormat: false,
skipLintChecks: false,
minimal: true,
});
await generatorGenerator(tree, {
name: 'util-lib',
directory: 'libs/internal-plugin/src/generators/util-lib',
unitTestRunner: 'jest',
});
const filesToCopy = [
'../util-lib/generator.ts',
'../util-lib/schema.json',
'../util-lib/schema.d.ts',
];
for (const file of filesToCopy) {
tree.write(
`libs/internal-plugin/src/generators/util-lib/${file}`,
readFileSync(join(__dirname, file))
);
}
}
You learned how to generate and test complex generators. In the next step we will learn how to use run-commands
executor and build our own custom executor.