From 1743ff10ed3cc3ab6bbc50a849a0ec3a1298df1e Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Wed, 19 Apr 2023 14:03:00 -0400 Subject: [PATCH] feat(core): add migration to update workspace generators to a local plugin (#12700) --- docs/generated/cli/workspace-generator.md | 36 ++- .../nx/documents/workspace-generator.md | 36 ++- .../generators/workspace-generator.json | 15 +- .../shared/deprecated/workspace-generators.md | 4 + e2e/nx-misc/src/misc.test.ts | 1 - e2e/nx-misc/src/workspace.test.ts | 177 ------------- e2e/nx-plugin/src/nx-plugin.test.ts | 25 +- packages/nx-plugin/.eslintrc.json | 3 +- .../nx-plugin/src/generators/plugin/plugin.ts | 2 + packages/nx/src/command-line/nx-commands.ts | 152 +----------- .../src/command-line/workspace-generators.ts | 234 +++--------------- packages/workspace/migrations.json | 6 + ...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 +- .../files/index.ts__tmpl__ | 10 - .../files/schema.json__tmpl__ | 17 -- .../workspace-generator/schema.d.ts | 5 +- .../workspace-generator/schema.json | 18 +- .../workspace-generator.spec.ts | 17 -- .../workspace-generator.ts | 45 +--- ...rkspace-generators-to-local-plugin.spec.ts | 174 +++++++++++++ ...ve-workspace-generators-to-local-plugin.ts | 188 ++++++++++++++ scripts/depcheck/missing.ts | 10 + 26 files changed, 538 insertions(+), 665 deletions(-) delete mode 100644 packages/workspace/src/generators/workspace-generator/files/index.ts__tmpl__ delete mode 100644 packages/workspace/src/generators/workspace-generator/files/schema.json__tmpl__ delete mode 100644 packages/workspace/src/generators/workspace-generator/workspace-generator.spec.ts create mode 100644 packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts create mode 100644 packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts diff --git a/docs/generated/cli/workspace-generator.md b/docs/generated/cli/workspace-generator.md index 1c471c86aac55..b5332ac1316c4 100644 --- a/docs/generated/cli/workspace-generator.md +++ b/docs/generated/cli/workspace-generator.md @@ -5,7 +5,9 @@ description: 'Runs a workspace generator from the tools/generators directory' # workspace-generator -Runs a workspace generator from the tools/generators directory + **Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators + + Runs a workspace generator from the tools/generators directory ## Usage @@ -17,23 +19,45 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx` ## Options +### dryRun + +Type: `boolean` + +Default: `false` + +Preview the changes without updating files + +### generator + +Type: `string` + +Name of the generator (e.g., @nrwl/js:library, library) + ### help Type: `boolean` Show help -### list-generators +### interactive Type: `boolean` -List the available workspace-generators +Default: `true` -### name +When false disables interactive input prompts for options -Type: `string` +### quiet + +Type: `boolean` + +Hides logs from tree operations (e.g. `CREATE package.json`) + +### verbose + +Type: `boolean` -The name of your generator +Prints additional information about the commands (e.g., stack traces) ### version diff --git a/docs/generated/packages/nx/documents/workspace-generator.md b/docs/generated/packages/nx/documents/workspace-generator.md index 1c471c86aac55..b5332ac1316c4 100644 --- a/docs/generated/packages/nx/documents/workspace-generator.md +++ b/docs/generated/packages/nx/documents/workspace-generator.md @@ -5,7 +5,9 @@ description: 'Runs a workspace generator from the tools/generators directory' # workspace-generator -Runs a workspace generator from the tools/generators directory + **Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators + + Runs a workspace generator from the tools/generators directory ## Usage @@ -17,23 +19,45 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx` ## Options +### dryRun + +Type: `boolean` + +Default: `false` + +Preview the changes without updating files + +### generator + +Type: `string` + +Name of the generator (e.g., @nrwl/js:library, library) + ### help Type: `boolean` Show help -### list-generators +### interactive Type: `boolean` -List the available workspace-generators +Default: `true` -### name +When false disables interactive input prompts for options -Type: `string` +### quiet + +Type: `boolean` + +Hides logs from tree operations (e.g. `CREATE package.json`) + +### verbose + +Type: `boolean` -The name of your generator +Prints additional information about the commands (e.g., stack traces) ### version diff --git a/docs/generated/packages/workspace/generators/workspace-generator.json b/docs/generated/packages/workspace/generators/workspace-generator.json index a3d70312514e7..af1e8ac5ab566 100644 --- a/docs/generated/packages/workspace/generators/workspace-generator.json +++ b/docs/generated/packages/workspace/generators/workspace-generator.json @@ -7,20 +7,7 @@ "title": "Create a custom generator", "description": "Create a custom generator.", "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Generator name.", - "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What name would you like to use for the workspace generator?" - }, - "skipFormat": { - "description": "Skip formatting files.", - "type": "boolean", - "default": false, - "x-priority": "internal" - } - }, + "properties": {}, "required": ["name"], "presets": [] }, diff --git a/docs/shared/deprecated/workspace-generators.md b/docs/shared/deprecated/workspace-generators.md index bc98c91f04258..92b8da8b25297 100644 --- a/docs/shared/deprecated/workspace-generators.md +++ b/docs/shared/deprecated/workspace-generators.md @@ -8,6 +8,10 @@ Check the [nx-plugin guide](/packages/nx-plugin) for information on creating a n ## Converting workspace generators to local generators +{% callout type=\"info\" %} +When migrating to Nx 16, a new workspace plugin is automatically generated in the tools folder if you already have workspace-generators. +{% /callout %} + - If you don't already have a local plugin, use Nx to generate one: ```shell diff --git a/e2e/nx-misc/src/misc.test.ts b/e2e/nx-misc/src/misc.test.ts index aee625037171e..5c3ac777b252d 100644 --- a/e2e/nx-misc/src/misc.test.ts +++ b/e2e/nx-misc/src/misc.test.ts @@ -86,7 +86,6 @@ describe('Nx Commands', () => { // check for schematics expect(listOutput).toContain('workspace'); expect(listOutput).toContain('library'); - expect(listOutput).toContain('workspace-generator'); // check for builders expect(listOutput).toContain('run-commands'); diff --git a/e2e/nx-misc/src/workspace.test.ts b/e2e/nx-misc/src/workspace.test.ts index e15b3144734cd..57d4c68554f53 100644 --- a/e2e/nx-misc/src/workspace.test.ts +++ b/e2e/nx-misc/src/workspace.test.ts @@ -756,180 +756,3 @@ describe('Workspace Tests', () => { }); }); }); - -describe('workspace-generator', () => { - const packageManager = getSelectedPackageManager() || 'pnpm'; - const proj = uniq('workspace'); - - beforeAll(() => { - runCreateWorkspace(proj, { - preset: 'ts', - packageManager, - }); - }); - - afterAll(() => cleanupProject()); - - let custom: string; - let failing: string; - - beforeEach(() => { - custom = uniq('custom'); - failing = uniq('custom-failing'); - runCLI(`g @nrwl/workspace:workspace-generator ${custom} --no-interactive`); - runCLI(`g @nrwl/workspace:workspace-generator ${failing} --no-interactive`); - - checkFilesExist( - `tools/generators/${custom}/index.ts`, - `tools/generators/${custom}/schema.json` - ); - checkFilesExist( - `tools/generators/${failing}/index.ts`, - `tools/generators/${failing}/schema.json` - ); - }); - - it('should compile only generator files with dependencies', () => { - const workspace = uniq('workspace'); - - updateFile( - 'tools/utils/command-line-utils.ts', - ` - export const noop = () => {} - ` - ); - updateFile( - 'tools/utils/logger.ts', - ` - export const log = (...args: any[]) => console.log(...args) - ` - ); - updateFile( - `tools/generators/utils.ts`, - ` - export const noop = ()=>{} - ` - ); - updateFile(`tools/generators/${custom}/index.ts`, (content) => { - return ` - import { log } from '../../utils/logger'; \n - ${content} - `; - }); - - runCLI(`workspace-generator ${custom} ${workspace} --no-interactive -d`); - - expect(() => - checkFilesExist( - `dist/out-tsc/tools/generators/${custom}/index.js`, - `dist/out-tsc/tools/generators/utils.js`, - `dist/out-tsc/tools/utils/logger.js` - ) - ).not.toThrow(); - expect(() => - checkFilesExist(`dist/out-tsc/tools/utils/utils.js`) - ).toThrow(); - }); - - it('should support workspace-specific generators', async () => { - const json = readJson(`tools/generators/${custom}/schema.json`); - json.properties['directory'] = { - type: 'string', - description: 'lib directory', - }; - json.properties['skipTsConfig'] = { - type: 'boolean', - description: 'skip changes to tsconfig', - }; - json.properties['inlineprop'] = json.properties['name']; - json.required = ['inlineprop']; - delete json.properties['name']; - - updateFile(`tools/generators/${custom}/schema.json`, JSON.stringify(json)); - - const indexFile = readFile(`tools/generators/${custom}/index.ts`); - updateFile( - `tools/generators/${custom}/index.ts`, - indexFile.replace( - 'name: schema.name', - 'name: schema.inlineprop, directory: schema.directory, skipTsConfig: schema.skipTsConfig' - ) - ); - - const helpOutput = runCLI(`workspace-generator ${custom} --help`); - expect(helpOutput).toContain( - `workspace-generator ${custom} [inlineprop] (options)` - ); - expect(helpOutput).toContain(`--directory`); - expect(helpOutput).toContain(`--skipTsConfig`); - - const workspace = uniq('workspace'); - const dryRunOutput = runCLI( - `workspace-generator ${custom} ${workspace} --no-interactive --directory=dir --skipTsConfig=true -d` - ); - expect(exists(`packages/dir/${workspace}/src/index.ts`)).toEqual(false); - expect(dryRunOutput).toContain( - `CREATE packages/dir/${workspace}/src/index.ts` - ); - - runCLI( - `workspace-generator ${custom} ${workspace} --no-interactive --directory=dir` - ); - checkFilesExist(`packages/dir/${workspace}/src/index.ts`); - - const jsonFailing = readJson(`tools/generators/${failing}/schema.json`); - jsonFailing.properties = {}; - jsonFailing.required = []; - updateFile( - `tools/generators/${failing}/schema.json`, - JSON.stringify(jsonFailing) - ); - - updateFile( - `tools/generators/${failing}/index.ts`, - ` - export default function() { - throw new Error(); - } - ` - ); - - try { - await runCLI(`workspace-generator ${failing} --no-interactive`); - fail(`Should exit 1 for a workspace-generator that throws an error`); - } catch (e) {} - - const listOutput = runCLI('workspace-generator --list-generators'); - expect(listOutput).toContain(custom); - expect(listOutput).toContain(failing); - }, 1000000); - - it('should support angular devkit schematics', () => { - const angularDevkitSchematic = uniq('angular-devkit-schematic'); - runCLI( - `g @nrwl/workspace:workspace-generator ${angularDevkitSchematic} --no-interactive` - ); - - const json = readJson( - `tools/generators/${angularDevkitSchematic}/schema.json` - ); - json.properties = {}; - json.required = []; - delete json.cli; - updateFile( - `tools/generators/${angularDevkitSchematic}/schema.json`, - JSON.stringify(json) - ); - - updateFile( - `tools/generators/${angularDevkitSchematic}/index.ts`, - ` - export default function() { - return (tree) => tree; - } - ` - ); - - runCLI(`workspace-generator ${angularDevkitSchematic} --no-interactive`); - }); -}); diff --git a/e2e/nx-plugin/src/nx-plugin.test.ts b/e2e/nx-plugin/src/nx-plugin.test.ts index 5c32f5392e929..139b767ae3453 100644 --- a/e2e/nx-plugin/src/nx-plugin.test.ts +++ b/e2e/nx-plugin/src/nx-plugin.test.ts @@ -259,9 +259,6 @@ describe('Nx Plugin', () => { expect(results).not.toContain(goodMigration); }); - /** - * @todo(@AgentEnder): reenable after figuring out @swc-node - */ describe('local plugins', () => { let plugin: string; beforeEach(() => { @@ -368,6 +365,28 @@ describe('Nx Plugin', () => { }); }); + describe('workspace-generator', () => { + let custom: string; + + it('should work with generate wrapper', () => { + custom = uniq('custom'); + const project = uniq('generated-project'); + runCLI(`g @nrwl/nx-plugin:plugin workspace-plugin --no-interactive`); + runCLI( + `g @nrwl/nx-plugin:generator ${custom} --project workspace-plugin --no-interactive` + ); + runCLI( + `workspace-generator ${custom} --name ${project} --no-interactive` + ); + expect(() => { + checkFilesExist( + `libs/${project}/src/index.ts`, + `libs/${project}/project.json` + ); + }); + }); + }); + describe('--directory', () => { it('should create a plugin in the specified directory', () => { const plugin = uniq('plugin'); diff --git a/packages/nx-plugin/.eslintrc.json b/packages/nx-plugin/.eslintrc.json index 1c6dadd2c16dc..48c388aedd5a7 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", - "@nx/workspace" + "@angular-devkit/schematics" ] } }, diff --git a/packages/nx-plugin/src/generators/plugin/plugin.ts b/packages/nx-plugin/src/generators/plugin/plugin.ts index 2d160d1f560c2..0c9a164db7590 100644 --- a/packages/nx-plugin/src/generators/plugin/plugin.ts +++ b/packages/nx-plugin/src/generators/plugin/plugin.ts @@ -22,6 +22,7 @@ import { addTsLibDependencies } from '@nx/js/src/utils/typescript/add-tslib-depe import { addSwcRegisterDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; import type { Schema } from './schema'; +import { tsLibVersion } from '@nx/js/src/utils/versions'; const nxVersion = require('../../../package.json').version; @@ -87,6 +88,7 @@ export async function pluginGenerator(host: Tree, schema: Schema) { addDependenciesToPackageJson( host, { + tslib: tsLibVersion, '@nx/devkit': nxVersion, }, { diff --git a/packages/nx/src/command-line/nx-commands.ts b/packages/nx/src/command-line/nx-commands.ts index 2cfa8279f8348..c2c1872ff69eb 100644 --- a/packages/nx/src/command-line/nx-commands.ts +++ b/packages/nx/src/command-line/nx-commands.ts @@ -266,16 +266,17 @@ export const commandsObject = yargs process.exit(0); }, }) - + /** + * @deprecated(v17): Remove `workspace-generator in v17. Use local plugins. + */ .command({ command: 'workspace-generator [name]', describe: 'Runs a workspace generator from the tools/generators directory', + deprecated: + 'Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators', aliases: ['workspace-schematic [name]'], builder: async (yargs) => - linkToNxDevAndExamples( - await withWorkspaceGeneratorOptions(yargs, process.argv.slice(3)), - 'workspace-generator' - ), + linkToNxDevAndExamples(withGenerateOptions(yargs), 'workspace-generator'), handler: workspaceGeneratorHandler, }) .command({ @@ -824,142 +825,11 @@ function withRunOneOptions(yargs: yargs.Argv) { } } -type OptionArgumentDefinition = { - type: yargs.Options['type']; - describe?: string; - default?: any; - choices?: yargs.Options['type'][]; - demandOption?: boolean; -}; - -type WorkspaceGeneratorProperties = { - [name: string]: - | { - type: yargs.Options['type']; - description?: string; - default?: any; - enum?: yargs.Options['type'][]; - demandOption?: boolean; - } - | { - type: yargs.PositionalOptionsType; - description?: string; - default?: any; - enum?: yargs.PositionalOptionsType[]; - $default: { - $source: 'argv'; - index: number; - }; - }; -}; - -function isPositionalProperty( - property: WorkspaceGeneratorProperties[keyof WorkspaceGeneratorProperties] -): property is { type: yargs.PositionalOptionsType } { - return property['$default']?.['$source'] === 'argv'; -} - -async function withWorkspaceGeneratorOptions( - yargs: yargs.Argv, - args: string[] -) { - // filter out only positional arguments - args = args.filter((a) => !a.startsWith('-')); - if (args.length) { - // this is an actual workspace generator - return withCustomGeneratorOptions(yargs, args[0]); - } else { - yargs - .option('list-generators', { - describe: 'List the available workspace-generators', - type: 'boolean', - }) - .positional('name', { - type: 'string', - describe: 'The name of your generator', - }); - /** - * Don't require `name` if only listing available - * schematics - */ - if ((await yargs.argv).listGenerators !== true) { - yargs.demandOption('name'); - } - return yargs; - } -} - -async function withCustomGeneratorOptions( - yargs: yargs.Argv, - generatorName: string -) { - const schema = ( - await import('./workspace-generators') - ).workspaceGeneratorSchema(generatorName); - const options = []; - const positionals = []; - - Object.entries( - (schema.properties ?? {}) as WorkspaceGeneratorProperties - ).forEach(([name, prop]) => { - const option: { name: string; definition: OptionArgumentDefinition } = { - name, - definition: { - describe: prop.description, - type: prop.type, - default: prop.default, - choices: prop.enum, - }, - }; - if (schema.required && schema.required.includes(name)) { - option.definition.demandOption = true; - } - options.push(option); - if (isPositionalProperty(prop)) { - positionals.push({ - name, - definition: { - describe: prop.description, - type: prop.type, - choices: prop.enum, - }, - }); - } - }); - - let command = generatorName; - positionals.forEach(({ name }) => { - command += ` [${name}]`; - }); - if (options.length) { - command += ' (options)'; - } - - yargs - .command({ - // this is the default and only command - command, - describe: schema.description || '', - builder: (y) => { - options.forEach(({ name, definition }) => { - y.option(name, definition); - }); - positionals.forEach(({ name, definition }) => { - y.positional(name, definition); - }); - return linkToNxDevAndExamples(y, 'workspace-generator'); - }, - handler: workspaceGeneratorHandler, - }) - .fail(() => void 0); // no action is needed on failure as Nx will handle it based on schema validation - - return yargs; -} - -async function workspaceGeneratorHandler() { - await ( - await import('./workspace-generators') - ).workspaceGenerators(process.argv.slice(3)); +/** + * @deprecated(v17): Remove `workspace-generator in v17. Use local plugins. + */ +async function workspaceGeneratorHandler(args: yargs.Arguments) { + await (await import('./workspace-generators')).workspaceGenerators(args); process.exit(0); } diff --git a/packages/nx/src/command-line/workspace-generators.ts b/packages/nx/src/command-line/workspace-generators.ts index 769f9407d2168..df8981abff628 100644 --- a/packages/nx/src/command-line/workspace-generators.ts +++ b/packages/nx/src/command-line/workspace-generators.ts @@ -1,213 +1,35 @@ -import * as chalk from 'chalk'; -import { execSync } from 'child_process'; -import { readdirSync, existsSync } from 'fs'; -import { copySync, removeSync } from 'fs-extra'; -import * as path from 'path'; -import * as yargsParser from 'yargs-parser'; -import { workspaceRoot } from '../utils/workspace-root'; -import { fileExists } from '../utils/fileutils'; +import yargs = require('yargs'); +import { readNxJson } from '../config/configuration'; +import { NxJsonConfiguration } from '../devkit-exports'; +import { NX_PREFIX } from '../utils/logger'; import { output } from '../utils/output'; -import type { CompilerOptions } from 'typescript'; -import { generate } from './generate'; -import { readJsonFile, writeJsonFile } from '../utils/fileutils'; -import { logger } from '../utils/logger'; -import { getPackageManagerCommand } from '../utils/package-manager'; -import { normalizePath } from '../utils/path'; -import { parserConfiguration } from './nx-commands'; -const rootDirectory = workspaceRoot; -const toolsDir = path.join(rootDirectory, 'tools'); -const generatorsDir = path.join(toolsDir, 'generators'); -const toolsTsConfigPath = path.join(toolsDir, 'tsconfig.tools.json'); - -type TsConfig = { - extends: string; - compilerOptions: CompilerOptions; - files?: string[]; - include?: string[]; - exclude?: string[]; - references?: Array<{ path: string }>; -}; - -export async function workspaceGenerators(args: string[]) { - const outDir = compileTools(); - const collectionFile = path.join(outDir, 'workspace-generators.json'); - const parsedArgs = parseOptions(args, outDir, collectionFile); - if (parsedArgs.listGenerators) { - return listGenerators(collectionFile); - } else { - process.exitCode = await generate(process.cwd(), parsedArgs); - } -} - -export function workspaceGeneratorSchema(name: string) { - const schemaFile = path.join(generatorsDir, name, 'schema.json'); - - if (fileExists(schemaFile)) { - return readJsonFile(schemaFile); - } else { - logger.error(`Cannot find schema for ${name}. Does the generator exist?`); - process.exit(1); - } -} - -// compile tools -function compileTools() { - const toolsOutDir = getToolsOutDir(); - removeSync(toolsOutDir); - compileToolsDir(toolsOutDir); - - const generatorsOutDir = path.join(toolsOutDir, 'generators'); - const collectionData = constructCollection(); - writeJsonFile( - path.join(generatorsOutDir, 'workspace-generators.json'), - collectionData - ); - return generatorsOutDir; -} - -function getToolsOutDir() { - const outDir = toolsTsConfig().compilerOptions.outDir; - - if (!outDir) { - logger.error(`${toolsTsConfigPath} must specify an outDir`); - process.exit(1); - } - - return path.resolve(toolsDir, outDir); -} - -function compileToolsDir(outDir: string) { - copySync(generatorsDir, path.join(outDir, 'generators')); - - const tmpTsConfigPath = createTmpTsConfig(toolsTsConfigPath, { - include: [path.join(generatorsDir, '**/*.ts')], - }); - - const pmc = getPackageManagerCommand(); - const tsc = `${pmc.exec} tsc`; - try { - execSync(`${tsc} -p ${tmpTsConfigPath}`, { - stdio: 'inherit', - cwd: rootDirectory, - }); - } catch { - process.exit(1); - } -} - -function constructCollection() { - const generators = {}; - const schematics = {}; - readdirSync(generatorsDir).forEach((c) => { - const childDir = path.join(generatorsDir, c); - if (existsSync(path.join(childDir, 'schema.json'))) { - const generatorOrSchematic = { - factory: `./${c}`, - schema: `./${normalizePath(path.join(c, 'schema.json'))}`, - description: `Schematic ${c}`, - }; - - const { isSchematic } = readJsonFile(path.join(childDir, 'schema.json')); - if (isSchematic) { - schematics[c] = generatorOrSchematic; - } else { - generators[c] = generatorOrSchematic; - } - } - }); - return { - name: 'workspace-generators', - version: '1.0', - generators, - schematics, - }; -} - -function toolsTsConfig(): TsConfig { - return readJsonFile(toolsTsConfigPath); -} - -function listGenerators(collectionFile: string) { - try { - const bodyLines: string[] = []; - - const collection = readJsonFile(collectionFile); - - bodyLines.push(chalk.bold(chalk.green('WORKSPACE GENERATORS'))); - bodyLines.push(''); - bodyLines.push( - ...Object.entries(collection.generators).map( - ([schematicName, schematicMeta]: [string, any]) => { - return `${chalk.bold(schematicName)} : ${schematicMeta.description}`; - } - ) - ); - bodyLines.push(''); - - output.log({ - title: '', - bodyLines, - }); - } catch (error) { - logger.fatal(error.message); - } -} - -function parseOptions( - args: string[], - outDir: string, - collectionFile: string -): { [k: string]: any } { - const schemaPath = path.join(outDir, args[0], 'schema.json'); - let booleanProps = []; - if (fileExists(schemaPath)) { - const { properties } = readJsonFile( - path.join(outDir, args[0], 'schema.json') - ); - if (properties) { - booleanProps = Object.keys(properties).filter( - (key) => properties[key].type === 'boolean' - ); - } - } - const parsed = yargsParser(args, { - boolean: ['dryRun', 'listGenerators', 'interactive', ...booleanProps], - alias: { - dryRun: ['d'], - listSchematics: ['l'], - }, - default: { - interactive: true, - }, - configuration: parserConfiguration, +/** + * Wraps `workspace-generator` to invoke `generate`. + * + * @deprecated(v17): Remove `workspace-generator in v17. Use local plugins. + */ +export async function workspaceGenerators(args: yargs.Arguments) { + const generator = process.argv.slice(3); + + output.warn({ + title: `${NX_PREFIX} Workspace Generators are no longer supported`, + bodyLines: [ + 'Instead, Nx now supports executing generators or executors from ', + 'local plugins. To run a generator from a local plugin, ', + 'use `nx generate` like you would with any other generator.', + '', + 'For more information, see: https://nx.dev/deprecated/workspace-generators', + ], }); - parsed['generator'] = `${collectionFile}:${parsed['_'][0]}`; - parsed['_'] = parsed['_'].slice(1); - return parsed; -} -function createTmpTsConfig( - tsconfigPath: string, - updateConfig: Partial -) { - const tmpTsConfigPath = path.join( - path.dirname(tsconfigPath), - 'tsconfig.generated.json' - ); - const originalTSConfig = readJsonFile(tsconfigPath); - const generatedTSConfig: TsConfig = { - ...originalTSConfig, - ...updateConfig, - }; - process.on('exit', () => cleanupTmpTsConfigFile(tmpTsConfigPath)); - writeJsonFile(tmpTsConfigPath, generatedTSConfig); + const nxJson: NxJsonConfiguration = readNxJson(); + const collection = nxJson.npmScope + ? `@${nxJson.npmScope}/workspace-plugin` + : 'workspace-plugin'; - return tmpTsConfigPath; -} + args._ = args._.slice(1); + args.generator = `${collection}:${generator}`; -function cleanupTmpTsConfigFile(tmpTsConfigPath: string) { - if (tmpTsConfigPath) { - removeSync(tmpTsConfigPath); - } + return (await import('./generate')).generate(process.cwd(), args); } diff --git a/packages/workspace/migrations.json b/packages/workspace/migrations.json index 52036a9620296..d78774ecc43b2 100644 --- a/packages/workspace/migrations.json +++ b/packages/workspace/migrations.json @@ -89,6 +89,12 @@ "version": "16.0.0-beta.1", "description": "Replace @nrwl/workspace with @nx/workspace", "implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages" + }, + "16-0-0-move-workspace-generators-into-local-plugin": { + "version": "16.0.0-beta.3", + "description": "Generates a plugin called 'workspace-plugin' containing your workspace generators.", + "cli": "nx", + "implementation": "./src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin" } }, "packageJsonUpdates": { 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 e8dd804227de1..8b059858b20d1 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 5a922e29c9fc3..48a95c2048d61 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 923edd59c2069..7470fe8b3bfa1 100644 --- a/packages/workspace/src/generators/move/lib/update-imports.ts +++ b/packages/workspace/src/generators/move/lib/update-imports.ts @@ -110,7 +110,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 2fc7d808c5def..284c04e1b8edd 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 53a360d1709dd..24c54abdf49b1 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; } diff --git a/packages/workspace/src/generators/workspace-generator/files/index.ts__tmpl__ b/packages/workspace/src/generators/workspace-generator/files/index.ts__tmpl__ deleted file mode 100644 index e33c339f6ca84..0000000000000 --- a/packages/workspace/src/generators/workspace-generator/files/index.ts__tmpl__ +++ /dev/null @@ -1,10 +0,0 @@ -import { Tree, formatFiles, installPackagesTask } from '@nx/devkit'; -import { libraryGenerator } from '@nx/js'; - -export default async function(tree: Tree, schema: any) { - await libraryGenerator(tree, {name: schema.name}); - await formatFiles(tree); - return () => { - installPackagesTask(tree) - } -} diff --git a/packages/workspace/src/generators/workspace-generator/files/schema.json__tmpl__ b/packages/workspace/src/generators/workspace-generator/files/schema.json__tmpl__ deleted file mode 100644 index ec10698f02a29..0000000000000 --- a/packages/workspace/src/generators/workspace-generator/files/schema.json__tmpl__ +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "cli": "nx", - "$id": "<%= name %>", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Library name", - "$default": { - "$source": "argv", - "index": 0 - } - } - }, - "required": ["name"] -} diff --git a/packages/workspace/src/generators/workspace-generator/schema.d.ts b/packages/workspace/src/generators/workspace-generator/schema.d.ts index 13e049e4b1cd1..e53f1202a2dbc 100644 --- a/packages/workspace/src/generators/workspace-generator/schema.d.ts +++ b/packages/workspace/src/generators/workspace-generator/schema.d.ts @@ -1,4 +1 @@ -export interface Schema { - name: string; - skipFormat: boolean; -} +export interface Schema {} diff --git a/packages/workspace/src/generators/workspace-generator/schema.json b/packages/workspace/src/generators/workspace-generator/schema.json index e62495f816a13..e2eb8576d3a1d 100644 --- a/packages/workspace/src/generators/workspace-generator/schema.json +++ b/packages/workspace/src/generators/workspace-generator/schema.json @@ -4,22 +4,6 @@ "title": "Create a custom generator", "description": "Create a custom generator.", "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Generator name.", - "$default": { - "$source": "argv", - "index": 0 - }, - "x-prompt": "What name would you like to use for the workspace generator?" - }, - "skipFormat": { - "description": "Skip formatting files.", - "type": "boolean", - "default": false, - "x-priority": "internal" - } - }, + "properties": {}, "required": ["name"] } diff --git a/packages/workspace/src/generators/workspace-generator/workspace-generator.spec.ts b/packages/workspace/src/generators/workspace-generator/workspace-generator.spec.ts deleted file mode 100644 index a4f14b145a98b..0000000000000 --- a/packages/workspace/src/generators/workspace-generator/workspace-generator.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import workspaceGenerator from './workspace-generator'; - -describe('workspace-generator', () => { - it('should generate a target', async () => { - const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); - const opts = { - name: 'custom', - skipFormat: true, - }; - - await workspaceGenerator(tree, opts); - - expect(tree.exists('tools/generators/custom/index.ts')).toBeTruthy(); - expect(tree.exists('tools/generators/custom/schema.json')).toBeTruthy(); - }); -}); diff --git a/packages/workspace/src/generators/workspace-generator/workspace-generator.ts b/packages/workspace/src/generators/workspace-generator/workspace-generator.ts index a6904cb052619..da0f9c888b747 100644 --- a/packages/workspace/src/generators/workspace-generator/workspace-generator.ts +++ b/packages/workspace/src/generators/workspace-generator/workspace-generator.ts @@ -1,42 +1,15 @@ import { Schema } from './schema'; -import { - Tree, - formatFiles, - generateFiles, - names, - joinPathFragments, - addDependenciesToPackageJson, -} from '@nx/devkit'; -import { nxVersion } from '../../utils/versions'; +import { Tree, stripIndents } from '@nx/devkit'; export default async function (host: Tree, schema: Schema) { - const options = normalizeOptions(schema); + const message = stripIndents`Workspace Generators are no longer supported. Instead, + Nx now supports executing generators or executors from local plugins. To get + started, install @nx/nx-plugin and run \`nx g plugin\`. - generateFiles( - host, - joinPathFragments(__dirname, 'files'), - joinPathFragments('tools/generators', schema.name), - options - ); + Afterwards, or if you already have an Nx plugin, you can run + \`nx g generator --project {my-plugin}\` to add a new generator. + + For more information, see: https://nx.dev/deprecated/workspace-generators`; - const installTask = addDependenciesToPackageJson( - host, - {}, - { - '@nx/devkit': nxVersion, - // types/node is neccessary for pnpm since it's used in tsconfig and transitive - // dependencies are not resolved correctly - '@types/node': 'latest', - } - ); - - if (!schema.skipFormat) { - await formatFiles(host); - } - return installTask; -} - -function normalizeOptions(options: Schema): any { - const name = names(options.name).fileName; - return { ...options, name, tmpl: '' }; + throw new Error(message); } diff --git a/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts new file mode 100644 index 0000000000000..ac9fa31229cd2 --- /dev/null +++ b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts @@ -0,0 +1,174 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { + Tree, + readProjectConfiguration, + readJson, + joinPathFragments, + GeneratorsJson, + ProjectConfiguration, + stripIndents, + getProjects, +} from '@nx/devkit'; + +import generator from './move-workspace-generators-to-local-plugin'; + +describe('move-workspace-generators-to-local-plugin', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should find single workspace generator successfully', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + }); + await generator(tree); + console.log(getProjects(tree).keys()); + 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, + }); + } + + await generator(tree); + + const config = readProjectConfiguration(tree, 'workspace-plugin'); + expect(config.root).toEqual('tools/workspace-plugin'); + + for (const generator of generators) { + assertValidGenerator(tree, config, generator); + } + }); + + it('should be idempotent', async () => { + const generators = [...new Array(10)].map((x) => uniq('generator')); + for (const name of generators) { + await workspaceGeneratorGenerator(tree, { + name, + }); + } + + await generator(tree); + + const generatorsJson = readJson( + tree, + 'tools/workspace-plugin/generators.json' + ); + + await generator(tree); + expect(readJson(tree, 'tools/workspace-plugin/generators.json')).toEqual( + generatorsJson + ); + + const config = readProjectConfiguration(tree, 'workspace-plugin'); + + for (const generator of generators) { + assertValidGenerator(tree, config, generator); + } + }); + + it('should merge new generators into existing plugin', async () => { + const generators = [...new Array(10)].map((x) => uniq('generator')); + for (const name of generators) { + await workspaceGeneratorGenerator(tree, { + name, + }); + } + + await generator(tree); + + const moreGenerators = [...new Array(5)].map((x) => uniq('generator')); + for (const name of moreGenerators) { + await workspaceGeneratorGenerator(tree, { + name, + }); + } + + await generator(tree); + + const config = readProjectConfiguration(tree, 'workspace-plugin'); + + for (const generator of generators.concat(moreGenerators)) { + assertValidGenerator(tree, config, 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)}`; +} + +async function workspaceGeneratorGenerator( + host: Tree, + schema: { name: string } +) { + const outputDirectory = joinPathFragments('tools/generators', schema.name); + + host.write( + joinPathFragments(outputDirectory, 'index.ts'), + stripIndents`import { Tree, formatFiles, installPackagesTask } from '@nx/devkit'; + import { libraryGenerator } from '@nx/workspace/generators'; + + export default async function(tree: Tree, schema: any) { + await libraryGenerator(tree, {name: schema.name}); + await formatFiles(tree); + return () => { + installPackagesTask(tree) + } + }` + ); + + host.write( + joinPathFragments(outputDirectory, 'schema.json'), + stripIndents`{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "<%= name %>", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": ["name"] + } + ` + ); +} diff --git a/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts new file mode 100644 index 0000000000000..a7b7e4b6764a6 --- /dev/null +++ b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts @@ -0,0 +1,188 @@ +import { + addDependenciesToPackageJson, + ensurePackage, + formatFiles, + getProjects, + getWorkspaceLayout, + joinPathFragments, + output, + ProjectConfiguration, + readJson, + readProjectConfiguration, + Tree, + updateJson, + writeJson, +} from '@nx/devkit'; +// nx-ignore-next-line +import * as path from 'path'; +import { + GeneratorsJson, + GeneratorsJsonEntry, +} from 'nx/src/config/misc-interfaces'; +import { moveGenerator } from '../../generators/move/move'; +import { nxVersion } from '../../utils/versions'; +import { PackageJson } from 'nx/src/utils/package-json'; + +const PROJECT_NAME = 'workspace-plugin'; +const DESTINATION = `tools/${PROJECT_NAME}`; + +export default async function (tree: Tree) { + const tasks = []; + if (!tree.children('tools/generators').length) { + return; + } + + let project = getProjects(tree).get(PROJECT_NAME); + if (!project) { + await createNewPlugin(tree); + tasks.push( + addDependenciesToPackageJson( + tree, + {}, + { + '@nx/nx-plugin': nxVersion, + } + ) + ); + project = readProjectConfiguration(tree, PROJECT_NAME); + } + await updateExistingPlugin(tree, project); + await formatFiles(tree); + return () => { + for (const task of tasks) { + task(); + } + }; +} + +// Inspired by packages/nx/src/command-line/workspace-generators.ts +function collectAndMoveGenerators(tree: Tree, destinationProjectRoot: string) { + const generators: Record = {}; + const generatorsDir = 'tools/generators'; + const destinationDir = joinPathFragments( + destinationProjectRoot, + '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[c] = { + implementation: `./src/generators/${c}`, + schema: `./src/generators/${joinPathFragments(c, 'schema.json')}`, + description: schema.description ?? `Generator ${c}`, + }; + tree.rename(childDir, joinPathFragments(destinationDir, c)); + } + } + return generators; +} + +async function createNewPlugin(tree: Tree) { + ensurePackage('@nx/nx-plugin', nxVersion); + const { pluginGenerator } = + // nx-ignore-next-line + require('@nx/nx-plugin/src/generators/plugin/plugin'); + + // nx-ignore-next-line + const { Linter } = ensurePackage('@nx/linter', nxVersion); + + const { npmScope } = getWorkspaceLayout(tree); + const importPath = npmScope ? `${npmScope}/${PROJECT_NAME}` : PROJECT_NAME; + + await pluginGenerator(tree, { + minimal: true, + name: PROJECT_NAME, + importPath: importPath, + skipTsConfig: false, + compiler: 'tsc', + linter: Linter.EsLint, + skipFormat: true, + skipLintChecks: false, + unitTestRunner: 'jest', + e2eTestRunner: 'none', + }); + getCreateGeneratorsJson()( + tree, + readProjectConfiguration(tree, PROJECT_NAME).root, + PROJECT_NAME + ); + await moveGeneratedPlugin(tree, DESTINATION, importPath); +} + +function moveGeneratedPlugin( + tree: Tree, + destination: string, + importPath: string +) { + const config = readProjectConfiguration(tree, PROJECT_NAME); + if (config.root !== DESTINATION) { + return moveGenerator(tree, { + destination, + projectName: PROJECT_NAME, + newProjectName: PROJECT_NAME, + updateImportPath: true, + destinationRelativeToRoot: true, + importPath: importPath, + }); + } +} + +function updateExistingPlugin(tree: Tree, project: ProjectConfiguration) { + const packageJson = readJson( + tree, + joinPathFragments(project.root, 'package.json') + ); + const defaultGeneratorsPath = joinPathFragments( + project.root, + 'generators.json' + ); + let generatorsJsonPath = + packageJson.generators || + packageJson.schematics || + tree.exists(defaultGeneratorsPath) + ? defaultGeneratorsPath + : null; + if (!generatorsJsonPath) { + getCreateGeneratorsJson()( + tree, + readProjectConfiguration(tree, PROJECT_NAME).root, + PROJECT_NAME + ); + generatorsJsonPath = defaultGeneratorsPath; + } + updateJson(tree, generatorsJsonPath, (json) => { + const generators = collectAndMoveGenerators(tree, project.root); + json.generators ??= {}; + for (const generator in generators) { + if (json.generators[generator]) { + output.warn({ + title: `Generator ${generator} already exists in ${project.name}`, + bodyLines: [ + 'Since you have a generator with the same name in your plugin, the generator from workspace-generators has been discarded.', + ], + }); + } else { + json.generators[generator] = generators[generator]; + } + } + return json; + }); +} + +function getCreateGeneratorsJson(): ( + host: Tree, + projectRoot: string, + projectName: string, + skipLintChecks?: boolean, + skipFormat?: boolean +) => Promise { + // We cant use `as typeof import('@nx/nx-plugin/src/generators/generator/generator');` here + // because it will cause a typescript error at build time. + const { createGeneratorsJson } = + // nx-ignore-next-line + require('@nx/nx-plugin/src/generators/generator/generator'); + return createGeneratorsJson; +} diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts index 9e3d262e03a1f..a0dbcdb40e615 100644 --- a/scripts/depcheck/missing.ts +++ b/scripts/depcheck/missing.ts @@ -192,6 +192,16 @@ const IGNORE_MATCHES_BY_FILE: Record = { '../../packages/angular/src/migrations/update-12-3-0/update-storybook.ts' ), ], + '@nx/nx-plugin': [ + join( + __dirname, + '../../packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts' + ), + join( + __dirname, + '../../packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts' + ), + ], }; export default async function getMissingDependencies(