diff --git a/nx.json b/nx.json index dd2c20c6cc802..d9b33357af92f 100644 --- a/nx.json +++ b/nx.json @@ -1,4 +1,5 @@ { + "$schema": "packages/nx/schemas/nx-schema.json", "implicitDependencies": { "package.json": "*", ".eslintrc.json": "*", diff --git a/packages/nx/migrations.json b/packages/nx/migrations.json index 8b4e0438fa079..35fa01082dd71 100644 --- a/packages/nx/migrations.json +++ b/packages/nx/migrations.json @@ -11,6 +11,12 @@ "version": "14.2.0-beta.0", "description": "Add JSON Schema to Nx configuration files", "factory": "./src/migrations/update-14-2-0/add-json-schema" + }, + "14-2-0-remove-default-collection": { + "cli": "nx", + "version": "14.2.0-beta.0", + "description": "Remove default collection from configuration to switch to prompts for collection", + "factory": "./src/migrations/update-14-2-0/remove-default-collection" } } } diff --git a/packages/nx/src/adapter/ngcli-adapter.ts b/packages/nx/src/adapter/ngcli-adapter.ts index 99a733e590940..3e1e0106f8cef 100644 --- a/packages/nx/src/adapter/ngcli-adapter.ts +++ b/packages/nx/src/adapter/ngcli-adapter.ts @@ -213,6 +213,9 @@ async function runSchematic( type AngularJsonConfiguration = WorkspaceJsonConfiguration & Pick & { schematics?: NxJsonConfiguration['generators']; + cli?: NxJsonConfiguration['cli'] & { + schematicCollections?: string[]; + }; }; export class NxScopedHost extends virtualFs.ScopedHost { protected __nxInMemoryWorkspace: WorkspaceJsonConfiguration | null; @@ -432,6 +435,7 @@ export class NxScopedHost extends virtualFs.ScopedHost { if (formatted) { const { cli, generators, defaultProject, ...workspaceJson } = formatted; + delete cli.schematicCollections; return merge( this.writeWorkspaceConfigFiles(context, workspaceJson), cli || generators || defaultProject @@ -446,6 +450,7 @@ export class NxScopedHost extends virtualFs.ScopedHost { defaultProject, ...angularJson } = w; + delete cli.schematicCollections; return merge( this.writeWorkspaceConfigFiles(context, angularJson), cli || schematics @@ -461,6 +466,8 @@ export class NxScopedHost extends virtualFs.ScopedHost { } const { cli, schematics, generators, defaultProject, ...angularJson } = config; + delete cli.schematicCollections; + return merge( this.writeWorkspaceConfigFiles(context, angularJson), this.__saveNxJsonProps({ diff --git a/packages/nx/src/command-line/generate.ts b/packages/nx/src/command-line/generate.ts index eb78956b1e520..d0c19c3464878 100644 --- a/packages/nx/src/command-line/generate.ts +++ b/packages/nx/src/command-line/generate.ts @@ -11,6 +11,8 @@ import * as chalk from 'chalk'; import { workspaceRoot } from '../utils/app-root'; import { NxJsonConfiguration } from '../config/nx-json'; import { printHelp } from '../utils/print-help'; +import { prompt } from 'enquirer'; +import { readJsonFile } from 'nx/src/utils/fileutils'; export interface GenerateOptions { collectionName: string; @@ -34,21 +36,121 @@ function printChanges(fileChanges: FileChange[]) { }); } -function convertToGenerateOptions( - generatorOptions: { [k: string]: any }, +async function promptForCollection( + generatorName: string, + ws: Workspaces, + interactive: boolean +) { + const packageJson = readJsonFile(`${workspaceRoot}/package.json`); + const collections = Array.from( + new Set([ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.devDependencies || {}), + ]) + ); + const choices = collections + .map((collectionName) => { + try { + const generator = ws.readGenerator(collectionName, generatorName); + + return `${collectionName}:${generator.normalizedGeneratorName}`; + } catch { + return null; + } + }) + .filter((c) => !!c); + + if (choices.length === 1) { + return choices[0]; + } else if (!interactive && choices.length > 1) { + throwInvalidInvocation(choices); + } else if (interactive && choices.length > 1) { + const noneOfTheAbove = `None of the above`; + choices.push(noneOfTheAbove); + let { generator, customCollection } = await prompt<{ + generator: string; + customCollection?: string; + }>([ + { + name: 'generator', + message: `Which generator would you like to use?`, + type: 'autocomplete', + choices, + }, + { + name: 'customCollection', + type: 'input', + message: `Which collection would you like to use?`, + skip: function () { + // Skip this question if the user did not answer None of the above + return this.state.answers.generator !== noneOfTheAbove; + }, + validate: function (value) { + if (this.skipped) { + return true; + } + try { + ws.readGenerator(value, generatorName); + return true; + } catch { + logger.error(`\nCould not find ${value}:${generatorName}`); + return false; + } + }, + }, + ]); + return customCollection + ? `${customCollection}:${generatorName}` + : generator; + } else { + throw new Error(`Could not find any generators named "${generatorName}"`); + } +} + +function parseGeneratorString(value: string): { + collection?: string; + generator: string; +} { + const separatorIndex = value.lastIndexOf(':'); + + if (separatorIndex > 0) { + return { + collection: value.slice(0, separatorIndex), + generator: value.slice(separatorIndex + 1), + }; + } else { + return { + generator: value, + }; + } +} + +async function convertToGenerateOptions( + generatorOptions: { [p: string]: any }, + ws: Workspaces, defaultCollectionName: string, mode: 'generate' | 'new' -): GenerateOptions { +): Promise { let collectionName: string | null = null; let generatorName: string | null = null; + const interactive = generatorOptions.interactive as boolean; if (mode === 'generate') { const generatorDescriptor = generatorOptions['generator'] as string; - const separatorIndex = generatorDescriptor.lastIndexOf(':'); + const { collection, generator } = parseGeneratorString(generatorDescriptor); - if (separatorIndex > 0) { - collectionName = generatorDescriptor.slice(0, separatorIndex); - generatorName = generatorDescriptor.slice(separatorIndex + 1); + if (collection) { + collectionName = collection; + generatorName = generator; + } else if (!defaultCollectionName) { + const generatorString = await promptForCollection( + generatorDescriptor, + ws, + interactive + ); + const parsedGeneratorString = parseGeneratorString(generatorString); + collectionName = parsedGeneratorString.collection; + generatorName = parsedGeneratorString.generator; } else { collectionName = defaultCollectionName; generatorName = generatorDescriptor; @@ -59,16 +161,18 @@ function convertToGenerateOptions( } if (!collectionName) { - throwInvalidInvocation(); + throwInvalidInvocation(['@nrwl/workspace:library']); } + logger.info(`NX Generating ${collectionName}:${generatorName}`); + const res = { collectionName, generatorName, generatorOptions, help: generatorOptions.help as boolean, dryRun: generatorOptions.dryRun as boolean, - interactive: generatorOptions.interactive as boolean, + interactive, defaults: generatorOptions.defaults as boolean, }; @@ -86,9 +190,11 @@ function convertToGenerateOptions( return res; } -function throwInvalidInvocation() { +function throwInvalidInvocation(availableGenerators: string[]) { throw new Error( - `Specify the generator name (e.g., nx generate @nrwl/workspace:library)` + `Specify the generator name (e.g., nx generate ${availableGenerators.join( + ', ' + )})` ); } @@ -122,7 +228,7 @@ export async function newWorkspace(cwd: string, args: { [k: string]: any }) { const isVerbose = args['verbose']; return handleErrors(isVerbose, async () => { - const opts = convertToGenerateOptions(args, null, 'new'); + const opts = await convertToGenerateOptions(args, ws, null, 'new'); const { normalizedGeneratorName, schema, implementationFactory } = ws.readGenerator(opts.collectionName, opts.generatorName); @@ -172,8 +278,9 @@ export async function generate(cwd: string, args: { [k: string]: any }) { return handleErrors(isVerbose, async () => { const workspaceDefinition = ws.readWorkspaceConfiguration(); - const opts = convertToGenerateOptions( + const opts = await convertToGenerateOptions( args, + ws, readDefaultCollection(workspaceDefinition), 'generate' ); diff --git a/packages/nx/src/migrations/update-14-2-0/remove-default-collection.spec.ts b/packages/nx/src/migrations/update-14-2-0/remove-default-collection.spec.ts new file mode 100644 index 0000000000000..f4880ad359cea --- /dev/null +++ b/packages/nx/src/migrations/update-14-2-0/remove-default-collection.spec.ts @@ -0,0 +1,44 @@ +import { createTreeWithEmptyWorkspace } from '../../generators/testing-utils/create-tree-with-empty-workspace'; +import type { Tree } from '../../generators/tree'; +import removeDefaultCollection from './remove-default-collection'; +import { + readWorkspaceConfiguration, + updateWorkspaceConfiguration, +} from '../../generators/utils/project-configuration'; + +describe('remove-default-collection', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(2); + }); + + it('should remove default collection from nx.json', async () => { + const config = readWorkspaceConfiguration(tree); + config.cli = { + defaultCollection: 'default-collection', + defaultProjectName: 'default-project', + }; + updateWorkspaceConfiguration(tree, config); + + await removeDefaultCollection(tree); + + expect(readWorkspaceConfiguration(tree).cli).toEqual({ + defaultProjectName: 'default-project', + }); + }); + + it('should remove cli entirely if defaultCollection was the only setting', async () => { + const config = readWorkspaceConfiguration(tree); + config.cli = { + defaultCollection: 'default-collection', + }; + updateWorkspaceConfiguration(tree, config); + + await removeDefaultCollection(tree); + + expect( + readWorkspaceConfiguration(tree).cli?.defaultCollection + ).toBeUndefined(); + }); +}); diff --git a/packages/nx/src/migrations/update-14-2-0/remove-default-collection.ts b/packages/nx/src/migrations/update-14-2-0/remove-default-collection.ts new file mode 100644 index 0000000000000..2ffd88e4eb6db --- /dev/null +++ b/packages/nx/src/migrations/update-14-2-0/remove-default-collection.ts @@ -0,0 +1,19 @@ +import { Tree } from '../../generators/tree'; +import { + readWorkspaceConfiguration, + updateWorkspaceConfiguration, +} from '../../generators/utils/project-configuration'; +import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available'; + +export default async function (tree: Tree) { + const workspaceConfiguration = readWorkspaceConfiguration(tree); + + delete workspaceConfiguration.cli?.defaultCollection; + if (Object.keys(workspaceConfiguration.cli).length === 0) { + delete workspaceConfiguration.cli; + } + + updateWorkspaceConfiguration(tree, workspaceConfiguration); + + await formatChangedFilesWithPrettierIfAvailable(tree); +} diff --git a/packages/nx/src/utils/plugins/models.ts b/packages/nx/src/utils/plugins/models.ts index 2cae45873debd..89cbded582179 100644 --- a/packages/nx/src/utils/plugins/models.ts +++ b/packages/nx/src/utils/plugins/models.ts @@ -1,21 +1,12 @@ -export interface PluginGenerator { - factory: string; - schema: string; - description: string; - aliases: string; - hidden: boolean; -} - -export interface PluginExecutor { - implementation: string; - schema: string; - description: string; -} +import { + ExecutorsJsonEntry, + GeneratorsJsonEntry, +} from '../../config/misc-interfaces'; export interface PluginCapabilities { name: string; - executors: { [name: string]: PluginExecutor }; - generators: { [name: string]: PluginGenerator }; + executors: { [name: string]: ExecutorsJsonEntry }; + generators: { [name: string]: GeneratorsJsonEntry }; } export interface CorePlugin {