Skip to content

Commit 5330d52

Browse files
alan-agius4dgp1130
authored andcommitted
refactor(@angular-devkit/schematics-cli): replace parser with yargs-parser
BREAKING CHANGE: camel case arguments are no longer allowed. Closes #13544, closes #12150, closes #22173
1 parent 21034b6 commit 5330d52

File tree

6 files changed

+96
-73
lines changed

6 files changed

+96
-73
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"@types/semver": "^7.0.0",
117117
"@types/text-table": "^0.2.1",
118118
"@types/uuid": "^8.0.0",
119+
"@types/yargs-parser": "^20.2.1",
119120
"@typescript-eslint/eslint-plugin": "5.13.0",
120121
"@typescript-eslint/parser": "5.13.0",
121122
"@yarnpkg/lockfile": "1.1.0",
@@ -217,6 +218,7 @@
217218
"webpack-dev-server": "4.7.4",
218219
"webpack-merge": "5.8.0",
219220
"webpack-subresource-integrity": "5.1.0",
221+
"yargs-parser": "21.0.1",
220222
"zone.js": "^0.11.3"
221223
}
222224
}

packages/angular_devkit/schematics_cli/BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ ts_library(
5151
"//packages/angular_devkit/schematics/tasks",
5252
"//packages/angular_devkit/schematics/tools",
5353
"@npm//@types/inquirer",
54-
"@npm//@types/minimist",
5554
"@npm//@types/node",
55+
"@npm//@types/yargs-parser",
5656
"@npm//ansi-colors",
5757
"@npm//inquirer", # @external
58-
"@npm//minimist", # @external
5958
"@npm//symbol-observable", # @external
59+
"@npm//yargs-parser", # @external
6060
],
6161
)
6262

packages/angular_devkit/schematics_cli/bin/schematics.ts

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics';
1515
import { NodeWorkflow } from '@angular-devkit/schematics/tools';
1616
import * as ansiColors from 'ansi-colors';
1717
import * as inquirer from 'inquirer';
18-
import minimist from 'minimist';
18+
import yargsParser from 'yargs-parser';
1919

