@oclif/core 2.8.12 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,7 +48,7 @@ export type Metadata = {
48
48
  [key: string]: MetadataFlag;
49
49
  };
50
50
  };
51
- type MetadataFlag = {
51
+ export type MetadataFlag = {
52
52
  setFromDefault?: boolean;
53
53
  defaultHelp?: unknown;
54
54
  };
@@ -368,4 +368,3 @@ export type ArgInput<T extends ArgOutput = {
368
368
  }> = {
369
369
  [P in keyof T]: Arg<T[P]>;
370
370
  };
371
- export {};
@@ -6,17 +6,14 @@ export declare class Parser<T extends ParserInput, TFlags extends OutputFlags<T[
6
6
  private readonly booleanFlags;
7
7
  private readonly flagAliases;
8
8
  private readonly context;
9
- private readonly metaData;
10
9
  private currentFlag?;
11
10
  constructor(input: T);
12
11
  parse(): Promise<ParserOutput<TFlags, BFlags, TArgs>>;
13
12
  private _flags;
14
- private _parseFlag;
15
- private _validateOptions;
16
13
  private _args;
17
14
  private _debugOutput;
18
15
  private _debugInput;
19
16
  private get _argTokens();
20
- private get _flagTokens();
21
17
  private _setNames;
18
+ private mapAndValidateFlags;
22
19
  }
@@ -63,7 +63,6 @@ class Parser {
63
63
  this.flagAliases = Object.fromEntries(Object.values(input.flags).flatMap(flag => {
64
64
  return (flag.aliases ?? []).map(a => [a, flag]);
65
65
  }));
66
- this.metaData = {};
67
66
  }
68
67
  async parse() {
69
68
  this._debugInput();
@@ -163,8 +162,7 @@ class Parser {
163
162
  const arg = Object.keys(this.input.args)[this._argTokens.length];
164
163
  this.raw.push({ type: 'arg', arg, input });
165
164
  }
166
- const { argv, args } = await this._args();
167
- const flags = await this._flags();
165
+ const [{ argv, args }, { flags, metadata }] = await Promise.all([this._args(), this._flags()]);
168
166
  this._debugOutput(argv, args, flags);
169
167
  const unsortedArgv = (dashdash ? [...argv, ...nonExistentFlags, '--'] : [...argv, ...nonExistentFlags]);
170
168
  return {
@@ -172,113 +170,118 @@ class Parser {
172
170
  flags,
173
171
  args: args,
174
172
  raw: this.raw,
175
- metadata: this.metaData,
173
+ metadata,
176
174
  nonExistentFlags,
177
175
  };
178
176
  }
179
- // eslint-disable-next-line complexity
180
177
  async _flags() {
181
- const flags = {};
182
- this.metaData.flags = {};
183
- for (const token of this._flagTokens) {
184
- const flag = this.input.flags[token.flag];
185
- if (!flag)
186
- throw new errors_1.CLIError(`Unexpected flag ${token.flag}`);
187
- if (flag.type === 'boolean') {
188
- if (token.input === `--no-${flag.name}`) {
189
- flags[token.flag] = false;
190
- }
191
- else {
192
- flags[token.flag] = true;
178
+ const validateOptions = (flag, input) => {
179
+ if (flag.options && !flag.options.includes(input))
180
+ throw new errors_1.FlagInvalidOptionError(flag, input);
181
+ return input;
182
+ };
183
+ const parseFlagOrThrowError = async (input, flag, token, context = {}) => {
184
+ if (!flag.parse)
185
+ return input;
186
+ try {
187
+ if (flag.type === 'boolean') {
188
+ return await flag.parse(input, { ...context, token }, flag);
193
189
  }
194
- flags[token.flag] = await this._parseFlag(flags[token.flag], flag, token);
190
+ return await flag.parse(input, { ...context, token }, flag);
195
191
  }
196
- else {
197
- const input = token.input;
198
- if (flag.delimiter && flag.multiple) {
199
- // split, trim, and remove surrounding doubleQuotes (which would hav been needed if the elements contain spaces)
200
- const values = await Promise.all(input.split(flag.delimiter).map(async (v) => this._parseFlag(v.trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1'), flag, token)));
201
- // then parse that each element aligns with the `options` property
202
- for (const v of values) {
203
- this._validateOptions(flag, v);
204
- }
205
- flags[token.flag] = flags[token.flag] || [];
206
- flags[token.flag].push(...values);
192
+ catch (error) {
193
+ error.message = `Parsing --${flag.name} \n\t${error.message}\nSee more help with --help`;
194
+ throw error;
195
+ }
196
+ };
197
+ /* Could add a valueFunction (if there is a value/env/default) and could metadata.
198
+ * Value function can be resolved later.
199
+ */
200
+ const addValueFunction = (fws) => {
201
+ const tokenLength = fws.tokens?.length;
202
+ // user provided some input
203
+ if (tokenLength) {
204
+ // boolean
205
+ if (fws.inputFlag.flag.type === 'boolean' && fws.tokens?.at(-1)?.input) {
206
+ return { ...fws, valueFunction: async (i) => parseFlagOrThrowError(i.tokens?.at(-1)?.input !== `--no-${i.inputFlag.name}`, i.inputFlag.flag, i.tokens?.at(-1), this.context) };
207
207
  }
208
- else {
209
- this._validateOptions(flag, input);
210
- const value = await this._parseFlag(input, flag, token);
211
- if (flag.multiple) {
212
- flags[token.flag] = flags[token.flag] || [];
213
- flags[token.flag].push(value);
214
- }
215
- else {
216
- flags[token.flag] = value;
217
- }
208
+ // multiple with custom delimiter
209
+ if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.delimiter && fws.inputFlag.flag.multiple) {
210
+ return {
211
+ ...fws, valueFunction: async (i) => (await Promise.all(((i.tokens ?? []).flatMap(token => token.input.split(i.inputFlag.flag.delimiter)))
212
+ // trim, and remove surrounding doubleQuotes (which would hav been needed if the elements contain spaces)
213
+ .map(v => v.trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1'))
214
+ .map(async (v) => parseFlagOrThrowError(v, i.inputFlag.flag, { ...i.tokens?.at(-1), input: v }, this.context)))).map(v => validateOptions(i.inputFlag.flag, v)),
215
+ };
218
216
  }
219
- }
220
- }
221
- for (const k of Object.keys(this.input.flags)) {
222
- const flag = this.input.flags[k];
223
- if (flags[k])
224
- continue;
225
- if (flag.env && Reflect.has(process.env, flag.env)) {
226
- const input = process.env[flag.env];
227
- if (flag.type === 'option') {
228
- if (input) {
229
- this._validateOptions(flag, input);
230
- flags[k] = await this._parseFlag(input, flag);
231
- }
217
+ // multiple in the oclif-core style
218
+ if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.multiple) {
219
+ return { ...fws, valueFunction: async (i) => Promise.all((fws.tokens ?? []).map(token => parseFlagOrThrowError(validateOptions(i.inputFlag.flag, token.input), i.inputFlag.flag, token, this.context))) };
232
220
  }
233
- else if (flag.type === 'boolean') {
234
- // eslint-disable-next-line no-negated-condition
235
- flags[k] = input !== undefined ? (0, util_1.isTruthy)(input) : false;
221
+ // simple option flag
222
+ if (fws.inputFlag.flag.type === 'option') {
223
+ return { ...fws, valueFunction: async (i) => parseFlagOrThrowError(validateOptions(i.inputFlag.flag, fws.tokens?.at(-1)?.input), i.inputFlag.flag, fws.tokens?.at(-1), this.context) };
236
224
  }
237
225
  }
238
- if (!(k in flags) && flag.default !== undefined) {
239
- this.metaData.flags[k] = { ...this.metaData.flags[k], setFromDefault: true };
240
- const defaultValue = (typeof flag.default === 'function' ? await flag.default({ options: flag, flags }) : flag.default);
241
- flags[k] = defaultValue;
242
- }
243
- }
244
- for (const k of Object.keys(this.input.flags)) {
245
- if ((k in flags) && Reflect.has(this.input.flags[k], 'defaultHelp')) {
246
- try {
247
- const defaultHelpProperty = Reflect.get(this.input.flags[k], 'defaultHelp');
248
- const defaultHelp = (typeof defaultHelpProperty === 'function' ? await defaultHelpProperty({
249
- options: flags[k],
250
- flags, ...this.context,
251
- }) : defaultHelpProperty);
252
- this.metaData.flags[k] = { ...this.metaData.flags[k], defaultHelp };
226
+ // no input: env flags
227
+ if (fws.inputFlag.flag.env && process.env[fws.inputFlag.flag.env]) {
228
+ const valueFromEnv = process.env[fws.inputFlag.flag.env];
229
+ if (fws.inputFlag.flag.type === 'option' && valueFromEnv) {
230
+ return { ...fws, valueFunction: async (i) => parseFlagOrThrowError(validateOptions(i.inputFlag.flag, valueFromEnv), i.inputFlag.flag, this.context) };
253
231
  }
254
- catch {
255
- // no-op
232
+ if (fws.inputFlag.flag.type === 'boolean') {
233
+ return { ...fws, valueFunction: async (i) => Promise.resolve((0, util_1.isTruthy)(process.env[i.inputFlag.flag.env] ?? 'false')) };
256
234
  }
257
235
  }
258
- }
259
- return flags;
260
- }
261
- async _parseFlag(input, flag, token) {
262
- if (!flag.parse)
263
- return input;
264
- try {
265
- const ctx = this.context;
266
- ctx.token = token;
267
- if (flag.type === 'boolean') {
268
- const ctx = this.context;
269
- ctx.token = token;
270
- return await flag.parse(input, ctx, flag);
236
+ // no input, but flag has default value
237
+ if (typeof fws.inputFlag.flag.default !== undefined) {
238
+ return {
239
+ ...fws, metadata: { setFromDefault: true },
240
+ valueFunction: typeof fws.inputFlag.flag.default === 'function' ?
241
+ (i, allFlags = {}) => fws.inputFlag.flag.default({ options: i.inputFlag.flag, flags: allFlags }) :
242
+ async () => fws.inputFlag.flag.default,
243
+ };
271
244
  }
272
- return flag.parse ? await flag.parse(input, ctx, flag) : input;
273
- }
274
- catch (error) {
275
- error.message = `Parsing --${flag.name} \n\t${error.message}\nSee more help with --help`;
276
- throw error;
277
- }
278
- }
279
- _validateOptions(flag, input) {
280
- if (flag.options && !flag.options.includes(input))
281
- throw new errors_1.FlagInvalidOptionError(flag, input);
245
+ // base case (no value function)
246
+ return fws;
247
+ };
248
+ const addHelpFunction = (fws) => {
249
+ if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.defaultHelp) {
250
+ return {
251
+ ...fws, helpFunction: typeof fws.inputFlag.flag.defaultHelp === 'function' ?
252
+ // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there
253
+ (i, flags, ...context) => i.inputFlag.flag.defaultHelp({ options: i.inputFlag, flags }, ...context) :
254
+ // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there
255
+ (i) => i.inputFlag.flag.defaultHelp,
256
+ };
257
+ }
258
+ return fws;
259
+ };
260
+ const addDefaultHelp = async (fws) => {
261
+ const valueReferenceForHelp = fwsArrayToObject(flagsWithAllValues.filter(fws => !fws.metadata?.setFromDefault));
262
+ return Promise.all(fws.map(async (fws) => fws.helpFunction ? ({ ...fws, metadata: { ...fws.metadata, defaultHelp: await fws.helpFunction?.(fws, valueReferenceForHelp, this.context) } }) : fws));
263
+ };
264
+ const fwsArrayToObject = (fwsArray) => Object.fromEntries(fwsArray.map(fws => [fws.inputFlag.name, fws.value]));
265
+ const flagTokenMap = this.mapAndValidateFlags();
266
+ const flagsWithValues = await Promise.all(Object.entries(this.input.flags)
267
+ // we check them if they have a token, or might have env, default, or defaultHelp. Also include booleans so they get their default value
268
+ .filter(([name, flag]) => flag.type === 'boolean' || flag.env || flag.default || 'defaultHelp' in flag || flagTokenMap.has(name))
269
+ // match each possible flag to its token, if there is one
270
+ .map(([name, flag]) => ({ inputFlag: { name, flag }, tokens: flagTokenMap.get(name) }))
271
+ .map(fws => addValueFunction(fws))
272
+ .filter(fws => fws.valueFunction !== undefined)
273
+ .map(fws => addHelpFunction(fws))
274
+ // we can't apply the default values until all the other flags are resolved because `flag.default` can reference other flags
275
+ .map(async (fws) => (fws.metadata?.setFromDefault ? fws : { ...fws, value: await fws.valueFunction?.(fws) })));
276
+ const valueReference = fwsArrayToObject(flagsWithValues.filter(fws => !fws.metadata?.setFromDefault));
277
+ const flagsWithAllValues = await Promise.all(flagsWithValues
278
+ .map(async (fws) => (fws.metadata?.setFromDefault ? { ...fws, value: await fws.valueFunction?.(fws, valueReference) } : fws)));
279
+ const finalFlags = (flagsWithAllValues.some(fws => typeof fws.helpFunction === 'function')) ? await addDefaultHelp(flagsWithAllValues) : flagsWithAllValues;
280
+ return {
281
+ // @ts-ignore original version returned an any. Not sure how to get to the return type for `flags` prop
282
+ flags: fwsArrayToObject(finalFlags),
283
+ metadata: { flags: Object.fromEntries(finalFlags.filter(fws => fws.metadata).map(fws => [fws.inputFlag.name, fws.metadata])) },
284
+ };
282
285
  }
283
286
  async _args() {
284
287
  const argv = [];
@@ -352,9 +355,6 @@ class Parser {
352
355
  get _argTokens() {
353
356
  return this.raw.filter(o => o.type === 'arg');
354
357
  }
355
- get _flagTokens() {
356
- return this.raw.filter(o => o.type === 'flag');
357
- }
358
358
  _setNames() {
359
359
  for (const k of Object.keys(this.input.flags)) {
360
360
  this.input.flags[k].name = k;
@@ -363,5 +363,17 @@ class Parser {
363
363
  this.input.args[k].name = k;
364
364
  }
365
365
  }
366
+ mapAndValidateFlags() {
367
+ const flagTokenMap = new Map();
368
+ for (const token of this.raw.filter(o => o.type === 'flag')) {
369
+ // fail fast if there are any invalid flags
370
+ if (!(token.flag in this.input.flags)) {
371
+ throw new errors_1.CLIError(`Unexpected flag ${token.flag}`);
372
+ }
373
+ const existing = flagTokenMap.get(token.flag) ?? [];
374
+ flagTokenMap.set(token.flag, [...existing, token]);
375
+ }
376
+ return flagTokenMap;
377
+ }
366
378
  }
367
379
  exports.Parser = Parser;
@@ -4,6 +4,7 @@ exports.validate = void 0;
4
4
  const errors_1 = require("./errors");
5
5
  const util_1 = require("../config/util");
6
6
  async function validate(parse) {
7
+ let cachedResolvedFlags;
7
8
  function validateArgs() {
8
9
  if (parse.output.nonExistentFlags?.length > 0) {
9
10
  throw new errors_1.NonExistentFlagsError({ parse, flags: parse.output.nonExistentFlags });
@@ -33,25 +34,31 @@ async function validate(parse) {
33
34
  }
34
35
  }
35
36
  async function validateFlags() {
36
- const promises = Object.entries(parse.input.flags).map(async ([name, flag]) => {
37
- const results = [];
37
+ const promises = Object.entries(parse.input.flags).flatMap(([name, flag]) => {
38
38
  if (parse.output.flags[name] !== undefined) {
39
- results.push(...await validateRelationships(name, flag), await validateDependsOn(name, flag.dependsOn ?? []), await validateExclusive(name, flag.exclusive ?? []), await validateExactlyOne(name, flag.exactlyOne ?? []));
39
+ return [
40
+ ...flag.relationships ? validateRelationships(name, flag) : [],
41
+ ...flag.dependsOn ? [validateDependsOn(name, flag.dependsOn)] : [],
42
+ ...flag.exclusive ? [validateExclusive(name, flag.exclusive)] : [],
43
+ ...flag.exactlyOne ? [validateExactlyOne(name, flag.exactlyOne)] : [],
44
+ ];
40
45
  }
41
- else if (flag.required) {
42
- results.push({ status: 'failed', name, validationFn: 'required', reason: `Missing required flag ${name}` });
46
+ if (flag.required) {
47
+ return [{ status: 'failed', name, validationFn: 'required', reason: `Missing required flag ${name}` }];
43
48
  }
44
- else if (flag.exactlyOne && flag.exactlyOne.length > 0) {
45
- results.push(validateAcrossFlags(flag));
49
+ if (flag.exactlyOne && flag.exactlyOne.length > 0) {
50
+ return [validateAcrossFlags(flag)];
46
51
  }
47
- return results;
52
+ return [];
48
53
  });
49
- const results = (await Promise.all(promises)).flat();
54
+ const results = (await Promise.all(promises));
50
55
  const failed = results.filter(r => r.status === 'failed');
51
56
  if (failed.length > 0)
52
57
  throw new errors_1.FailedFlagValidationError({ parse, failed });
53
58
  }
54
59
  async function resolveFlags(flags) {
60
+ if (cachedResolvedFlags)
61
+ return cachedResolvedFlags;
55
62
  const promises = flags.map(async (flag) => {
56
63
  if (typeof flag === 'string') {
57
64
  return [flag, parse.output.flags[flag]];
@@ -60,15 +67,10 @@ async function validate(parse) {
60
67
  return result ? [flag.name, parse.output.flags[flag.name]] : null;
61
68
  });
62
69
  const resolved = await Promise.all(promises);
63
- return Object.fromEntries(resolved.filter(r => r !== null));
64
- }
65
- function getPresentFlags(flags) {
66
- return Object.keys(flags).reduce((acc, key) => {
67
- if (flags[key] !== undefined)
68
- acc.push(key);
69
- return acc;
70
- }, []);
70
+ cachedResolvedFlags = Object.fromEntries(resolved.filter(r => r !== null));
71
+ return cachedResolvedFlags;
71
72
  }
73
+ const getPresentFlags = (flags) => Object.keys(flags).filter(key => key !== undefined);
72
74
  function validateAcrossFlags(flag) {
73
75
  const base = { name: flag.name, validationFn: 'validateAcrossFlags' };
74
76
  const intersection = Object.entries(parse.input.flags)
@@ -131,30 +133,21 @@ async function validate(parse) {
131
133
  }
132
134
  return { ...base, status: 'success' };
133
135
  }
134
- async function validateRelationships(name, flag) {
135
- if (!flag.relationships)
136
- return [];
137
- const results = await Promise.all(flag.relationships.map(async (relationship) => {
138
- const flags = relationship.flags ?? [];
139
- const results = [];
136
+ function validateRelationships(name, flag) {
137
+ return ((flag.relationships ?? []).map(relationship => {
140
138
  switch (relationship.type) {
141
139
  case 'all':
142
- results.push(await validateDependsOn(name, flags));
143
- break;
140
+ return validateDependsOn(name, relationship.flags);
144
141
  case 'some':
145
- results.push(await validateSome(name, flags));
146
- break;
142
+ return validateSome(name, relationship.flags);
147
143
  case 'none':
148
- results.push(await validateExclusive(name, flags));
149
- break;
144
+ return validateExclusive(name, relationship.flags);
150
145
  default:
151
- break;
146
+ throw new Error(`Unknown relationship type: ${relationship.type}`);
152
147
  }
153
- return results;
154
148
  }));
155
- return results.flat();
156
149
  }
157
150
  validateArgs();
158
- await validateFlags();
151
+ return validateFlags();
159
152
  }
160
153
  exports.validate = validate;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oclif/core",
3
3
  "description": "base library for oclif CLIs",
4
- "version": "2.8.12",
4
+ "version": "2.9.0",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/core/issues",
7
7
  "dependencies": {
@@ -42,6 +42,7 @@
42
42
  "@oclif/plugin-plugins": "^2.4.7",
43
43
  "@oclif/test": "^2.3.15",
44
44
  "@types/ansi-styles": "^3.2.1",
45
+ "@types/benchmark": "^2.1.2",
45
46
  "@types/chai": "^4.3.4",
46
47
  "@types/chai-as-promised": "^7.1.5",
47
48
  "@types/clean-stack": "^2.1.1",
@@ -61,6 +62,7 @@
61
62
  "@types/supports-color": "^8.1.1",
62
63
  "@types/wordwrap": "^1.0.1",
63
64
  "@types/wrap-ansi": "^3.0.0",
65
+ "benchmark": "^2.1.4",
64
66
  "chai": "^4.3.7",
65
67
  "chai-as-promised": "^7.1.1",
66
68
  "commitlint": "^12.1.4",
@@ -114,7 +116,8 @@
114
116
  "prepack": "yarn run build",
115
117
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
116
118
  "test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --timeout 1200000",
117
- "pretest": "yarn build --noEmit && tsc -p test --noEmit --skipLibCheck"
119
+ "pretest": "yarn build --noEmit && tsc -p test --noEmit --skipLibCheck",
120
+ "test:perf": "ts-node test/perf/parser.perf.ts"
118
121
  },
119
122
  "types": "lib/index.d.ts"
120
123
  }