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