Skip to content

Commit b80fd7b

Browse files
committed
refactor(@angular-devkit/architect-cli): remove yargs-parser dependency
Replaces `yargs-parser` with `node:util` `parseArgs` in the architect CLI binary. This aligns the argument parsing logic with other CLI tools in the repo and removes an external dependency.
1 parent edeb41c commit b80fd7b

File tree

3 files changed

+108
-38
lines changed

3 files changed

+108
-38
lines changed

packages/angular_devkit/architect_cli/BUILD.bazel

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ ts_project(
1919
deps = [
2020
":node_modules/@angular-devkit/architect",
2121
":node_modules/@angular-devkit/core",
22-
":node_modules/yargs-parser",
2322
"//:node_modules/@types/node",
24-
"//:node_modules/@types/yargs-parser",
2523
],
2624
)
2725

packages/angular_devkit/architect_cli/bin/architect.ts

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99

1010
import { Architect } from '@angular-devkit/architect';
1111
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
12-
import { JsonValue, json, logging, schema, tags, workspaces } from '@angular-devkit/core';
12+
import { JsonValue, json, logging, schema, strings, tags, workspaces } from '@angular-devkit/core';
1313
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node';
1414
import { existsSync } from 'node:fs';
1515
import * as path from 'node:path';
16-
import { styleText } from 'node:util';
17-
import yargsParser, { camelCase, decamelize } from 'yargs-parser';
16+
import { parseArgs, styleText } from 'node:util';
1817

