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,23 @@ 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
+ coerce : coerceToStringMap . bind ( null , strings . dasherize ( name ) ) ,
268
+ itemValueType : 'string' ,
269
+ }
270
+ : {
271
+ type : types [ 0 ] ,
272
+ } ) ,
195
273
} ;
196
274
197
275
options . push ( option ) ;
@@ -211,3 +289,93 @@ export async function parseJsonSchemaToOptions(
211
289
return a . name . localeCompare ( b . name ) ;
212
290
} ) ;
213
291
}
292
+
293
+ /**
294
+ * Adds schema options to a command also this keeps track of options that are required for analytics.
295
+ * **Note:** This method should be called from the command bundler method.
296
+ *
297
+ * @returns A map from option name to analytics configuration.
298
+ */
299
+ export function addSchemaOptionsToCommand < T > (
300
+ localYargs : Argv < T > ,
301
+ options : Option [ ] ,
302
+ includeDefaultValues : boolean ,
303
+ ) : Map < string , string > {
304
+ const booleanOptionsWithNoPrefix = new Set < string > ( ) ;
305
+ const keyValuePairOptions = new Set < string > ( ) ;
306
+ const optionsWithAnalytics = new Map < string , string > ( ) ;
307
+
308
+ for ( const option of options ) {
309
+ const {
310
+ default : defaultVal ,
311
+ positional,
312
+ deprecated,
313
+ description,
314
+ alias,
315
+ userAnalytics,
316
+ type,
317
+ itemValueType,
318
+ hidden,
319
+ name,
320
+ choices,
321
+ } = option ;
322
+
323
+ const sharedOptions : YargsOptions & PositionalOptions = {
324
+ alias,
325
+ hidden,
326
+ description,
327
+ deprecated,
328
+ choices,
329
+ // This should only be done when `--help` is used otherwise default will override options set in angular.json.
330
+ ...( includeDefaultValues ? { default : defaultVal } : { } ) ,
331
+ } ;
332
+
333
+ let dashedName = strings . dasherize ( name ) ;
334
+
335
+ // Handle options which have been defined in the schema with `no` prefix.
336
+ if ( type === 'boolean' && dashedName . startsWith ( 'no-' ) ) {
337
+ dashedName = dashedName . slice ( 3 ) ;
338
+ booleanOptionsWithNoPrefix . add ( dashedName ) ;
339
+ }
340
+
341
+ if ( itemValueType ) {
342
+ keyValuePairOptions . add ( name ) ;
343
+ }
344
+
345
+ if ( positional === undefined ) {
346
+ localYargs = localYargs . option ( dashedName , {
347
+ array : itemValueType ? true : undefined ,
348
+ type : itemValueType ?? type ,
349
+ ...sharedOptions ,
350
+ } ) ;
351
+ } else {
352
+ localYargs = localYargs . positional ( dashedName , {
353
+ type : type === 'array' || type === 'count' ? 'string' : type ,
354
+ ...sharedOptions ,
355
+ } ) ;
356
+ }
357
+
358
+ // Record option of analytics.
359
+ if ( userAnalytics !== undefined ) {
360
+ optionsWithAnalytics . set ( name , userAnalytics ) ;
361
+ }
362
+ }
363
+
364
+ // Handle options which have been defined in the schema with `no` prefix.
365
+ if ( booleanOptionsWithNoPrefix . size ) {
366
+ localYargs . middleware ( ( options : Arguments ) => {
367
+ for ( const key of booleanOptionsWithNoPrefix ) {
368
+ if ( key in options ) {
369
+ options [ `no-${ key } ` ] = ! options [ key ] ;
370
+ delete options [ key ] ;
371
+ }
372
+ }
373
+ } , false ) ;
374
+ }
375
+
376
+ if ( keyValuePairOptions . size ) {
377
+ localYargs . middleware ( stringMapMiddleware ( keyValuePairOptions ) , true ) ;
378
+ }
379
+
380
+ return optionsWithAnalytics ;
381
+ }
0 commit comments