6
6
* found in the LICENSE file at https://angular.dev/license
7
7
*/
8
8
9
- import { json } from '@angular-devkit/core' ;
10
- import yargs from 'yargs' ;
9
+ import { json , strings } from '@angular-devkit/core' ;
10
+ import yargs , { Arguments , Argv , PositionalOptions , Options as YargsOptions } from 'yargs' ;
11
11
12
12
/**
13
13
* An option description.
@@ -43,6 +43,55 @@ export interface Option extends yargs.Options {
43
43
* If this is falsey, do not report this option.
44
44
*/
45
45
userAnalytics ?: string ;
46
+
47
+ /**
48
+ * Type of the values in a key/value pair field.
49
+ */
50
+ itemValueType ?: 'string' ;
51
+ }
52
+
53
+ function coerceToStringMap (
54
+ dashedName : string ,
55
+ value : ( string | undefined ) [ ] ,
56
+ ) : Record < string , string > | Promise < never > {
57
+ const stringMap : Record < string , string > = { } ;
58
+ for ( const pair of value ) {
59
+ // This happens when the flag isn't passed at all.
60
+ if ( pair === undefined ) {
61
+ continue ;
62
+ }
63
+
64
+ const eqIdx = pair . indexOf ( '=' ) ;
65
+ if ( eqIdx === - 1 ) {
66
+ // TODO: Remove workaround once yargs properly handles thrown errors from coerce.
67
+ // Right now these sometimes end up as uncaught exceptions instead of proper validation
68
+ // errors with usage output.
69
+ return Promise . reject (
70
+ new Error (
71
+ `Invalid value for argument: ${ dashedName } , Given: '${ pair } ', Expected key=value pair` ,
72
+ ) ,
73
+ ) ;
74
+ }
75
+ const key = pair . slice ( 0 , eqIdx ) ;
76
+ const value = pair . slice ( eqIdx + 1 ) ;
77
+ stringMap [ key ] = value ;
78
+ }
79
+
80
+ return stringMap ;
81
+ }
82
+
83
+ function isStringMap ( node : json . JsonObject ) : boolean {
84
+ // Exclude fields with more specific kinds of properties.
85
+ if ( node . properties || node . patternProperties ) {
86
+ return false ;
87
+ }
88
+
89
+ // Restrict to additionalProperties with string values.
90
+ return (
91
+ json . isJsonObject ( node . additionalProperties ) &&
92
+ ! node . additionalProperties . enum &&
93
+ node . additionalProperties . type === 'string'
94
+ ) ;
46
95
}
47
96
48
97
export async function parseJsonSchemaToOptions (
@@ -106,10 +155,13 @@ export async function parseJsonSchemaToOptions(
106
155
107
156
return false ;
108
157
158
+ case 'object' :
159
+ return isStringMap ( current ) ;
160
+
109
161
default :
110
162
return false ;
111
163
}
112
- } ) as ( 'string' | 'number' | 'boolean' | 'array' ) [ ] ;
164
+ } ) as ( 'string' | 'number' | 'boolean' | 'array' | 'object' ) [ ] ;
113
165
114
166
if ( types . length == 0 ) {
115
167
// This means it's not usable on the command line. e.g. an Object.
@@ -150,7 +202,6 @@ export async function parseJsonSchemaToOptions(
150
202
}
151
203
}
152
204
153
- const type = types [ 0 ] ;
154
205
const $default = current . $default ;
155
206
const $defaultIndex =
156
207
json . isJsonObject ( $default ) && $default [ '$source' ] == 'argv' ? $default [ 'index' ] : undefined ;
@@ -182,7 +233,6 @@ export async function parseJsonSchemaToOptions(
182
233
const option : Option = {
183
234
name,
184
235
description : '' + ( current . description === undefined ? '' : current . description ) ,
185
- type,
186
236
default : defaultValue ,
187
237
choices : enumValues . length ? enumValues : undefined ,
188
238
required,
@@ -192,6 +242,14 @@ export async function parseJsonSchemaToOptions(
192
242
userAnalytics,
193
243
deprecated,
194
244
positional,
245
+ ...( types [ 0 ] === 'object'
246
+ ? {
247
+ type : 'array' ,
248
+ itemValueType : 'string' ,
249
+ }
250
+ : {
251
+ type : types [ 0 ] ,
252
+ } ) ,
195
253
} ;
196
254
197
255
options . push ( option ) ;
@@ -211,3 +269,90 @@ export async function parseJsonSchemaToOptions(
211
269
return a . name . localeCompare ( b . name ) ;
212
270
} ) ;
213
271
}
272
+
273
+ /**
274
+ * Adds schema options to a command also this keeps track of options that are required for analytics.
275
+ * **Note:** This method should be called from the command bundler method.
276
+ *
277
+ * @returns A map from option name to analytics configuration.
278
+ */
279
+ export function addSchemaOptionsToCommand < T > (
280
+ localYargs : Argv < T > ,
281
+ options : Option [ ] ,
282
+ includeDefaultValues : boolean ,
283
+ ) : Map < string , string > {
284
+ const booleanOptionsWithNoPrefix = new Set < string > ( ) ;
285
+ const keyValuePairOptions = new Set < string > ( ) ;
286
+ const optionsWithAnalytics = new Map < string , string > ( ) ;
287
+
288
+ for ( const option of options ) {
289
+ const {
290
+ default : defaultVal ,
291
+ positional,
292
+ deprecated,
293
+ description,
294
+ alias,
295
+ userAnalytics,
296
+ type,
297
+ itemValueType,
298
+ hidden,
299
+ name,
300
+ choices,
301
+ } = option ;
302
+
303
+ let dashedName = strings . dasherize ( name ) ;
304
+
305
+ // Handle options which have been defined in the schema with `no` prefix.
306
+ if ( type === 'boolean' && dashedName . startsWith ( 'no-' ) ) {
307
+ dashedName = dashedName . slice ( 3 ) ;
308
+ booleanOptionsWithNoPrefix . add ( dashedName ) ;
309
+ }
310
+
311
+ if ( itemValueType ) {
312
+ keyValuePairOptions . add ( name ) ;
313
+ }
314
+
315
+ const sharedOptions : YargsOptions & PositionalOptions = {
316
+ alias,
317
+ hidden,
318
+ description,
319
+ deprecated,
320
+ choices,
321
+ coerce : itemValueType ? coerceToStringMap . bind ( null , dashedName ) : undefined ,
322
+ // This should only be done when `--help` is used otherwise default will override options set in angular.json.
323
+ ...( includeDefaultValues ? { default : defaultVal } : { } ) ,
324
+ } ;
325
+
326
+ if ( positional === undefined ) {
327
+ localYargs = localYargs . option ( dashedName , {
328
+ array : itemValueType ? true : undefined ,
329
+ type : itemValueType ?? type ,
330
+ ...sharedOptions ,
331
+ } ) ;
332
+ } else {
333
+ localYargs = localYargs . positional ( dashedName , {
334
+ type : type === 'array' || type === 'count' ? 'string' : type ,
335
+ ...sharedOptions ,
336
+ } ) ;
337
+ }
338
+
339
+ // Record option of analytics.
340
+ if ( userAnalytics !== undefined ) {
341
+ optionsWithAnalytics . set ( name , userAnalytics ) ;
342
+ }
343
+ }
344
+
345
+ // Handle options which have been defined in the schema with `no` prefix.
346
+ if ( booleanOptionsWithNoPrefix . size ) {
347
+ localYargs . middleware ( ( options : Arguments ) => {
348
+ for ( const key of booleanOptionsWithNoPrefix ) {
349
+ if ( key in options ) {
350
+ options [ `no-${ key } ` ] = ! options [ key ] ;
351
+ delete options [ key ] ;
352
+ }
353
+ }
354
+ } , false ) ;
355
+ }
356
+
357
+ return optionsWithAnalytics ;
358
+ }
0 commit comments