From 5e2ee707cfac6e890ca56291b6da6dff3579fd63 Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Tue, 18 Oct 2022 13:58:06 -0700 Subject: [PATCH] feat(core): add generator to migrate workspace generators to a local plugin --- packages/nx-plugin/.eslintrc.json | 3 +- packages/nx-plugin/generators.json | 5 + .../files/generators.json__tmpl__ | 16 ++ .../local-plugin-from-tools/generator.spec.ts | 173 ++++++++++++++++++ .../local-plugin-from-tools/generator.ts | 135 ++++++++++++++ .../local-plugin-from-tools/schema.d.ts | 5 + .../local-plugin-from-tools/schema.json | 24 +++ ...roject-configuration-in-new-destination.ts | 14 +- .../generators/move/lib/normalize-schema.ts | 3 +- .../src/generators/move/lib/update-imports.ts | 4 +- .../src/generators/move/lib/utils.ts | 4 + .../workspace/src/generators/move/schema.d.ts | 3 +- 12 files changed, 379 insertions(+), 10 deletions(-) create mode 100644 packages/nx-plugin/src/generators/local-plugin-from-tools/files/generators.json__tmpl__ create mode 100644 packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts create mode 100644 packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts create mode 100644 packages/nx-plugin/src/generators/local-plugin-from-tools/schema.d.ts create mode 100644 packages/nx-plugin/src/generators/local-plugin-from-tools/schema.json diff --git a/packages/nx-plugin/.eslintrc.json b/packages/nx-plugin/.eslintrc.json index cc515a27eb399b..48c388aedd5a72 100644 --- a/packages/nx-plugin/.eslintrc.json +++ b/packages/nx-plugin/.eslintrc.json @@ -23,8 +23,7 @@ "error", "@angular-devkit/architect", "@angular-devkit/core", - "@angular-devkit/schematics", - "@nrwl/workspace" + "@angular-devkit/schematics" ] } }, diff --git a/packages/nx-plugin/generators.json b/packages/nx-plugin/generators.json index b889b233cf3b86..456e88aab7f4cb 100644 --- a/packages/nx-plugin/generators.json +++ b/packages/nx-plugin/generators.json @@ -61,6 +61,11 @@ "factory": "./src/generators/executor/executor#executorSchematic", "schema": "./src/generators/executor/schema.json", "description": "Create an executor for an Nx Plugin." + }, + "local-plugin-from-tools": { + "factory": "./src/generators/local-plugin-from-tools/generator", + "schema": "./src/generators/local-plugin-from-tools/schema.json", + "description": "Migrate existing workspace-generators to a workspace-tools plugin" } } } diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/files/generators.json__tmpl__ b/packages/nx-plugin/src/generators/local-plugin-from-tools/files/generators.json__tmpl__ new file mode 100644 index 00000000000000..221a89c6354510 --- /dev/null +++ b/packages/nx-plugin/src/generators/local-plugin-from-tools/files/generators.json__tmpl__ @@ -0,0 +1,16 @@ +{ + "name": "<%= importPath %>", + "generators": { + <% generators.forEach((generator, idx) => { -%> + "<%= generator.name %>": { + "implementation": "<%= generator.implementation %>", + "schema": "<%= generator.schema %>", + <% if( generator.description ) { -%> + "description": "<%= generator.description %>" + <%_ } -%> + <% if( generator.cli ) { -%> + "cli": "<%= generator.cli %>" + <%_ } -%> + }<%= (idx < generators.length - 1) ? "," : "" %><% }) -%> + } +} diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts b/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts new file mode 100644 index 00000000000000..ebed7552062481 --- /dev/null +++ b/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts @@ -0,0 +1,173 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + Tree, + readProjectConfiguration, + readJson, + joinPathFragments, + GeneratorsJson, + ProjectConfiguration, + getProjects, +} from '@nrwl/devkit'; + +import generator from './generator'; +import workspaceGeneratorGenerator from '@nrwl/workspace/src/generators/workspace-generator/workspace-generator'; +import { LocalPluginFromToolsGeneratorSchema } from './schema'; + +describe('local-plugin-from-tools generator', () => { + let tree: Tree; + + const createOptions = ( + overrides?: Partial + ): Partial => ({ + ...overrides, + }); + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should find single workspace generator successfully', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + skipFormat: false, + }); + await generator(tree, createOptions()); + const config = readProjectConfiguration(tree, 'workspace-plugin'); + expect(config.root).toEqual('tools/workspace-plugin'); + + assertValidGenerator(tree, config, 'my-generator'); + }); + + it('should convert multiple workspace generators successfully', async () => { + const generators = [...new Array(10)].map((x) => uniq('generator')); + for (const name of generators) { + await workspaceGeneratorGenerator(tree, { + name, + skipFormat: false, + }); + } + + await generator(tree, createOptions()); + + const config = readProjectConfiguration(tree, 'workspace-plugin'); + expect(config.root).toEqual('tools/workspace-plugin'); + + for (const generator of generators) { + assertValidGenerator(tree, config, generator); + } + }); + + describe('--plugin-name', () => { + it('should obey project name', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + skipFormat: false, + }); + await generator( + tree, + createOptions({ + pluginName: 'workspace-tools', + }) + ); + + const config = readProjectConfiguration(tree, 'workspace-tools'); + expect(config.root).toEqual('tools/workspace-tools'); + + assertValidGenerator(tree, config, 'my-generator'); + }); + }); + + describe('--tools-project-root', () => { + it('should place plugin in specified root', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + skipFormat: false, + }); + await generator( + tree, + createOptions({ + toolsProjectRoot: 'libs/workspace-plugin', + }) + ); + const config = readProjectConfiguration(tree, 'workspace-plugin'); + expect(config.root).toEqual('libs/workspace-plugin'); + + assertValidGenerator(tree, config, 'my-generator'); + }); + }); + + describe('--import-path', () => { + it('should support basic import paths', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + skipFormat: false, + }); + await generator( + tree, + createOptions({ + importPath: 'workspace-tools', + }) + ); + + const config = readProjectConfiguration(tree, 'workspace-plugin'); + expect(config.root).toEqual('tools/workspace-plugin'); + expect( + readJson(tree, 'tsconfig.base.json').compilerOptions.paths[ + 'workspace-tools' + ] + ).toEqual(['tools/workspace-plugin/src/index.ts']); + + assertValidGenerator(tree, config, 'my-generator'); + }); + + it('should support scoped import paths', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + skipFormat: false, + }); + await generator( + tree, + createOptions({ + importPath: '@workspace/plugin', + }) + ); + + const config = readProjectConfiguration(tree, 'workspace-plugin'); + expect(config.root).toEqual('tools/workspace-plugin'); + expect( + readJson(tree, 'tsconfig.base.json').compilerOptions.paths[ + '@workspace/plugin' + ] + ).toEqual(['tools/workspace-plugin/src/index.ts']); + + assertValidGenerator(tree, config, 'my-generator'); + }); + }); +}); + +function assertValidGenerator( + tree: Tree, + config: ProjectConfiguration, + generator: string +) { + const generatorsJson = readJson( + tree, + joinPathFragments(config.root, 'generators.json') + ); + expect(generatorsJson.generators).toHaveProperty(generator); + const generatorImplPath = joinPathFragments( + config.root, + generatorsJson.generators[generator].implementation, + 'index.ts' + ); + expect(tree.exists(generatorImplPath)).toBeTruthy(); + const generatorSchemaPath = joinPathFragments( + config.root, + generatorsJson.generators[generator].schema + ); + expect(tree.exists(generatorSchemaPath)).toBeTruthy(); +} + +function uniq(prefix: string) { + return `${prefix}${Math.floor(Math.random() * 10000000)}`; +} diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts b/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts new file mode 100644 index 00000000000000..950b9150cc6f2d --- /dev/null +++ b/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts @@ -0,0 +1,135 @@ +import { + formatFiles, + generateFiles, + getWorkspaceLayout, + joinPathFragments, + readJson, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import pluginGenerator from '../plugin/plugin'; +import * as path from 'path'; +import { LocalPluginFromToolsGeneratorSchema } from './schema'; +import { Linter } from '@nrwl/linter'; +import { GeneratorsJsonEntry } from 'nx/src/config/misc-interfaces'; +import { moveGenerator } from '@nrwl/workspace'; + +function normalizeOptions( + tree: Tree, + schema: Partial +): LocalPluginFromToolsGeneratorSchema { + const { npmScope } = getWorkspaceLayout(tree); + const pluginName = schema.pluginName ?? 'workspace-plugin'; + + return { + importPath: schema.importPath ?? `@${npmScope}/${pluginName}`, + pluginName, + toolsProjectRoot: + schema.toolsProjectRoot ?? joinPathFragments('tools', pluginName), + }; +} + +function addFiles( + tree: Tree, + options: LocalPluginFromToolsGeneratorSchema, + generators: (GeneratorsJsonEntry & { name: string })[] +) { + const templateOptions = { + ...options, + generators, + tmpl: '', + }; + generateFiles( + tree, + path.join(__dirname, 'files'), + options.toolsProjectRoot, + templateOptions + ); +} + +export default async function ( + tree: Tree, + options: Partial +) { + const normalizedOptions = normalizeOptions(tree, options); + await pluginGenerator(tree, { + minimal: true, + name: normalizedOptions.pluginName, + importPath: normalizedOptions.importPath, + skipTsConfig: false, + compiler: 'tsc', + linter: Linter.EsLint, + skipFormat: true, + skipLintChecks: false, + unitTestRunner: 'jest', + }); + await moveGeneratedPlugin(tree, normalizedOptions); + addFiles( + tree, + normalizedOptions, + collectAndMoveGenerators(tree, normalizedOptions) + ); + await formatFiles(tree); +} + +// Inspired by packages/nx/src/command-line/workspace-generators.ts +function collectAndMoveGenerators( + tree: Tree, + options: LocalPluginFromToolsGeneratorSchema +) { + const generators: ({ + name: string; + } & GeneratorsJsonEntry)[] = []; + const generatorsDir = 'tools/generators'; + const destinationDir = joinPathFragments( + readProjectConfiguration(tree, options.pluginName).root, + 'src', + 'generators' + ); + for (const c of tree.children('tools/generators')) { + const childDir = path.join(generatorsDir, c); + const schemaPath = joinPathFragments(childDir, 'schema.json'); + if (tree.exists(schemaPath)) { + const schema = readJson(tree, schemaPath); + generators.push({ + name: c, + implementation: `./src/generators/${c}`, + schema: `./src/generators/${joinPathFragments(c, 'schema.json')}`, + description: schema.description ?? `Generator ${c}`, + }); + moveFolder(tree, childDir, joinPathFragments(destinationDir, c)); + } + } + return generators; +} + +function moveFolder(tree: Tree, source: string, destination: string) { + for (const child of tree.children(source)) { + const existingPath = joinPathFragments(source, child); + const newPath = joinPathFragments(destination, child); + if (tree.isFile(existingPath)) { + tree.write(newPath, tree.read(existingPath)); + tree.delete(existingPath); + } else { + moveFolder(tree, existingPath, newPath); + } + } + tree.delete(source); +} + +function moveGeneratedPlugin( + tree: Tree, + options: LocalPluginFromToolsGeneratorSchema +) { + const config = readProjectConfiguration(tree, options.pluginName); + if (config.root !== options.toolsProjectRoot) { + return moveGenerator(tree, { + destination: options.toolsProjectRoot, + projectName: options.pluginName, + newProjectName: options.pluginName, + updateImportPath: true, + destinationRelativeToRoot: true, + importPath: options.importPath, + }); + } +} diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/schema.d.ts b/packages/nx-plugin/src/generators/local-plugin-from-tools/schema.d.ts new file mode 100644 index 00000000000000..8491134980d80d --- /dev/null +++ b/packages/nx-plugin/src/generators/local-plugin-from-tools/schema.d.ts @@ -0,0 +1,5 @@ +export interface LocalPluginFromToolsGeneratorSchema { + pluginName: string; + importPath: string; + toolsProjectRoot: string; +} diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/schema.json b/packages/nx-plugin/src/generators/local-plugin-from-tools/schema.json new file mode 100644 index 00000000000000..6676f0902d4e0b --- /dev/null +++ b/packages/nx-plugin/src/generators/local-plugin-from-tools/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "LocalPluginFromTools", + "title": "", + "type": "object", + "description": "Creates a local plugin project from existing workspace generators.", + "properties": { + "pluginName": { + "type": "string", + "description": "The name of the plugin created to contain the workspace generators.", + "default": "nx-plugin" + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like @myorg/my-awesome-lib." + }, + "toolsProjectRoot": { + "type": "string", + "description": "Where the new file should be located. Defaults to tools/workspace-tools." + } + }, + "required": ["pluginName"] +} diff --git a/packages/workspace/src/generators/move/lib/create-project-configuration-in-new-destination.ts b/packages/workspace/src/generators/move/lib/create-project-configuration-in-new-destination.ts index 5c7d4a569d4b50..c0bc17acbbafd6 100644 --- a/packages/workspace/src/generators/move/lib/create-project-configuration-in-new-destination.ts +++ b/packages/workspace/src/generators/move/lib/create-project-configuration-in-new-destination.ts @@ -10,17 +10,21 @@ export function createProjectConfigurationInNewDestination( schema: NormalizedSchema, projectConfig: ProjectConfiguration ) { - if (projectConfig.name) { - projectConfig.name = schema.newProjectName; - } + projectConfig.name = schema.newProjectName; + + // Subtle bug if project name === path, where the updated name was being overrideen. + const { name, ...rest } = projectConfig; // replace old root path with new one - const projectString = JSON.stringify(projectConfig); + const projectString = JSON.stringify(rest); const newProjectString = projectString.replace( new RegExp(projectConfig.root, 'g'), schema.relativeToRootDestination ); - const newProject: ProjectConfiguration = JSON.parse(newProjectString); + const newProject: ProjectConfiguration = { + name, + ...JSON.parse(newProjectString), + }; // Create a new project with the root replaced addProjectConfiguration(tree, schema.newProjectName, newProject); diff --git a/packages/workspace/src/generators/move/lib/normalize-schema.ts b/packages/workspace/src/generators/move/lib/normalize-schema.ts index ce128e78af1fc2..c2d030c48cc0d5 100644 --- a/packages/workspace/src/generators/move/lib/normalize-schema.ts +++ b/packages/workspace/src/generators/move/lib/normalize-schema.ts @@ -11,7 +11,8 @@ export function normalizeSchema( const destination = schema.destination.startsWith('/') ? normalizeSlashes(schema.destination.slice(1)) : schema.destination; - const newProjectName = getNewProjectName(destination); + const newProjectName = + schema.newProjectName ?? getNewProjectName(destination); const { npmScope } = getWorkspaceLayout(tree); return { diff --git a/packages/workspace/src/generators/move/lib/update-imports.ts b/packages/workspace/src/generators/move/lib/update-imports.ts index 37ebecf7c30727..c7b08858989076 100644 --- a/packages/workspace/src/generators/move/lib/update-imports.ts +++ b/packages/workspace/src/generators/move/lib/update-imports.ts @@ -108,7 +108,9 @@ export function updateImports( if (schema.updateImportPath) { tsConfig.compilerOptions.paths[projectRef.to] = updatedPath; - delete tsConfig.compilerOptions.paths[projectRef.from]; + if (projectRef.from !== projectRef.to) { + delete tsConfig.compilerOptions.paths[projectRef.from]; + } } else { tsConfig.compilerOptions.paths[projectRef.from] = updatedPath; } diff --git a/packages/workspace/src/generators/move/lib/utils.ts b/packages/workspace/src/generators/move/lib/utils.ts index 26b5453c777b96..cb8f54f8b318fe 100644 --- a/packages/workspace/src/generators/move/lib/utils.ts +++ b/packages/workspace/src/generators/move/lib/utils.ts @@ -19,6 +19,10 @@ export function getDestination( schema: Schema, project: ProjectConfiguration ): string { + if (schema.destinationRelativeToRoot) { + return schema.destination; + } + const projectType = project.projectType; const workspaceLayout = getWorkspaceLayout(host); diff --git a/packages/workspace/src/generators/move/schema.d.ts b/packages/workspace/src/generators/move/schema.d.ts index 53a360d1709dd3..24c54abdf49b19 100644 --- a/packages/workspace/src/generators/move/schema.d.ts +++ b/packages/workspace/src/generators/move/schema.d.ts @@ -4,10 +4,11 @@ export interface Schema { importPath?: string; updateImportPath: boolean; skipFormat?: boolean; + destinationRelativeToRoot?: boolean; + newProjectName?: string; } export interface NormalizedSchema extends Schema { importPath: string; - newProjectName: string; relativeToRootDestination: string; }