Skip to content

Commit

Permalink
feat(core): prompt for available generators (#10463)
Browse files Browse the repository at this point in the history
  • Loading branch information
FrozenPandaz committed May 27, 2022
1 parent affa979 commit 3bcaa18
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 28 deletions.
1 change: 1 addition & 0 deletions nx.json
@@ -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
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
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
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
@@ -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();
});
});
@@ -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
@@ -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

1 comment on commit 3bcaa18

@vercel
Copy link

@vercel vercel bot commented on 3bcaa18 May 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-nrwl.vercel.app
nx-five.vercel.app
nx.dev
nx-dev-git-master-nrwl.vercel.app

Please sign in to comment.