diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts index 0a4215be8eed..ef4ef8f6e3db 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -130,21 +130,22 @@ function isStringMap(node: json.JsonObject): boolean { ); } -const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string']); +const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string'] as const); +type SupportedPrimitiveType = Parameters[0]; /** * 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); } /** * 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 { @@ -156,6 +157,10 @@ function isSupportedArrayItemSchema(schema: json.JsonObject): boolean { return true; } + if (isJsonObject(schema.items)) { + return isSupportedArrayItemSchema(schema.items); + } + if (json.isJsonArray(schema.items)) { return schema.items.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)); } @@ -177,6 +182,40 @@ function isSupportedArrayItemSchema(schema: json.JsonObject): boolean { 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; + } + + for (const key of ['items', 'oneOf', 'anyOf']) { + if (json.isJsonArray(schema[key])) { + for (const item in schema[key]) { + if (isJsonObject(item)) { + const result = getSupportedArrayType(item); + if (result) return result; + } + } + } + } + + return 'string'; +} + /** * Gets the supported types for a JSON schema node. * @param current The JSON schema node to get the supported types for. @@ -198,7 +237,7 @@ function getSupportedTypes( case 'string': return true; case 'array': - return isJsonObject(current.items) && isSupportedArrayItemSchema(current.items); + return isSupportedArrayItemSchema(current); case 'object': return isStringMap(current); default: @@ -377,9 +416,13 @@ export async function parseJsonSchemaToOptions( type: 'array', itemValueType: 'string', } - : { - type, - }), + : type === 'array' + ? { + type: getSupportedArrayType(current), + } + : { + type, + }), }; options.push(option); diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts index d311373d69f0..5b32cf5efa20 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -43,6 +43,10 @@ describe('parseJsonSchemaToOptions', () => { 'enum': ['always', 'never', 'default-array'], }, }, + 'arrayWithNumbers': { + 'type': 'array', + 'items': { 'type': 'number' }, + }, 'extendable': { 'type': 'object', 'properties': {}, @@ -116,6 +120,9 @@ describe('parseJsonSchemaToOptions', () => { ], }, }, + 'oneOfAtRoot': { + 'oneOf': [{ 'type': 'array', 'items': { 'type': 'string' } }, { 'type': 'boolean' }], + }, }, }; const registry = new schema.CoreSchemaRegistry(); @@ -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', () => { + 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(