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,75 @@ 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
+ /**
54
+ * Note: This is done in a middleware because of how coerce and check work in
55
+ * yargs: coerce cannot throw validation errors but check only receives the
56
+ * post-coerce values. Instead of building a brittle communication channel
57
+ * between those two functions, it's easier to do both inside a single middleware.
58
+ */
59
+ function coerceToStringMap ( dashedName : string , value : ( string | undefined ) [ ] ) {
60
+ const stringMap : Record < string , string > = { } ;
61
+ for ( const pair of value ) {
62
+ // This happens when the flag isn't passed at all.
63
+ if ( pair === undefined ) {
64
+ continue ;
65
+ }
66
+
67
+ const eqIdx = pair . indexOf ( '=' ) ;
68
+ if ( eqIdx === - 1 ) {
69
+ // This error will be picked up later in the check() callback.
70
+ // We can't throw in coerce and checks only happen after coerce completed.
71
+ throw new Error (
72
+ `Invalid value for argument: ${ dashedName } , Given: '${ pair } ', Expected key=value pair` ,
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 stringMapMiddleware ( optionNames : Set < string > ) {
84
+ return ( argv : Arguments ) => {
85
+ for ( const name of optionNames ) {
86
+ if ( name in argv ) {
87
+ const value = argv [ name ] ;
88
+ const dashedName = strings . dasherize ( name ) ;
89
+ const newValue = coerceToStringMap ( dashedName , value as ( string | undefined ) [ ] ) ;
90
+ argv [ name ] = argv [ dashedName ] = newValue ;
91
+ }
92
+ }
93
+ } ;
94
+ }
95
+
96
+ function isStringMap ( node : json . JsonObject ) {
97
+ if ( node . properties ) {
98
+ return false ;
99
+ }
100
+ if ( node . patternProperties ) {
101
+ return false ;
102
+ }
103
+ if ( ! json . isJsonObject ( node . additionalProperties ) ) {
104
+ return false ;
105
+ }
106
+
107
+ if ( node . additionalProperties ?. type !== 'string' ) {
108
+ return false ;
109
+ }
110
+ if ( node . additionalProperties ?. enum ) {
111
+ return false ;
112
+ }
113
+
114
+ return true ;
46
115
}
47
116
48
117
export async function parseJsonSchemaToOptions (
@@ -106,10 +175,13 @@ export async function parseJsonSchemaToOptions(
106
175
107
176
return false ;
108
177
178
+ case 'object' :
179
+ return isStringMap ( current ) ;
180
+
109
181
default :
110
182
return false ;
111
183
}
112
- } ) as ( 'string' | 'number' | 'boolean' | 'array' ) [ ] ;
184
+ } ) as ( 'string' | 'number' | 'boolean' | 'array' | 'object' ) [ ] ;
113
185
114
186
if ( types . length == 0 ) {
115
187
// This means it's not usable on the command line. e.g. an Object.
@@ -150,7 +222,6 @@ export async function parseJsonSchemaToOptions(
150
222
}
151
223
}
152
224
153
- const type = types [ 0 ] ;
154
225
const $default = current . $default ;
155
226
const $defaultIndex =
156
227
json . isJsonObject ( $default ) && $default [ '$source' ] == 'argv' ? $default [ 'index' ] : undefined ;
@@ -182,16 +253,22 @@ export async function parseJsonSchemaToOptions(
182
253
const option : Option = {
183
254
name,
184
255
description : '' + ( current . description === undefined ? '' : current . description ) ,
185
- type,
186
256
default : defaultValue ,
187
257
choices : enumValues . length ? enumValues : undefined ,
188
- required,
189
258
alias,
190
259
format,
191
260
hidden,
192
261
userAnalytics,
193
262
deprecated,
194
263
positional,
264
+ ...( types [ 0 ] === 'object'
265
+ ? {
266
+ type : 'array' ,
267
+ itemValueType : 'string' ,
268
+ }
269
+ : {
270
+ type : types [ 0 ] ,
271
+ } ) ,
195
272
} ;
196
273
197
274
options . push ( option ) ;
@@ -211,3 +288,93 @@ export async function parseJsonSchemaToOptions(
211
288
return a . name . localeCompare ( b . name ) ;
212
289
} ) ;
213
290
}
291
+
292
+ /**
293
+ * Adds schema options to a command also this keeps track of options that are required for analytics.
294
+ * **Note:** This method should be called from the command bundler method.
295
+ *
296
+ * @returns A map from option name to analytics configuration.
297
+ */
298
+ export function addSchemaOptionsToCommand < T > (
299
+ localYargs : Argv < T > ,
300
+ options : Option [ ] ,
301
+ includeDefaultValues : boolean ,
302
+ ) : Map < string , string > {
303
+ const booleanOptionsWithNoPrefix = new Set < string > ( ) ;
304
+ const keyValuePairOptions = new Set < string > ( ) ;
305
+ const optionsWithAnalytics = new Map < string , string > ( ) ;
306
+
307
+ for ( const option of options ) {
308
+ const {
309
+ default : defaultVal ,
310
+ positional,
311
+ deprecated,
312
+ description,
313
+ alias,
314
+ userAnalytics,
315
+ type,
316
+ itemValueType,
317
+ hidden,
318
+ name,
319
+ choices,
320
+ } = option ;
321
+
322
+ const sharedOptions : YargsOptions & PositionalOptions = {
323
+ alias,
324
+ hidden,
325
+ description,
326
+ deprecated,
327
+ choices,
328
+ // This should only be done when `--help` is used otherwise default will override options set in angular.json.
329
+ ...( includeDefaultValues ? { default : defaultVal } : { } ) ,
330
+ } ;
331
+
332
+ let dashedName = strings . dasherize ( name ) ;
333
+
334
+ // Handle options which have been defined in the schema with `no` prefix.
335
+ if ( type === 'boolean' && dashedName . startsWith ( 'no-' ) ) {
336
+ dashedName = dashedName . slice ( 3 ) ;
337
+ booleanOptionsWithNoPrefix . add ( dashedName ) ;
338
+ }
339
+
340
+ if ( itemValueType ) {
341
+ keyValuePairOptions . add ( name ) ;
342
+ }
343
+
344
+ if ( positional === undefined ) {
345
+ localYargs = localYargs . option ( dashedName , {
346
+ array : itemValueType ? true : undefined ,
347
+ type : itemValueType ?? type ,
348
+ ...sharedOptions ,
349
+ } ) ;
350
+ } else {
351
+ localYargs = localYargs . positional ( dashedName , {
352
+ type : type === 'array' || type === 'count' ? 'string' : type ,
353
+ ...sharedOptions ,
354
+ } ) ;
355
+ }
356
+
357
+ // Record option of analytics.
358
+ if ( userAnalytics !== undefined ) {
359
+ optionsWithAnalytics . set ( name , userAnalytics ) ;
360
+ }
361
+ }
362
+
363
+ // Handle options which have been defined in the schema with `no` prefix.
364
+ if ( booleanOptionsWithNoPrefix . size ) {
365
+ localYargs . middleware ( ( options : Arguments ) => {
366
+ for ( const key of booleanOptionsWithNoPrefix ) {
367
+ if ( key in options ) {
368
+ options [ `no-${ key } ` ] = ! options [ key ] ;
369
+ delete options [ key ] ;
370
+ }
371
+ }
372
+ } , false ) ;
373
+ }
374
+
375
+ if ( keyValuePairOptions . size ) {
376
+ localYargs . middleware ( stringMapMiddleware ( keyValuePairOptions ) , true ) ;
377
+ }
378
+
379
+ return optionsWithAnalytics ;
380
+ }
0 commit comments