@percy/cli-command 1.0.0-beta.8 → 1.0.1

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.
package/dist/parse.js ADDED
@@ -0,0 +1,400 @@
1
+ import logger from '@percy/logger';
2
+ import { camelcase } from '@percy/config/utils';
3
+ import { flagUsage } from './help.js'; // Make it possible to identify parse errors.
4
+
5
+ export class ParseError extends Error {
6
+ name = this.constructor.name;
7
+ } // Helper that makes it possible to throw parse errors without throw or new keywords.
8
+
9
+ function parseError(msg) {
10
+ throw new ParseError(msg);
11
+ } // Helper to identify a potential command-line option as a flag.
12
+
13
+
14
+ function isFlag(option) {
15
+ // not a flag if it doesn't start with a dash
16
+ if (!option.startsWith('-')) return false; // safe to say it's a flag without a space
17
+
18
+ if (!option.includes(' ')) return true; // containing a space with no operator is not a proper flag
19
+
20
+ if (!option.includes('=')) return false; // as long as the operator precedes the space, it is a flag
21
+
22
+ return option.indexOf('=') < option.indexOf(' ');
23
+ } // Parses input to identify a command. Consumes input and adds to parsed output as needed, returning
24
+ // true if any defined command was found. A command is only identified via leading unparsed input
25
+ // options. If a command is found, parsed output is updated accordingly and the current input option
26
+ // is removed from further processing. An implicit help command is also supported.
27
+
28
+
29
+ async function maybeParseCommand(input, parsed) {
30
+ var _input$;
31
+
32
+ // a flag, no defined commands, or already parsed other options
33
+ if (isFlag(input[0]) || !parsed.command.definition.commands || parsed.operators.size || parsed.argv.length) {
34
+ return;
35
+ } // implicit help
36
+
37
+
38
+ if (input[0] === 'help') {
39
+ parsed.help = true;
40
+ input.shift();
41
+ } // nested commands can be a single argument with command names separated by colons (:)
42
+
43
+
44
+ let names = ((_input$ = input[0]) === null || _input$ === void 0 ? void 0 : _input$.split(':')) ?? [];
45
+ let command;
46
+
47
+ for (let name of names) {
48
+ var _parent$definition$co;
49
+
50
+ let parent = command || parsed.command; // find any nested command by name
51
+
52
+ command = (_parent$definition$co = parent.definition.commands) === null || _parent$definition$co === void 0 ? void 0 : _parent$definition$co.find(cmd => cmd.name === name);
53
+ if (!command) break; // maintain nested names and parent relationships
54
+
55
+ name = parent.parent ? `${parent.name}:${command.name}` : command.name;
56
+ command = await normalizeCommand(command, {
57
+ name,
58
+ parent
59
+ });
60
+ }
61
+
62
+ if (command) {
63
+ // update parsed properties and discard current input
64
+ parsed.log = logger(`cli:${command.name}`);
65
+ parsed.command = command;
66
+ input.shift();
67
+ } // help was requested, drain remaining input
68
+
69
+
70
+ if (parsed.help) {
71
+ input.splice(0, input.length);
72
+ }
73
+
74
+ return parsed.help || !!command;
75
+ } // Parses input to identify a flag option. Consumes input and adds to parsed output as needed,
76
+ // returning true if any defined flag was found. If a found flag is missing a required argument, the
77
+ // next input option will be consumed as that argument. Boolean flags support negation if their
78
+ // default value is true, integer flags are automatically parsed, and flags that can be provided
79
+ // multiple times will be concatenated together. Implicit help and version flags are also supported.
80
+
81
+
82
+ async function maybeParseFlag(input, parsed) {
83
+ var _value;
84
+
85
+ if (!isFlag(input[0])) return; // end of options and arguments, consume remaining input
86
+
87
+ if (input[0] === '--') {
88
+ let [, ...argv] = input.splice(0, input.length);
89
+ parsed.argv.push(...argv);
90
+ return true;
91
+ } // a short flag was provided
92
+
93
+
94
+ let short = input[0][1] !== '-' && input[0][1]; // get any value set with `=` and remove any `=` from short values
95
+
96
+ let [name, value] = short ? ['', input[0].substr(2) || null] : input[0].split('=');
97
+ value = short && (_value = value) !== null && _value !== void 0 && _value.startsWith('=') ? value.substr(1) : value; // the flag was negated
98
+
99
+ let negated = name.startsWith('--no-');
100
+ name = name.replace(/^--(no-)?/, ''); // search for the provided short or long flag
101
+
102
+ let flag = parsed.command.definition.flags.find(f => {
103
+ return short ? f.short === short : f.name === name;
104
+ });
105
+
106
+ if (flag) {
107
+ // discard current input
108
+ input.shift();
109
+
110
+ if (!flag.type || flag.type === 'boolean') {
111
+ // short boolean flags can be provided with other short flags
112
+ if (short && value) input.unshift(`-${value}`); // a value was provided to a boolean flag
113
+
114
+ if (!short && value) {
115
+ parseError(`Unexpected option argument for '${flagUsage(flag)}'`);
116
+ } // a boolean value is based on if it was negated
117
+
118
+
119
+ value = !negated;
120
+ } else if (value == null) {
121
+ // expected a value for a non-boolean flag
122
+ if (!input.length || isFlag(input[0])) {
123
+ parseError(`Missing option argument for '${flagUsage(flag)}'`);
124
+ } // the value is actually the next input option
125
+
126
+
127
+ value = input.shift();
128
+ } // parse the value according to flag properties
129
+
130
+
131
+ if (flag.type === 'integer') value = parseInt(value, 10);
132
+ if (flag.parse) value = await flag.parse(value);
133
+
134
+ if (flag.multiple) {
135
+ value = (parsed.operators.get(flag) ?? []).concat(value);
136
+ } // add this flag to the set of parsed operators
137
+
138
+
139
+ parsed.operators.set(flag, value);
140
+ return true;
141
+ } // implicit version and help support
142
+
143
+
144
+ if (parsed.command.definition.version && (name === 'version' || short === 'V')) {
145
+ parsed.version = true;
146
+ } else if (name === 'help' || short === 'h') {
147
+ parsed.help = true;
148
+ } // help or version was requested, drain remaining input
149
+
150
+
151
+ if (parsed.help || parsed.version) {
152
+ input.splice(0, input.length);
153
+ return true;
154
+ }
155
+ } // Parses input to identify an argument option. Consumes input and adds to parsed output as needed,
156
+ // returning true if any defined argument was found. Arguments are matched according to their
157
+ // respective index and command-line position.
158
+
159
+
160
+ async function maybeParseArg(input, parsed) {
161
+ var _parsed$command$defin;
162
+
163
+ if (isFlag(input[0])) return; // the argument index needs to be tracked since flag options can precede argument options
164
+
165
+ let index = parsed.__args_index__ ?? (parsed.__args_index__ = 0); // there are arguments left to parse
166
+
167
+ if (index < ((_parsed$command$defin = parsed.command.definition.args) === null || _parsed$command$defin === void 0 ? void 0 : _parsed$command$defin.length)) {
168
+ parsed.__args_index__ += 1;
169
+ let value = input.shift();
170
+ let arg = parsed.command.definition.args[index];
171
+ if (arg.parse) value = await arg.parse(value); // add this arg to the set of parsed operators
172
+
173
+ parsed.operators.set(arg, value);
174
+ return true;
175
+ }
176
+ } // Parses input to identify unknown options. Consumes input and adds to parsed output as needed,
177
+ // returning true when unknown options are found. By default, unknown options result in an error
178
+ // unless the command is defined to accept loose options. If `loose` is a string, it will be logged
179
+ // as a warning when unknown options are found. When the first unknown option is found, all
180
+ // remaining options are also considered unknown.
181
+
182
+
183
+ async function maybeParseUnknown(input, parsed) {
184
+ let {
185
+ loose
186
+ } = parsed.command.definition;
187
+
188
+ if (!loose) {
189
+ // throw a more descriptive error message
190
+ let reason = isFlag(input[0]) ? 'Unknown option' : 'Unexpected argument';
191
+ parseError(`${reason} '${input[0]}'`);
192
+ } else if (typeof loose === 'string') {
193
+ // warn when loose is a string
194
+ parsed.log.warn(loose);
195
+ } // consume all remaining unknown input options
196
+
197
+
198
+ let unknown = input.splice(0, input.length);
199
+ parsed.argv.push(...unknown);
200
+ return true;
201
+ } // Parses an option's value defined by an environment variable. Adds to parsed output as needed,
202
+ // returning true when the option's value was parsed from the environment. Boolean environment
203
+ // variables will be interpreted as true unless "false" or "0", and integer environment variables
204
+ // will be parsed automatically.
205
+
206
+
207
+ async function maybeParseEnv(option, parsed) {
208
+ // no env option, already provided, or no defined env var
209
+ if (!option.env || parsed.operators.has(option) || !(option.env in process.env)) {
210
+ return;
211
+ } // get the value from the process env
212
+
213
+
214
+ let value = process.env[option.env];
215
+ let flag = parsed.command.definition.flags.includes(option);
216
+
217
+ if (flag && (!option.type || option.type === 'boolean')) {
218
+ // parse booleans as not "false" or not "0"
219
+ value = value !== 'false' && value !== '0';
220
+ } else if (option.type === 'integer') {
221
+ // parse integer option values
222
+ value = parseInt(value, 10);
223
+ } // custom parse and add this option to the set of parsed operators
224
+
225
+
226
+ if (option.parse) value = await option.parse(value);
227
+ parsed.operators.set(option, value);
228
+ return true;
229
+ } // Helper to find and return a matching flag by name.
230
+
231
+
232
+ function findFlag(name, parsed, map) {
233
+ if (name.startsWith('--')) name = name.substr(2);
234
+ let {
235
+ flags
236
+ } = parsed.command.definition;
237
+ let match = flags.find(f => f.name === name);
238
+ return map ? map(match) : match;
239
+ } // Throws an error if the provided flag is inclusive of any missing flags.
240
+
241
+
242
+ function validateInclusiveFlag(flag, parsed) {
243
+ if (!flag.inclusive) return;
244
+ let invalid = flag.inclusive.reduce((invalid, name) => findFlag(name, parsed, flag => // invalid options are options that have _not_ been provided
245
+ !parsed.operators.has(flag) ? invalid.concat(flag) : invalid), []);
246
+
247
+ if (invalid.length) {
248
+ // show full flag usage for invalid options
249
+ invalid = [flag, ...invalid].map(f => flagUsage(f)).join("', '");
250
+ parseError(`Options must be used together: '${invalid}'`);
251
+ }
252
+ } // Throws an error if the provided flag is exclusive of other provided flags.
253
+
254
+
255
+ function validateExclusiveFlag(flag, parsed) {
256
+ if (!flag.exclusive) return;
257
+ let invalid = flag.exclusive.reduce((invalid, name) => findFlag(name, parsed, flag => // invalid options are options that _have_ been provided
258
+ parsed.operators.has(flag) ? invalid.concat(flag) : invalid), []);
259
+
260
+ if (invalid.length) {
261
+ // show full flag usage for invalid options
262
+ invalid = [flag, ...invalid].map(f => flagUsage(f)).join("', '");
263
+ parseError(`Options cannot be used together: '${invalid}'`);
264
+ }
265
+ } // Returns the attribute name of an option, which may be contingent on it's parsed value. If an
266
+ // option is deprecated and is recommended to be replaced with flag, the attribute name will be
267
+ // mapped to the replacement flag's attribute name. If a deprecated option is found in the provided
268
+ // operators, a deprecation warning will be logged with any recommendation message.
269
+
270
+
271
+ function attributeName(option, parsed) {
272
+ let {
273
+ attribute,
274
+ deprecated
275
+ } = option;
276
+ let value = parsed.operators.get(option) ?? option.default; // the option's attribute name might be contingent on it's value
277
+
278
+ let name = typeof attribute === 'function' ? attribute(value) : attribute; // default the attribute name to the camelcase name or short character
279
+
280
+ name || (name = option.name ? camelcase(option.name) : option.short); // deprecated options can be mapped to other options
281
+
282
+ if (deprecated) {
283
+ var _recommend;
284
+
285
+ let [until, recommend] = Array.isArray(deprecated) ? deprecated : [typeof deprecated === 'string' ? deprecated : '']; // recommended replacement flag
286
+
287
+ if ((_recommend = recommend) !== null && _recommend !== void 0 && _recommend.startsWith('--')) {
288
+ let opt = findFlag(recommend, parsed);
289
+ recommend = opt ? `Use '${flagUsage(opt)}' instead.` : '';
290
+ name = opt ? attributeName(opt, parsed) : name;
291
+ } // operator was provided, log deprecation warning
292
+
293
+
294
+ if (parsed.operators.has(option)) {
295
+ // show better usage message for flags
296
+ let flag = parsed.command.definition.flags.includes(option);
297
+ let usage = flag ? flagUsage(option) : option.name;
298
+ parsed.log.deprecated((`The '${usage}' ${flag ? 'option' : 'argument'} ` + `will be removed in ${until || 'a future release'}. ` + (recommend || '')).trim());
299
+ }
300
+ }
301
+
302
+ return name;
303
+ } // Normalizes a command runner into an object of command properties. Optionally derives nested
304
+ // commands from any provided definition commands method.
305
+
306
+
307
+ async function normalizeCommand(command, properties) {
308
+ var _ref;
309
+
310
+ // shallow copy command definition
311
+ let { ...definition
312
+ } = command.definition; // evaluate commands if needed
313
+
314
+ if (typeof definition.commands === 'function') {
315
+ definition.commands = await definition.commands();
316
+ } // create a shallow copy with additional properties
317
+
318
+
319
+ let normalized = { ...command,
320
+ ...properties,
321
+ definition
322
+ }; // inherit parent package information by default
323
+
324
+ normalized.packageInformation || (normalized.packageInformation = (_ref = (properties === null || properties === void 0 ? void 0 : properties.parent) || command.parent) === null || _ref === void 0 ? void 0 : _ref.packageInformation);
325
+ return normalized;
326
+ } // Parses and validates command-line arguments according to a command definition.
327
+
328
+
329
+ export async function parse(command, argv) {
330
+ // initial input and output
331
+ let input = argv.slice().filter(a => a != null);
332
+ let parsed = {
333
+ command: await normalizeCommand(command),
334
+ operators: new Map(),
335
+ log: logger('cli'),
336
+ flags: {},
337
+ args: {},
338
+ argv: []
339
+ }; // while there is input left, parse it
340
+
341
+ while (input.length) {
342
+ // parse and mutate input and output, looping if an option is found
343
+ if (await maybeParseCommand(input, parsed)) continue;
344
+ if (await maybeParseFlag(input, parsed)) continue;
345
+ if (await maybeParseArg(input, parsed)) continue;
346
+ await maybeParseUnknown(input, parsed);
347
+ } // help or version requested, skip remaining parse logic
348
+
349
+
350
+ if (parsed.help || parsed.version) {
351
+ return parsed;
352
+ } // validate and map defined flags
353
+
354
+
355
+ for (let flag of parsed.command.definition.flags) {
356
+ // collect last-minute env info
357
+ await maybeParseEnv(flag, parsed);
358
+
359
+ if (parsed.operators.has(flag)) {
360
+ // validate before setting the parsed attribute
361
+ validateInclusiveFlag(flag, parsed);
362
+ validateExclusiveFlag(flag, parsed);
363
+ let value = parsed.operators.get(flag);
364
+ if (flag.validate) await flag.validate(value, parsed);
365
+ parsed.flags[attributeName(flag, parsed)] = value;
366
+ } else if (flag.default != null) {
367
+ // a default value needs to be set, but not parsed
368
+ parsed.flags[attributeName(flag, parsed)] = flag.default;
369
+ }
370
+ } // validate and map defined args
371
+
372
+
373
+ for (let arg of parsed.command.definition.args ?? []) {
374
+ // collect last-minute env info
375
+ await maybeParseEnv(arg, parsed);
376
+
377
+ if (parsed.operators.has(arg)) {
378
+ var _arg$deprecated, _arg$deprecated$;
379
+
380
+ // validate before setting the parsed attribute
381
+ let value = parsed.operators.get(arg);
382
+ if (arg.validate) await arg.validate(value, parsed); // a deprecated arg might be mapped to a flag
383
+
384
+ if ((_arg$deprecated = arg.deprecated) !== null && _arg$deprecated !== void 0 && (_arg$deprecated$ = _arg$deprecated[1]) !== null && _arg$deprecated$ !== void 0 && _arg$deprecated$.startsWith('--') && findFlag(arg.deprecated[1], parsed)) {
385
+ parsed.flags[attributeName(arg, parsed)] = value;
386
+ } else {
387
+ parsed.args[attributeName(arg, parsed)] = value;
388
+ }
389
+ } else if (arg.required) {
390
+ // a required argument was not provided
391
+ parseError(`Missing required argument '${arg.name}'`);
392
+ } else if (arg.default != null) {
393
+ // a default value needs to be set, but not parsed
394
+ parsed.args[attributeName(arg, parsed)] = arg.default;
395
+ }
396
+ }
397
+
398
+ return parsed;
399
+ }
400
+ export default parse;
package/dist/utils.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from '@percy/config/utils';
2
+ export * from '@percy/core/utils';
package/package.json CHANGED
@@ -1,32 +1,41 @@
1
1
  {
2
2
  "name": "@percy/cli-command",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
- "main": "dist/index.js",
6
- "types": "types/index.d.ts",
7
- "files": [
8
- "dist",
9
- "types/index.d.ts"
10
- ],
11
- "scripts": {
12
- "build": "babel --root-mode upward src --out-dir dist",
13
- "lint": "eslint --ignore-path ../../.gitignore .",
14
- "test": "cross-env NODE_ENV=test mocha",
15
- "test:coverage": "nyc yarn test",
16
- "test:types": "tsd"
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/percy/cli",
8
+ "directory": "packages/cli-command"
17
9
  },
18
10
  "publishConfig": {
19
11
  "access": "public"
20
12
  },
21
- "mocha": {
22
- "require": "../../scripts/babel-register"
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "engines": {
17
+ "node": ">=14"
18
+ },
19
+ "bin": {
20
+ "percy-cli-readme": "./bin/readme"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "type": "module",
24
+ "exports": {
25
+ ".": "./dist/index.js",
26
+ "./utils": "./dist/utils.js",
27
+ "./test/helpers": "./test/helpers.js"
28
+ },
29
+ "scripts": {
30
+ "build": "node ../../scripts/build",
31
+ "lint": "eslint --ignore-path ../../.gitignore .",
32
+ "test": "node ../../scripts/test",
33
+ "test:coverage": "yarn test --coverage"
23
34
  },
24
35
  "dependencies": {
25
- "@oclif/command": "^1.5.19",
26
- "@oclif/config": "^1.14.0",
27
- "@oclif/plugin-help": "^3.1.0",
28
- "@percy/config": "^1.0.0-beta.8",
29
- "@percy/logger": "^1.0.0-beta.8"
36
+ "@percy/config": "1.0.1",
37
+ "@percy/core": "1.0.1",
38
+ "@percy/logger": "1.0.1"
30
39
  },
31
- "gitHead": "6015850e7c20c130d625fcb327b10d7513b35707"
40
+ "gitHead": "38917e6027299d6cd86008e2ccd005d90bbf89c0"
32
41
  }
package/types/index.d.ts DELETED
@@ -1,45 +0,0 @@
1
- // Minimum TypeScript Version: 3.8
2
- import Command from '@oclif/command';
3
- import * as Parser from '@oclif/parser';
4
-
5
- export namespace flags {
6
- interface PercyFlagBase { percyrc?: string }
7
- interface PercyOptionFlag<T> extends PercyFlagBase, Parser.flags.IOptionFlag<T> {}
8
- interface PercyBooleanFlag<T> extends PercyFlagBase, Parser.flags.IBooleanFlag<T> {}
9
- interface PercyDefinition<T> extends Parser.flags.Definition<T> {
10
- (options: { multiple: true } & Partial<PercyOptionFlag<T[]>>): PercyOptionFlag<T[]>;
11
- (options: ({ required: true } | { default: Parser.flags.Default<T> }) & Partial<PercyOptionFlag<T>>): PercyOptionFlag<T>;
12
- (options?: Partial<PercyOptionFlag<T>>): PercyOptionFlag<T | undefined>;
13
- }
14
-
15
- type PercyParseOptions<T> = { parse: PercyOptionFlag<T>['parse'] } & Partial<PercyOptionFlag<T>>;
16
- function build<T>(defaults: PercyParseOptions<T>): PercyDefinition<T>;
17
- function build(defaults: Partial<PercyOptionFlag<string>>): PercyDefinition<string>;
18
- function option<T>(options: PercyParseOptions<T>): PercyOptionFlag<T | undefined>;
19
- function boolean<T = boolean>(options?: Partial<PercyBooleanFlag<T>>): PercyBooleanFlag<T>;
20
- const integer: PercyDefinition<number>;
21
- const string: PercyDefinition<string>;
22
-
23
- const logging: {
24
- verbose: PercyOptionFlag<boolean>,
25
- quiet: PercyOptionFlag<boolean>,
26
- silent: PercyOptionFlag<boolean>
27
- };
28
-
29
- const discovery: {
30
- ['allowed-hostnames']: PercyOptionFlag<string[]>,
31
- ['network-idle-timeout']: PercyOptionFlag<number>,
32
- ['disable-asset-cache']: PercyOptionFlag<boolean>
33
- };
34
-
35
- const config: {
36
- config: PercyOptionFlag<string>
37
- };
38
- }
39
-
40
- export default abstract class PercyCommand extends Command {
41
- isPercyEnabled(): boolean
42
- percyrc(): { [x: string]: any }
43
- flags: { [x: string]: any }
44
- args: { [x: string]: any }
45
- }