2020
/**
2121
* Parse the name of schematic passed in argument, and return a {collection, schematic} named
@@ -35,10 +35,11 @@ function parseSchematicName(str: string | null): { collection: string; schematic
3535
let collection = '@angular-devkit/schematics-cli';
3636

3737
let schematic = str;
38-
if (schematic && schematic.indexOf(':') != -1) {
38+
if (schematic?.includes(':')) {
39+
const lastIndexOfColon = schematic.lastIndexOf(':');
3940
[collection, schematic] = [
40-
schematic.slice(0, schematic.lastIndexOf(':')),
41-
schematic.substring(schematic.lastIndexOf(':') + 1),
41+
schematic.slice(0, lastIndexOfColon),
42+
schematic.substring(lastIndexOfColon + 1),
4243
];
4344
}
4445

@@ -113,41 +114,41 @@ export async function main({
113114
stdout = process.stdout,
114115
stderr = process.stderr,
115116
}: MainOptions): Promise<0 | 1> {
116-
const argv = parseArgs(args);
117+
const { cliOptions, schematicOptions, _ } = parseArgs(args);
117118

118119
// Create a separate instance to prevent unintended global changes to the color configuration
119120
// Create function is not defined in the typings. See: https://github.com/doowb/ansi-colors/pull/44
120121
const colors = (ansiColors as typeof ansiColors & { create: () => typeof ansiColors }).create();
121122

122123
/** Create the DevKit Logger used through the CLI. */
123-
const logger = createConsoleLogger(argv['verbose'], stdout, stderr, {
124+
const logger = createConsoleLogger(!!cliOptions.verbose, stdout, stderr, {
124125
info: (s) => s,
125126
debug: (s) => s,
126127
warn: (s) => colors.bold.yellow(s),
127128
error: (s) => colors.bold.red(s),
128129
fatal: (s) => colors.bold.red(s),
129130
});
130131

131-
if (argv.help) {
132+
if (cliOptions.help) {
132133
logger.info(getUsage());
133134

134135
return 0;
135136
}
136137

137138
/** Get the collection an schematic name from the first argument. */
138139
const { collection: collectionName, schematic: schematicName } = parseSchematicName(
139-
argv._.shift() || null,
140+
_.shift() || null,
140141
);
141142

142143
const isLocalCollection = collectionName.startsWith('.') || collectionName.startsWith('/');
143144

144145
/** Gather the arguments for later use. */
145-
const debugPresent = argv['debug'] !== null;
146-
const debug = debugPresent ? !!argv['debug'] : isLocalCollection;
147-
const dryRunPresent = argv['dry-run'] !== null;
148-
const dryRun = dryRunPresent ? !!argv['dry-run'] : debug;
149-
const force = argv['force'];
150-
const allowPrivate = argv['allow-private'];
146+
const debugPresent = cliOptions.debug !== null;
147+
const debug = debugPresent ? !!cliOptions.debug : isLocalCollection;
148+
const dryRunPresent = cliOptions['dry-run'] !== null;
149+
const dryRun = dryRunPresent ? !!cliOptions['dry-run'] : debug;
150+
const force = !!cliOptions.force;
151+
const allowPrivate = !!cliOptions['allow-private'];
151152

152153
/** Create the workflow scoped to the working directory that will be executed with this run. */
153154
const workflow = new NodeWorkflow(process.cwd(), {
@@ -158,7 +159,7 @@ export async function main({
158159
});
159160

160161
/** If the user wants to list schematics, we simply show all the schematic names. */
161-
if (argv['list-schematics']) {
162+
if (cliOptions['list-schematics']) {
162163
return _listSchematics(workflow, collectionName, logger);
163164
}
164165

@@ -236,39 +237,16 @@ export async function main({
236237
}
237238
});
238239

239-
/**
240-
* Remove every options from argv that we support in schematics itself.
241-
*/
242-
const parsedArgs = Object.assign({}, argv) as Record<string, unknown>;
243-
delete parsedArgs['--'];
244-
for (const key of booleanArgs) {
245-
delete parsedArgs[key];
246-
}
247-
248-
/**
249-
* Add options from `--` to args.
250-
*/
251-
const argv2 = minimist(argv['--']);
252-
for (const key of Object.keys(argv2)) {
253-
parsedArgs[key] = argv2[key];
254-
}
255-
256240
// Show usage of deprecated options
257241
workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg));
258242

259243
// Pass the rest of the arguments as the smart default "argv". Then delete it.
260-
workflow.registry.addSmartDefaultProvider('argv', (schema) => {
261-
if ('index' in schema) {
262-
return argv._[Number(schema['index'])];
263-
} else {
264-
return argv._;
265-
}
266-
});
267-
268-
delete parsedArgs._;
244+
workflow.registry.addSmartDefaultProvider('argv', (schema) =>
245+
'index' in schema ? _[Number(schema['index'])] : _,
246+
);
269247

270248
// Add prompts.
271-
if (argv['interactive'] && isTTY()) {
249+
if (cliOptions.interactive && isTTY()) {
272250
workflow.registry.usePromptProvider(_createPromptProvider());
273251
}
274252

@@ -285,7 +263,7 @@ export async function main({
285263
.execute({
286264
collection: collectionName,
287265
schematic: schematicName,
288-
options: parsedArgs,
266+
options: schematicOptions,
289267
allowPrivate: allowPrivate,
290268
debug: debug,
291269
logger: logger,
@@ -308,9 +286,9 @@ export async function main({
308286
// "See above" because we already printed the error.
309287
logger.fatal('The Schematic workflow failed. See above.');
310288
} else if (debug) {
311-
logger.fatal('An error occured:\n' + err.stack);
289+
logger.fatal(`An error occured:\n${err.stack}`);
312290
} else {
313-
logger.fatal(err.stack || err.message);
291+
logger.fatal(`Error: ${err.message}`);
314292
}
315293

316294
return 1;
@@ -322,7 +300,7 @@ export async function main({
322300
*/
323301
function getUsage(): string {
324302
return tags.stripIndent`
325-
schematics [CollectionName:]SchematicName [options, ...]
303+
schematics [collection-name:]schematic-name [options, ...]
326304
327305
By default, if the collection name is not specified, use the internal collection provided
328306
by the Schematics CLI.
@@ -354,34 +332,75 @@ function getUsage(): string {
354332

355333
/** Parse the command line. */
356334
const booleanArgs = [
357-
'allowPrivate',
358335
'allow-private',
359336
'debug',
360337
'dry-run',
361-
'dryRun',
362338
'force',
363339
'help',
364340
'list-schematics',
365-
'listSchematics',
366341
'verbose',
367342
'interactive',
368-
];
369-
370-
function parseArgs(args: string[] | undefined): minimist.ParsedArgs {
371-
return minimist(args, {
372-
boolean: booleanArgs,
373-
alias: {
374-
'dryRun': 'dry-run',
375-
'listSchematics': 'list-schematics',
376-
'allowPrivate': 'allow-private',
377-
},
343+
] as const;
344+
345+
type ElementType<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer ElementType>
346+
? ElementType
347+
: never;
348+
349+
interface Options {
350+
_: string[];
351+
schematicOptions: Record<string, unknown>;
352+
cliOptions: Partial<Record<ElementType<typeof booleanArgs>, boolean | null>>;
353+
}
354+
355+
/** Parse the command line. */
356+
function parseArgs(args: string[]): Options {
357+
const { _, ...options } = yargsParser(args, {
358+
boolean: booleanArgs as unknown as string[],
378359
default: {
379360
'interactive': true,
380361
'debug': null,
381-
'dryRun': null,
362+
'dry-run': null,
363+
},
364+
configuration: {
365+
'dot-notation': false,
366+
'boolean-negation': true,
367+
'strip-aliased': true,
368+
'camel-case-expansion': false,
382369
},
383-
'--': true,
384370
});
371+
372+
// Camelize options as yargs will return the object in kebab-case when camel casing is disabled.
373+
const schematicOptions: Options['schematicOptions'] = {};
374+
const cliOptions: Options['cliOptions'] = {};
375+
376+
const isCliOptions = (
377+
key: ElementType<typeof booleanArgs> | string,
378+
): key is ElementType<typeof booleanArgs> =>
379+
booleanArgs.includes(key as ElementType<typeof booleanArgs>);
380+
381+
// Casting temporary until https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59065 is merged and released.
382+
const { camelCase, decamelize } = yargsParser as yargsParser.Parser & {
383+
camelCase(str: string): string;
384+
decamelize(str: string, joinString?: string): string;
385+
};
386+
387+
for (const [key, value] of Object.entries(options)) {
388+
if (/[A-Z]/.test(key)) {
389+
throw new Error(`Unknown argument ${key}. Did you mean ${decamelize(key)}?`);
390+
}
391+
392+
if (isCliOptions(key)) {
393+
cliOptions[key] = value;
394+
} else {
395+
schematicOptions[camelCase(key)] = value;
396+
}
397+
}
398+
399+
return {
400+
_,
401+
schematicOptions,
402+
cliOptions,
403+
};
385404
}
386405

387406
function isTTY(): boolean {

packages/angular_devkit/schematics_cli/bin/schematics_spec.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,11 @@ describe('schematics-cli binary', () => {
3535
expect(res).toEqual(0);
3636
});
3737

38-
it('listSchematics works', async () => {
38+
it('errors when using camel case listSchematics', async () => {
3939
const args = ['--listSchematics'];
40-
const res = await main({ args, stdout, stderr });
41-
expect(stdout.lines).toMatch(/blank/);
42-
expect(stdout.lines).toMatch(/schematic/);
43-
expect(res).toEqual(0);
40+
await expectAsync(main({ args, stdout, stderr })).toBeRejectedWithError(
41+
'Unknown argument listSchematics. Did you mean list-schematics?',
42+
);
4443
});
4544

4645
it('dry-run works', async () => {

packages/angular_devkit/schematics_cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"@angular-devkit/schematics": "0.0.0-PLACEHOLDER",
2121
"ansi-colors": "4.1.1",
2222
"inquirer": "8.2.0",
23-
"minimist": "1.2.5",
24-
"symbol-observable": "4.0.0"
23+
"symbol-observable": "4.0.0",
24+
"yargs-parser": "21.0.1"
2525
}
2626
}

yarn.lock

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@
178178

179179
"@angular/dev-infra-private@https://github.com/angular/dev-infra-private-builds.git#5e484f9c4ab6b47f84263d115d6cf9e13ce4f32a":
180180
version "0.0.0-104c49ad795097101ab3aa268a8e9af2cdf04a8d"
181-
uid "5e484f9c4ab6b47f84263d115d6cf9e13ce4f32a"
182181
resolved "https://github.com/angular/dev-infra-private-builds.git#5e484f9c4ab6b47f84263d115d6cf9e13ce4f32a"
183182
dependencies:
184183
"@angular-devkit/build-angular" "14.0.0-next.3"
@@ -2223,7 +2222,7 @@
22232222
dependencies:
22242223
"@types/node" "*"
22252224

2226-
"@types/yargs-parser@*":
2225+
"@types/yargs-parser@*", "@types/yargs-parser@^20.2.1":
22272226
version "20.2.1"
22282227
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129"
22292228
integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==
@@ -9124,7 +9123,6 @@ [email protected], sass@^1.49.0:
91249123

91259124
"sauce-connect-proxy@https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz":
91269125
version "0.0.0"
9127-
uid "992e2cb0d91e54b27a4f5bbd2049f3b774718115"
91289126
resolved "https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz#992e2cb0d91e54b27a4f5bbd2049f3b774718115"
91299127

91309128
saucelabs@^1.5.0:
@@ -10924,6 +10922,11 @@ yaml@^1.10.0, yaml@^1.5.0:
1092410922
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
1092510923
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
1092610924

10925+
10926+
version "21.0.1"
10927+
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35"
10928+
integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==
10929+
1092710930
yargs-parser@^18.1.2:
1092810931
version "18.1.3"
1092910932
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"

0 commit comments

Comments
 (0)