feat(core): prompt for available generators (#10463)

This commit is contained in:
Jason Jean 2022-05-27 17:46:43 -04:00 committed by GitHub
parent affa979642
commit 3bcaa185ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 203 additions and 28 deletions

View File

@ -1,4 +1,5 @@
{
"$schema": "packages/nx/schemas/nx-schema.json",
"implicitDependencies": {
"package.json": "*",
".eslintrc.json": "*",

View File

@ -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"
}
}
}

View File

@ -213,6 +213,9 @@ async function runSchematic(
type AngularJsonConfiguration = WorkspaceJsonConfiguration &
Pick<NxJsonConfiguration, 'cli' | 'defaultProject' | 'generators'> & {
schematics?: NxJsonConfiguration['generators'];
cli?: NxJsonConfiguration['cli'] & {
schematicCollections?: string[];
};
};
export class NxScopedHost extends virtualFs.ScopedHost<any> {
protected __nxInMemoryWorkspace: WorkspaceJsonConfiguration | null;
@ -432,6 +435,7 @@ export class NxScopedHost extends virtualFs.ScopedHost<any> {
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<any> {
defaultProject,
...angularJson
} = w;
delete cli.schematicCollections;
return merge(
this.writeWorkspaceConfigFiles(context, angularJson),
cli || schematics
@ -461,6 +466,8 @@ export class NxScopedHost extends virtualFs.ScopedHost<any> {
}
const { cli, schematics, generators, defaultProject, ...angularJson } =
config;
delete cli.schematicCollections;
return merge(
this.writeWorkspaceConfigFiles(context, angularJson),
this.__saveNxJsonProps({

View File

@ -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<GenerateOptions> {
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'
);

View File

@ -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();
});
});

View File

@ -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);
}

View File

@ -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 {