Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions packages/angular/cli/src/command-builder/utilities/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,21 +130,22 @@
);
}

const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string']);
const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string'] as const);
type SupportedPrimitiveType = Parameters<typeof SUPPORTED_PRIMITIVE_TYPES.add>[0];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider the below, which IMO it's slightly easier to read.

Suggested change
type SupportedPrimitiveType = Parameters<typeof SUPPORTED_PRIMITIVE_TYPES.add>[0];
const types = ['boolean', 'number', 'string'] as const;
const SUPPORTED_PRIMITIVE_TYPES: ReadonlySet<string> = new Set(types);
type SupportedPrimitiveType = (typeof types)[number];


/**
* Checks if a string is a supported primitive type.
* @param value The string to check.
* @returns `true` if the string is a supported primitive type, otherwise `false`.
*/
function isSupportedPrimitiveType(value: string): boolean {
return SUPPORTED_PRIMITIVE_TYPES.has(value);
function isSupportedPrimitiveType(value: string): value is SupportedPrimitiveType {
return SUPPORTED_PRIMITIVE_TYPES.has(value as any);

Check failure on line 142 in packages/angular/cli/src/command-builder/utilities/json-schema.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove the casting, this is not needed if you change the set type.

Suggested change
return SUPPORTED_PRIMITIVE_TYPES.has(value as any);
return SUPPORTED_PRIMITIVE_TYPES.has(value);

}

/**
* Recursively checks if a JSON schema for an array's items is a supported primitive type.
* It supports `oneOf` and `anyOf` keywords.
* @param schema The JSON schema for the array's items.
* @param schema The JSON schema to check.
* @returns `true` if the schema is a supported primitive type, otherwise `false`.
*/
function isSupportedArrayItemSchema(schema: json.JsonObject): boolean {
Expand All @@ -156,6 +157,10 @@
return true;
}

if (isJsonObject(schema.items)) {
return isSupportedArrayItemSchema(schema.items);
}

if (json.isJsonArray(schema.items)) {
return schema.items.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item));
}
Expand All @@ -177,6 +182,40 @@
return false;
}

/**
* Recursively finds the first supported array primitive type for the given JSON schema.
* It supports `oneOf` and `anyOf` keywords.
* @param schema The JSON schema to inspect.
* @returns The supported primitive type or 'string' if none is found.
*/
function getSupportedArrayType(schema: json.JsonObject): SupportedPrimitiveType {
if (typeof schema.type === 'string' && isSupportedPrimitiveType(schema.type)) {
return schema.type;
}

if (json.isJsonArray(schema.enum)) {
return 'string';
}

if (isJsonObject(schema.items)) {
const result = getSupportedArrayType(schema.items);
if (result) return result;

Check failure on line 202 in packages/angular/cli/src/command-builder/utilities/json-schema.ts

View workflow job for this annotation

GitHub Actions / lint

Expected { after 'if' condition
}

for (const key of ['items', 'oneOf', 'anyOf']) {
if (json.isJsonArray(schema[key])) {
for (const item in schema[key]) {

Check failure on line 207 in packages/angular/cli/src/command-builder/utilities/json-schema.ts

View workflow job for this annotation

GitHub Actions / lint

For-in loops over arrays skips holes, returns indices as strings, and may visit the prototype chain or other enumerable properties. Use a more robust iteration method such as for-of or array.forEach instead
if (isJsonObject(item)) {
const result = getSupportedArrayType(item);
if (result) return result;

Check failure on line 210 in packages/angular/cli/src/command-builder/utilities/json-schema.ts

View workflow job for this annotation

GitHub Actions / lint

Expected { after 'if' condition
}
}
}
}

return 'string';
}

/**
* Gets the supported types for a JSON schema node.
* @param current The JSON schema node to get the supported types for.
Expand All @@ -198,7 +237,7 @@
case 'string':
return true;
case 'array':
return isJsonObject(current.items) && isSupportedArrayItemSchema(current.items);
return isSupportedArrayItemSchema(current);
case 'object':
return isStringMap(current);
default:
Expand Down Expand Up @@ -377,9 +416,13 @@
type: 'array',
itemValueType: 'string',
}
: {
type,
}),
: type === 'array'
Copy link
Collaborator

@alan-agius4 alan-agius4 Feb 26, 2026

Choose a reason for hiding this comment

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

This isn't right as it cause the array type to be replaced by a primitive and forces yargs to parse it as a non array. Example --flag foo bar will now become invalid.

? {
type: getSupportedArrayType(current),
}
: {
type,
}),
};

options.push(option);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ describe('parseJsonSchemaToOptions', () => {
'enum': ['always', 'never', 'default-array'],
},
},
'arrayWithNumbers': {
'type': 'array',
'items': { 'type': 'number' },
},
'extendable': {
'type': 'object',
'properties': {},
Expand Down Expand Up @@ -116,6 +120,9 @@ describe('parseJsonSchemaToOptions', () => {
],
},
},
'oneOfAtRoot': {
'oneOf': [{ 'type': 'array', 'items': { 'type': 'string' } }, { 'type': 'boolean' }],
},
},
};
const registry = new schema.CoreSchemaRegistry();
Expand Down Expand Up @@ -199,6 +206,35 @@ describe('parseJsonSchemaToOptions', () => {
});
});

describe('type=array, oneOf at root', () => {
it('parses valid option value', async () => {
expect(
await parse([
'--oneOfAtRoot',
'first',
'--oneOfAtRoot',
'second',
'--oneOfAtRoot',
'third',
]),
).toEqual(jasmine.objectContaining({ 'oneOfAtRoot': ['first', 'second', 'third'] }));
});

it('parses --no prefix', async () => {
expect(await parse(['--no-oneOfAtRoot'])).toEqual(
jasmine.objectContaining({ 'oneOfAtRoot': false }),
);
});
});

describe('type=Array<number>', () => {
it('parses valid option value', async () => {
expect(await parse(['--arrayWithNumbers', '42', '--arrayWithNumbers', '24'])).toEqual(
jasmine.objectContaining({ 'arrayWithNumbers': [42, 24] }),
);
});
});

describe('type=string, enum', () => {
it('parses valid option value', async () => {
expect(await parse(['--ssr', 'never'])).toEqual(
Expand Down
Loading