Skip to content

Commit d9950dc

Browse files
committed
fix(@angular/cli): support string | boolean union types in CLI options
This commit enhances the CLI's command builder to correctly handle JSON schema properties that define a union type of `string` and `boolean`. Previously, only the first type in a union was considered, preventing options like `--runner-config` from being used as both a boolean flag and a string value.
1 parent 245d464 commit d9950dc

File tree

3 files changed

+83
-5
lines changed

3 files changed

+83
-5
lines changed

packages/angular/cli/src/command-builder/utilities/json-schema.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,18 +257,18 @@ function getEnumValues(
257257
*/
258258
function getDefaultValue(
259259
current: json.JsonObject,
260-
type: string,
260+
type: ReadonlyArray<string>,
261261
): string | number | boolean | unknown[] | undefined {
262262
const defaultValue = current.default;
263263
if (defaultValue === undefined) {
264264
return undefined;
265265
}
266266

267-
if (type === 'array') {
267+
if (type.includes('array')) {
268268
return Array.isArray(defaultValue) && defaultValue.length > 0 ? defaultValue : undefined;
269269
}
270270

271-
if (typeof defaultValue === type) {
271+
if (type.includes(typeof defaultValue)) {
272272
return defaultValue as string | number | boolean;
273273
}
274274

@@ -343,7 +343,12 @@ export async function parseJsonSchemaToOptions(
343343
return;
344344
}
345345

346-
const [type] = types;
346+
// Allow Yargs to infer the option type for string AND boolean options
347+
const type =
348+
types.length === 2 && types.includes('string') && types.includes('boolean')
349+
? undefined
350+
: types[0];
351+
347352
const $default = current.$default;
348353
const $defaultIndex =
349354
isJsonObject($default) && $default['$source'] === 'argv' ? $default['index'] : undefined;
@@ -362,7 +367,7 @@ export async function parseJsonSchemaToOptions(
362367
const option: Option = {
363368
name,
364369
description: String(current.description ?? ''),
365-
default: getDefaultValue(current, type),
370+
default: getDefaultValue(current, types),
366371
choices: enumValues?.length ? enumValues : undefined,
367372
required,
368373
alias: getAliases(current),

packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,29 @@ describe('parseJsonSchemaToOptions', () => {
257257
});
258258
});
259259

260+
it(`should not set 'type' when it is a 'string' and a 'boolean'`, async () => {
261+
const registry = new schema.CoreSchemaRegistry();
262+
const options = await parseJsonSchemaToOptions(
263+
registry,
264+
{
265+
'type': 'object',
266+
'properties': {
267+
'runner': {
268+
'type': ['string', 'boolean'],
269+
},
270+
},
271+
},
272+
false,
273+
);
274+
275+
expect(options).toEqual([
276+
jasmine.objectContaining({
277+
name: 'runner',
278+
type: undefined,
279+
}),
280+
]);
281+
});
282+
260283
describe('with required positional argument', () => {
261284
it('marks the required argument as required', async () => {
262285
const jsonSchema = {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ng } from '../../utils/process';
2+
import { writeMultipleFiles } from '../../utils/fs';
3+
import { applyVitestBuilder } from '../../utils/vitest';
4+
import assert from 'node:assert';
5+
6+
export default async function () {
7+
await applyVitestBuilder();
8+
9+
// Test the boolean `true` usage of `--runner-config`.
10+
// It should log that it is searching for a config file.
11+
const { stdout: boolTrueStdout } = await ng('test', '--runner-config');
12+
assert.match(
13+
boolTrueStdout,
14+
/Automatically searching/,
15+
'Expected an automatic search message for --runner-config',
16+
);
17+
assert.match(boolTrueStdout, /1 passed/, 'Expected 1 test to pass with --runner-config');
18+
19+
// Test the string usage of `--runner-config`.
20+
// It should log that it is using the specified config file.
21+
const customConfigPath = 'vitest.custom.mjs';
22+
await writeMultipleFiles({
23+
[customConfigPath]: `
24+
import { defineConfig } from 'vitest/config';
25+
export default defineConfig({
26+
test: {
27+
28+
},
29+
});
30+
`,
31+
});
32+
33+
const { stdout: stringStdout } = await ng('test', `--runner-config=${customConfigPath}`);
34+
assert.match(
35+
stringStdout,
36+
/vitest\.custom\.mjs/,
37+
'Expected a message confirming the use of the custom config file.',
38+
);
39+
assert.match(stringStdout, /1 passed/, 'Expected all tests to pass with string config.');
40+
41+
// Test the boolean `false` usage of `--runner-config`.
42+
// It should not log any messages about searching for or using a config file.
43+
const { stdout: boolFalseStdout } = await ng('test', '--no-runner-config');
44+
assert.doesNotMatch(
45+
boolFalseStdout,
46+
/Automatically searching/,
47+
'Should not search for a config with --no-runner-config',
48+
);
49+
assert.match(boolFalseStdout, /1 passed/, 'Expected 1 test to pass with --no-runner-config');
50+
}

0 commit comments

Comments
 (0)