1918
function findUp(names: string | string[], from: string) {
2019
if (!Array.isArray(names)) {
@@ -62,36 +61,22 @@ async function _executeTarget(
6261
parentLogger: logging.Logger,
6362
workspace: workspaces.WorkspaceDefinition,
6463
root: string,
65-
argv: ReturnType<typeof yargsParser>,
64+
targetStr: string,
65+
options: json.JsonObject,
6666
registry: schema.SchemaRegistry,
6767
) {
6868
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, root);
6969
const architect = new Architect(architectHost, registry);
7070

7171
// Split a target into its parts.
72-
const {
73-
_: [targetStr = ''],
74-
help,
75-
...options
76-
} = argv;
77-
const [project, target, configuration] = targetStr.toString().split(':');
72+
const [project, target, configuration] = targetStr.split(':');
7873
const targetSpec = { project, target, configuration };
7974

8075
const logger = new logging.Logger('jobs');
8176
const logs: logging.LogEntry[] = [];
8277
logger.subscribe((entry) => logs.push({ ...entry, message: `${entry.name}: ` + entry.message }));
8378

84-
// Camelize options as yargs will return the object in kebab-case when camel casing is disabled.
85-
const camelCasedOptions: json.JsonObject = {};
86-
for (const [key, value] of Object.entries(options)) {
87-
if (/[A-Z]/.test(key)) {
88-
throw new Error(`Unknown argument ${key}. Did you mean ${decamelize(key)}?`);
89-
}
90-
91-
camelCasedOptions[camelCase(key)] = value as JsonValue;
92-
}
93-
94-
const run = await architect.scheduleTarget(targetSpec, camelCasedOptions, { logger });
79+
const run = await architect.scheduleTarget(targetSpec, options, { logger });
9580

9681
// Wait for full completion of the builder.
9782
try {
@@ -122,20 +107,108 @@ async function _executeTarget(
122107
}
123108
}
124109

110+
const CLI_OPTION_DEFINITIONS = {
111+
'help': { type: 'boolean' },
112+
'verbose': { type: 'boolean' },
113+
} as const;
114+
115+
interface Options {
116+
positionals: string[];
117+
builderOptions: json.JsonObject;
118+
cliOptions: Partial<Record<keyof typeof CLI_OPTION_DEFINITIONS, boolean>>;
119+
}
120+
121+
/** Parse the command line. */
122+
function parseOptions(args: string[]): Options {
123+
const { values, tokens } = parseArgs({
124+
args,
125+
strict: false,
126+
tokens: true,
127+
allowPositionals: true,
128+
allowNegative: true,
129+
options: CLI_OPTION_DEFINITIONS,
130+
});
131+
132+
const builderOptions: json.JsonObject = {};
133+
const positionals: string[] = [];
134+
135+
for (let i = 0; i < tokens.length; i++) {
136+
const token = tokens[i];
137+
138+
if (token.kind === 'positional') {
139+
positionals.push(token.value);
140+
continue;
141+
}
142+
143+
if (token.kind !== 'option') {
144+
continue;
145+
}
146+
147+
const name = token.name;
148+
let value: JsonValue = token.value ?? true;
149+
150+
// `parseArgs` already handled known boolean args and their --no- forms.
151+
// Only process options not in CLI_OPTION_DEFINITIONS here.
152+
if (name in CLI_OPTION_DEFINITIONS) {
153+
continue;
154+
}
155+
156+
if (/[A-Z]/.test(name)) {
157+
throw new Error(
158+
`Unknown argument ${name}. Did you mean ${strings.decamelize(name).replaceAll('_', '-')}?`,
159+
);
160+
}
161+
162+
// Handle --no-flag for unknown options, treating it as false
163+
if (name.startsWith('no-')) {
164+
const realName = name.slice(3);
165+
builderOptions[strings.camelize(realName)] = false;
166+
continue;
167+
}
168+
169+
// Handle value for unknown options
170+
if (token.inlineValue === undefined) {
171+
// Look ahead
172+
const nextToken = tokens[i + 1];
173+
if (nextToken?.kind === 'positional') {
174+
value = nextToken.value;
175+
i++; // Consume next token
176+
} else {
177+
value = true; // Treat as boolean if no value follows
178+
}
179+
}
180+
181+
// Type inference for numbers
182+
if (typeof value === 'string' && !isNaN(Number(value))) {
183+
value = Number(value);
184+
}
185+
186+
const camelName = strings.camelize(name);
187+
if (Object.prototype.hasOwnProperty.call(builderOptions, camelName)) {
188+
const existing = builderOptions[camelName];
189+
if (Array.isArray(existing)) {
190+
existing.push(value);
191+
} else {
192+
builderOptions[camelName] = [existing, value] as JsonValue;
193+
}
194+
} else {
195+
builderOptions[camelName] = value;
196+
}
197+
}
198+
199+
return {
200+
positionals,
201+
builderOptions,
202+
cliOptions: values as Options['cliOptions'],
203+
};
204+
}
205+
125206
async function main(args: string[]): Promise<number> {
126207
/** Parse the command line. */
127-
const argv = yargsParser(args, {
128-
boolean: ['help'],
129-
configuration: {
130-
'dot-notation': false,
131-
'boolean-negation': true,
132-
'strip-aliased': true,
133-
'camel-case-expansion': false,
134-
},
135-
});
208+
const { positionals, cliOptions, builderOptions } = parseOptions(args);
136209

137210
/** Create the DevKit Logger used through the CLI. */
138-
const logger = createConsoleLogger(argv['verbose'], process.stdout, process.stderr, {
211+
const logger = createConsoleLogger(!!cliOptions['verbose'], process.stdout, process.stderr, {
139212
info: (s) => s,
140213
debug: (s) => s,
141214
warn: (s) => styleText(['yellow', 'bold'], s),
@@ -144,8 +217,8 @@ async function main(args: string[]): Promise<number> {
144217
});
145218

146219
// Check the target.
147-
const targetStr = argv._[0] || '';
148-
if (!targetStr || argv.help) {
220+
const targetStr = positionals[0];
221+
if (!targetStr || cliOptions.help) {
149222
// Show architect usage if there's no target.
150223
usage(logger);
151224
}
@@ -181,7 +254,7 @@ async function main(args: string[]): Promise<number> {
181254
// Clear the console.
182255
process.stdout.write('\u001Bc');
183256

184-
return await _executeTarget(logger, workspace, root, argv, registry);
257+
return await _executeTarget(logger, workspace, root, targetStr, builderOptions, registry);
185258
}
186259

187260
main(process.argv.slice(2)).then(

packages/angular_devkit/architect_cli/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
],
1616
"dependencies": {
1717
"@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER",
18-
"@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER",
19-
"yargs-parser": "22.0.0"
18+
"@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER"
2019
}
2120
}

0 commit comments

Comments
 (0)