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,62 @@ 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
+ if ( node . properties ) {
85
+ return false ;
86
+ }
87
+ if ( node . patternProperties ) {
88
+ return false ;
89
+ }
90
+ if ( ! json . isJsonObject ( node . additionalProperties ) ) {
91
+ return false ;
92
+ }
93
+
94
+ if ( node . additionalProperties . type !== 'string' ) {
95
+ return false ;
96
+ }
97
+ if ( node . additionalProperties . enum ) {
98
+ return false ;
99
+ }
100
+
101
+ return true ;
46
102
}
47
103
48
104
export async function parseJsonSchemaToOptions (
@@ -106,10 +162,13 @@ export async function parseJsonSchemaToOptions(
106
162
107
163
return false ;
108
164
165
+ case 'object' :
166
+ return isStringMap ( current ) ;
167
+
109
168
default :
110
169
return false ;
111
170
}
112
- } ) as ( 'string' | 'number' | 'boolean' | 'array' ) [ ] ;
171
+ } ) as ( 'string' | 'number' | 'boolean' | 'array' | 'object' ) [ ] ;
113
172
114
173
if ( types . length == 0 ) {
115
174
// This means it's not usable on the command line. e.g. an Object.
@@ -150,7 +209,6 @@ export async function parseJsonSchemaToOptions(
150
209
}
151
210
}
152
211
153
- const type = types [ 0 ] ;
154
212
const $default = current . $default ;
155
213
const $defaultIndex =
156
214
json . isJsonObject ( $default ) && $default [ '$source' ] == 'argv' ? $default [ 'index' ] : undefined ;
@@ -182,7 +240,6 @@ export async function parseJsonSchemaToOptions(
182
240
const option : Option = {
183
241
name,
184
242
description : '' + ( current . description === undefined ? '' : current . description ) ,
185
- type,
186
243
default : defaultValue ,
187
244
choices : enumValues . length ? enumValues : undefined ,
188
245
required,
@@ -192,6 +249,14 @@ export async function parseJsonSchemaToOptions(
192
249
userAnalytics,
193
250
deprecated,
194
251
positional,
252
+ ...( types [ 0 ] === 'object'
253
+ ? {
254
+ type : 'array' ,
255
+ itemValueType : 'string' ,
256
+ }
257
+ : {
258
+ type : types [ 0 ] ,
259
+ } ) ,
195
260
} ;
196
261
197
262
options . push ( option ) ;
@@ -211,3 +276,90 @@ export async function parseJsonSchemaToOptions(
211
276
return a . name . localeCompare ( b . name ) ;
212
277
} ) ;
213
278
}
279
+
280
+ /**
281
+ * Adds schema options to a command also this keeps track of options that are required for analytics.
282
+ * **Note:** This method should be called from the command bundler method.
283
+ *
284
+ * @returns A map from option name to analytics configuration.
285
+ */
286
+ export function addSchemaOptionsToCommand < T > (
287
+ localYargs : Argv < T > ,
288
+ options : Option [ ] ,
289
+ includeDefaultValues : boolean ,
290
+ ) : Map < string , string > {
291
+ const booleanOptionsWithNoPrefix = new Set < string > ( ) ;
292
+ const keyValuePairOptions = new Set < string > ( ) ;
293
+ const optionsWithAnalytics = new Map < string , string > ( ) ;
294
+
295
+ for ( const option of options ) {
296
+ const {
297
+ default : defaultVal ,
298
+ positional,
299
+ deprecated,
300
+ description,
301
+ alias,
302
+ userAnalytics,
303
+ type,
304
+ itemValueType,
305
+ hidden,
306
+ name,
307
+ choices,
308
+ } = option ;
309
+
310
+ let dashedName = strings . dasherize ( name ) ;
311
+
312
+ // Handle options which have been defined in the schema with `no` prefix.
313
+ if ( type === 'boolean' && dashedName . startsWith ( 'no-' ) ) {
314
+ dashedName = dashedName . slice ( 3 ) ;
315
+ booleanOptionsWithNoPrefix . add ( dashedName ) ;
316
+ }
317
+
318
+ if ( itemValueType ) {
319
+ keyValuePairOptions . add ( name ) ;
320
+ }
321
+
322
+ const sharedOptions : YargsOptions & PositionalOptions = {
323
+ alias,
324
+ hidden,
325
+ description,
326
+ deprecated,
327
+ choices,
328
+ coerce : itemValueType ? coerceToStringMap . bind ( null , dashedName ) : undefined ,
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
+ if ( positional === undefined ) {
334
+ localYargs = localYargs . option ( dashedName , {
335
+ array : itemValueType ? true : undefined ,
336
+ type : itemValueType ?? type ,
337
+ ...sharedOptions ,
338
+ } ) ;
339
+ } else {
340
+ localYargs = localYargs . positional ( dashedName , {
341
+ type : type === 'array' || type === 'count' ? 'string' : type ,
342
+ ...sharedOptions ,
343
+ } ) ;
344
+ }
345
+
346
+ // Record option of analytics.
347
+ if ( userAnalytics !== undefined ) {
348
+ optionsWithAnalytics . set ( name , userAnalytics ) ;
349
+ }
350
+ }
351
+
352
+ // Handle options which have been defined in the schema with `no` prefix.
353
+ if ( booleanOptionsWithNoPrefix . size ) {
354
+ localYargs . middleware ( ( options : Arguments ) => {
355
+ for ( const key of booleanOptionsWithNoPrefix ) {
356
+ if ( key in options ) {
357
+ options [ `no-${ key } ` ] = ! options [ key ] ;
358
+ delete options [ key ] ;
359
+ }
360
+ }
361
+ } , false ) ;
362
+ }
363
+
364
+ return optionsWithAnalytics ;
365
+ }
0 commit comments