Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): prompt for available generators #10463

Merged
merged 1 commit into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions nx.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "packages/nx/schemas/nx-schema.json",
"implicitDependencies": {
"package.json": "*",
".eslintrc.json": "*",
Expand Down
6 changes: 6 additions & 0 deletions packages/nx/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
7 changes: 7 additions & 0 deletions packages/nx/src/adapter/ngcli-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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({
Expand Down
133 changes: 120 additions & 13 deletions packages/nx/src/command-line/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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,
};

Expand All @@ -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(
', '
)})`
);
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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'
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 6 additions & 15 deletions packages/nx/src/utils/plugins/models.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down