@japa/runner 2.0.7 → 2.1.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.
package/README.md CHANGED
@@ -7,6 +7,11 @@ Japa is an API first testing framework. It focuses only on testing Node.js (back
7
7
 
8
8
  #### 💁 Please visit https://japa.dev for documentation
9
9
 
10
+ <br />
11
+ <hr>
12
+
13
+ ![](https://cdn.jsdelivr.net/gh/thetutlage/static/sponsorkit/sponsors.png)
14
+
10
15
  [github-actions-image]: https://img.shields.io/github/workflow/status/japa/runner/test?style=for-the-badge "github-actions"
11
16
 
12
17
  [github-actions-url]: https://github.com/japa/runner/actions/workflows/test.yml
package/build/index.d.ts CHANGED
@@ -1,22 +1,11 @@
1
- import { TestExecutor, ReporterContract } from '@japa/core';
2
1
  import { Test, TestContext, Group, Suite, Runner } from './src/Core';
2
+ import { TestExecutor, ReporterContract } from '@japa/core';
3
3
  import { Config, PluginFn, RunnerHooksHandler, RunnerHooksCleanupHandler } from './src/Contracts';
4
4
  export { Test, Config, Suite, Runner, Group, PluginFn, TestContext, ReporterContract, RunnerHooksHandler, RunnerHooksCleanupHandler, };
5
5
  /**
6
6
  * Configure the tests runner
7
7
  */
8
8
  export declare function configure(options: Config): void;
9
- /**
10
- * Add a new test
11
- */
12
- export declare function test(title: string, callback?: TestExecutor<TestContext, undefined>): Test<undefined>;
13
- export declare namespace test {
14
- var group: (title: string, callback: (group: Group) => void) => void;
15
- }
16
- /**
17
- * Run japa tests
18
- */
19
- export declare function run(): Promise<void>;
20
9
  /**
21
10
  * Process CLI arguments into configuration options. The following
22
11
  * command line arguments are processed.
@@ -28,5 +17,17 @@ export declare function run(): Promise<void>;
28
17
  * * --files=Specify files to match and run
29
18
  * * --force-exit=Enable/disable force exit
30
19
  * * --timeout=Define timeout for all the tests
20
+ * * -h, --help=Show help
31
21
  */
32
22
  export declare function processCliArgs(argv: string[]): Partial<Config>;
23
+ /**
24
+ * Run japa tests
25
+ */
26
+ export declare function run(): Promise<void>;
27
+ /**
28
+ * Add a new test
29
+ */
30
+ export declare function test(title: string, callback?: TestExecutor<TestContext, undefined>): Test<undefined>;
31
+ export declare namespace test {
32
+ var group: (title: string, callback: (group: Group) => void) => void;
33
+ }
package/build/index.js CHANGED
@@ -11,21 +11,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  return (mod && mod.__esModule) ? mod : { "default": mod };
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.processCliArgs = exports.run = exports.test = exports.configure = exports.TestContext = exports.Group = exports.Runner = exports.Suite = exports.Test = void 0;
14
+ exports.test = exports.run = exports.processCliArgs = exports.configure = exports.TestContext = exports.Group = exports.Runner = exports.Suite = exports.Test = void 0;
15
15
  const getopts_1 = __importDefault(require("getopts"));
16
16
  const path_1 = require("path");
17
17
  const fast_glob_1 = __importDefault(require("fast-glob"));
18
18
  const inclusion_1 = __importDefault(require("inclusion"));
19
19
  const url_1 = require("url");
20
20
  const hooks_1 = require("@poppinss/hooks");
21
+ const package_json_1 = require("./package.json");
22
+ const cliui_1 = require("@poppinss/cliui");
21
23
  const errors_printer_1 = require("@japa/errors-printer");
22
- const core_1 = require("@japa/core");
23
24
  const Core_1 = require("./src/Core");
24
25
  Object.defineProperty(exports, "Test", { enumerable: true, get: function () { return Core_1.Test; } });
25
26
  Object.defineProperty(exports, "TestContext", { enumerable: true, get: function () { return Core_1.TestContext; } });
26
27
  Object.defineProperty(exports, "Group", { enumerable: true, get: function () { return Core_1.Group; } });
27
28
  Object.defineProperty(exports, "Suite", { enumerable: true, get: function () { return Core_1.Suite; } });
28
29
  Object.defineProperty(exports, "Runner", { enumerable: true, get: function () { return Core_1.Runner; } });
30
+ const core_1 = require("@japa/core");
29
31
  /**
30
32
  * Filtering layers allowed by the refiner
31
33
  */
@@ -51,7 +53,7 @@ const emitter = new core_1.Emitter();
51
53
  /**
52
54
  * Active suite for tests
53
55
  */
54
- let activeSuite = new Core_1.Suite('default', emitter);
56
+ let activeSuite;
55
57
  /**
56
58
  * Currently active group
57
59
  */
@@ -85,25 +87,6 @@ function validateSuitesFilter() {
85
87
  throw new Error(`Unrecognized suite "${invalidSuites[0]}". Make sure to define it in the config first`);
86
88
  }
87
89
  }
88
- /**
89
- * End tests. We wait for the "beforeExit" event when
90
- * forceExit is not set to true
91
- */
92
- async function endTests(runner) {
93
- if (runnerOptions.forceExit) {
94
- await runner.end();
95
- }
96
- else {
97
- return new Promise((resolve) => {
98
- async function beforeExit() {
99
- process.removeListener('beforeExit', beforeExit);
100
- await runner.end();
101
- resolve();
102
- }
103
- process.on('beforeExit', beforeExit);
104
- });
105
- }
106
- }
107
90
  /**
108
91
  * Process command line argument into a string value
109
92
  */
@@ -118,7 +101,10 @@ function processAsString(argv, flagName, onMatch) {
118
101
  * The ending of the file is matched
119
102
  */
120
103
  function isFileAllowed(filePath, filters) {
121
- return !!filters.find((matcher) => {
104
+ if (!filters.files || !filters.files.length) {
105
+ return true;
106
+ }
107
+ return !!filters.files.find((matcher) => {
122
108
  if (filePath.endsWith(matcher)) {
123
109
  return true;
124
110
  }
@@ -127,13 +113,84 @@ function isFileAllowed(filePath, filters) {
127
113
  }
128
114
  /**
129
115
  * Returns "true" when no filters are applied or the name is part
130
- * of the applied filter
116
+ * of the applied filter.
131
117
  */
132
- function isSuiteAllowed(name, filters) {
133
- if (!filters || !filters.length) {
118
+ function isSuiteAllowed(suite, filters) {
119
+ if (!filters.suites || !filters.suites.length) {
134
120
  return true;
135
121
  }
136
- return filters.includes(name);
122
+ return filters.suites.includes(suite.name);
123
+ }
124
+ /**
125
+ * Collect files using the files collector function or by processing
126
+ * the glob pattern.
127
+ *
128
+ * The return value is further filtered against the `--files` filter.
129
+ */
130
+ async function collectFiles(files) {
131
+ if (Array.isArray(files) || typeof files === 'string') {
132
+ const collectedFiles = await (0, fast_glob_1.default)(files, {
133
+ absolute: true,
134
+ onlyFiles: true,
135
+ cwd: runnerOptions.cwd,
136
+ });
137
+ return collectedFiles.filter((file) => isFileAllowed(file, runnerOptions.filters));
138
+ }
139
+ else if (typeof files === 'function') {
140
+ const collectedFiles = await files();
141
+ return collectedFiles.filter((file) => isFileAllowed(file, runnerOptions.filters));
142
+ }
143
+ throw new Error('Invalid value for "files" property. Expected a string, array or a function');
144
+ }
145
+ /**
146
+ * Import test files using the configured importer.
147
+ */
148
+ async function importFiles(files) {
149
+ for (let file of files) {
150
+ recentlyImportedFile = file;
151
+ await runnerOptions.importer(file);
152
+ }
153
+ }
154
+ /**
155
+ * End tests. We wait for the "beforeExit" event when
156
+ * forceExit is not set to true
157
+ */
158
+ async function endTests(runner) {
159
+ if (runnerOptions.forceExit) {
160
+ await runner.end();
161
+ }
162
+ else {
163
+ return new Promise((resolve) => {
164
+ async function beforeExit() {
165
+ process.removeListener('beforeExit', beforeExit);
166
+ await runner.end();
167
+ resolve();
168
+ }
169
+ process.on('beforeExit', beforeExit);
170
+ });
171
+ }
172
+ }
173
+ /**
174
+ * Show help output in stdout.
175
+ */
176
+ function showHelp() {
177
+ const green = cliui_1.logger.colors.green.bind(cliui_1.logger.colors);
178
+ const grey = cliui_1.logger.colors.grey.bind(cliui_1.logger.colors);
179
+ console.log(`@japa/runner v${package_json_1.version}
180
+
181
+ Options:
182
+ ${green('--tests')} ${grey('Specify test titles')}
183
+ ${green('--tags')} ${grey('Specify test tags')}
184
+ ${green('--groups')} ${grey('Specify group titles')}
185
+ ${green('--ignore-tags')} ${grey('Specify negated tags')}
186
+ ${green('--files')} ${grey('Specify files to match and run')}
187
+ ${green('--force-exit')} ${grey('Enable/disable force exit')}
188
+ ${green('--timeout')} ${grey('Define timeout for all the tests')}
189
+ ${green('-h, --help')} ${grey('Display this message')}
190
+
191
+ Examples:
192
+ ${grey('$ node bin/test.js --tags="@github"')}
193
+ ${grey('$ node bin/test.js --files="example.spec.js" --force-exit')}`);
137
194
  }
138
195
  /**
139
196
  * Configure the tests runner
@@ -158,92 +215,70 @@ function configure(options) {
158
215
  }
159
216
  exports.configure = configure;
160
217
  /**
161
- * Add a new test
218
+ * Process CLI arguments into configuration options. The following
219
+ * command line arguments are processed.
220
+ *
221
+ * * --tests=Specify test titles
222
+ * * --tags=Specify test tags
223
+ * * --groups=Specify group titles
224
+ * * --ignore-tags=Specify negated tags
225
+ * * --files=Specify files to match and run
226
+ * * --force-exit=Enable/disable force exit
227
+ * * --timeout=Define timeout for all the tests
228
+ * * -h, --help=Show help
162
229
  */
163
- function test(title, callback) {
164
- ensureIsConfigured('Cannot add test without configuring the test runner');
165
- const testInstance = new Core_1.Test(title, getContext, emitter, runnerOptions.refiner);
166
- /**
167
- * Set filename
168
- */
169
- testInstance.options.meta.fileName = recentlyImportedFile;
170
- /**
171
- * Define timeout on the test when exists globally
172
- */
173
- if (globalTimeout !== undefined) {
174
- testInstance.timeout(globalTimeout);
175
- }
230
+ function processCliArgs(argv) {
231
+ const parsed = (0, getopts_1.default)(argv, {
232
+ string: ['tests', 'tags', 'groups', 'ignoreTags', 'files', 'timeout'],
233
+ boolean: ['forceExit', 'help'],
234
+ alias: {
235
+ ignoreTags: 'ignore-tags',
236
+ forceExit: 'force-exit',
237
+ help: 'h',
238
+ },
239
+ });
240
+ const config = {
241
+ filters: {},
242
+ };
243
+ processAsString(parsed, 'tags', (tags) => (config.filters.tags = tags));
244
+ processAsString(parsed, 'ignoreTags', (tags) => {
245
+ config.filters.tags = config.filters.tags || [];
246
+ tags.forEach((tag) => config.filters.tags.push(`!${tag}`));
247
+ });
248
+ processAsString(parsed, 'groups', (groups) => (config.filters.groups = groups));
249
+ processAsString(parsed, 'tests', (tests) => (config.filters.tests = tests));
250
+ processAsString(parsed, 'files', (files) => (config.filters.files = files));
176
251
  /**
177
- * Define test executor function
252
+ * Show help
178
253
  */
179
- if (callback) {
180
- testInstance.run(callback);
254
+ if (parsed.help) {
255
+ showHelp();
256
+ process.exit(0);
181
257
  }
182
258
  /**
183
- * Add test to the group or suite
259
+ * Get suites
184
260
  */
185
- if (activeGroup) {
186
- activeGroup.add(testInstance);
187
- }
188
- else {
189
- activeSuite.add(testInstance);
261
+ if (parsed._.length) {
262
+ processAsString({ suites: parsed._ }, 'suites', (suites) => (config.filters.suites = suites));
190
263
  }
191
- return testInstance;
192
- }
193
- exports.test = test;
194
- /**
195
- * Define test group
196
- */
197
- test.group = function (title, callback) {
198
- ensureIsConfigured('Cannot add test group without configuring the test runner');
199
264
  /**
200
- * Disallow nested groups
265
+ * Get timeout
201
266
  */
202
- if (activeGroup) {
203
- throw new Error('Cannot create nested test groups');
267
+ if (parsed.timeout) {
268
+ const value = Number(parsed.timeout);
269
+ if (!isNaN(value)) {
270
+ config.timeout = value;
271
+ }
204
272
  }
205
- activeGroup = new Core_1.Group(title, emitter, runnerOptions.refiner);
206
273
  /**
207
- * Set filename
208
- */
209
- activeGroup.options.meta.fileName = recentlyImportedFile;
210
- callback(activeGroup);
211
- /**
212
- * Add group to the default suite
274
+ * Get forceExit
213
275
  */
214
- activeSuite.add(activeGroup);
215
- activeGroup = undefined;
216
- };
217
- /**
218
- * Collect files using the files collector function or by processing
219
- * the glob pattern
220
- */
221
- async function collectFiles(files) {
222
- if (Array.isArray(files) || typeof files === 'string') {
223
- return await (0, fast_glob_1.default)(files, { absolute: true, onlyFiles: true, cwd: runnerOptions.cwd });
224
- }
225
- else if (typeof files === 'function') {
226
- return await files();
227
- }
228
- throw new Error('Invalid value for "files" property. Expected a string, array or a function');
229
- }
230
- /**
231
- * Import test files using the configured importer. Also
232
- * filter files using the file filter. (if mentioned).
233
- */
234
- async function importFiles(files) {
235
- for (let file of files) {
236
- recentlyImportedFile = file;
237
- if (runnerOptions.filters.files && runnerOptions.filters.files.length) {
238
- if (isFileAllowed(file, runnerOptions.filters.files)) {
239
- await runnerOptions.importer(file);
240
- }
241
- }
242
- else {
243
- await runnerOptions.importer(file);
244
- }
276
+ if (parsed.forceExit) {
277
+ config.forceExit = true;
245
278
  }
279
+ return config;
246
280
  }
281
+ exports.processCliArgs = processCliArgs;
247
282
  /**
248
283
  * Run japa tests
249
284
  */
@@ -290,10 +325,19 @@ async function run() {
290
325
  * as part of the default suite
291
326
  */
292
327
  if ('files' in runnerOptions && runnerOptions.files.length) {
328
+ /**
329
+ * Create a default suite for files with no suite
330
+ */
293
331
  globalTimeout = runnerOptions.timeout;
294
332
  const files = await collectFiles(runnerOptions.files);
295
- runner.add(activeSuite);
296
- await importFiles(files);
333
+ /**
334
+ * Create and register suite when files are collected.
335
+ */
336
+ if (files.length) {
337
+ activeSuite = new Core_1.Suite('default', emitter, runnerOptions.refiner);
338
+ runner.add(activeSuite);
339
+ await importFiles(files);
340
+ }
297
341
  }
298
342
  /**
299
343
  * Step 5: Entertain suites property and import test files
@@ -301,20 +345,26 @@ async function run() {
301
345
  */
302
346
  if ('suites' in runnerOptions) {
303
347
  for (let suite of runnerOptions.suites) {
304
- if (isSuiteAllowed(suite.name, runnerOptions.filters.suites)) {
348
+ if (isSuiteAllowed(suite, runnerOptions.filters)) {
305
349
  if (suite.timeout !== undefined) {
306
350
  globalTimeout = suite.timeout;
307
351
  }
308
352
  else {
309
353
  globalTimeout = runnerOptions.timeout;
310
354
  }
311
- activeSuite = new Core_1.Suite(suite.name, emitter);
312
- if (typeof suite.configure === 'function') {
313
- suite.configure(activeSuite);
314
- }
315
355
  const files = await collectFiles(suite.files);
316
- runner.add(activeSuite);
317
- await importFiles(files);
356
+ /**
357
+ * Only register the suite and import files when the suite
358
+ * files glob + filter has returned one or more files.
359
+ */
360
+ if (files.length) {
361
+ activeSuite = new Core_1.Suite(suite.name, emitter, runnerOptions.refiner);
362
+ if (typeof suite.configure === 'function') {
363
+ suite.configure(activeSuite);
364
+ }
365
+ runner.add(activeSuite);
366
+ await importFiles(files);
367
+ }
318
368
  }
319
369
  }
320
370
  }
@@ -371,58 +421,59 @@ async function run() {
371
421
  }
372
422
  exports.run = run;
373
423
  /**
374
- * Process CLI arguments into configuration options. The following
375
- * command line arguments are processed.
376
- *
377
- * * --tests=Specify test titles
378
- * * --tags=Specify test tags
379
- * * --groups=Specify group titles
380
- * * --ignore-tags=Specify negated tags
381
- * * --files=Specify files to match and run
382
- * * --force-exit=Enable/disable force exit
383
- * * --timeout=Define timeout for all the tests
424
+ * Add a new test
384
425
  */
385
- function processCliArgs(argv) {
386
- const parsed = (0, getopts_1.default)(argv, {
387
- string: ['tests', 'tags', 'groups', 'ignoreTags', 'files', 'timeout'],
388
- boolean: ['forceExit'],
389
- alias: {
390
- ignoreTags: 'ignore-tags',
391
- forceExit: 'force-exit',
392
- },
393
- });
394
- const config = {
395
- filters: {},
396
- };
397
- processAsString(parsed, 'tags', (tags) => (config.filters.tags = tags));
398
- processAsString(parsed, 'ignoreTags', (tags) => {
399
- config.filters.tags = config.filters.tags || [];
400
- tags.forEach((tag) => config.filters.tags.push(`!${tag}`));
401
- });
402
- processAsString(parsed, 'groups', (groups) => (config.filters.groups = groups));
403
- processAsString(parsed, 'tests', (tests) => (config.filters.tests = tests));
404
- processAsString(parsed, 'files', (files) => (config.filters.files = files));
426
+ function test(title, callback) {
427
+ ensureIsConfigured('Cannot add test without configuring the test runner');
428
+ const testInstance = new Core_1.Test(title, getContext, emitter, runnerOptions.refiner, activeGroup);
405
429
  /**
406
- * Get suites
430
+ * Set filename
407
431
  */
408
- if (parsed._.length) {
409
- processAsString({ suites: parsed._ }, 'suites', (suites) => (config.filters.suites = suites));
432
+ testInstance.options.meta.fileName = recentlyImportedFile;
433
+ /**
434
+ * Define timeout on the test when exists globally
435
+ */
436
+ if (globalTimeout !== undefined) {
437
+ testInstance.timeout(globalTimeout);
410
438
  }
411
439
  /**
412
- * Get timeout
440
+ * Define test executor function
413
441
  */
414
- if (parsed.timeout) {
415
- const value = Number(parsed.timeout);
416
- if (!isNaN(value)) {
417
- config.timeout = value;
418
- }
442
+ if (callback) {
443
+ testInstance.run(callback);
419
444
  }
420
445
  /**
421
- * Get forceExit
446
+ * Add test to the group or suite
422
447
  */
423
- if (parsed.forceExit) {
424
- config.forceExit = true;
448
+ if (activeGroup) {
449
+ activeGroup.add(testInstance);
425
450
  }
426
- return config;
451
+ else {
452
+ activeSuite.add(testInstance);
453
+ }
454
+ return testInstance;
427
455
  }
428
- exports.processCliArgs = processCliArgs;
456
+ exports.test = test;
457
+ /**
458
+ * Define test group
459
+ */
460
+ test.group = function (title, callback) {
461
+ ensureIsConfigured('Cannot add test group without configuring the test runner');
462
+ /**
463
+ * Disallow nested groups
464
+ */
465
+ if (activeGroup) {
466
+ throw new Error('Cannot create nested test groups');
467
+ }
468
+ activeGroup = new Core_1.Group(title, emitter, runnerOptions.refiner);
469
+ /**
470
+ * Set filename
471
+ */
472
+ activeGroup.options.meta.fileName = recentlyImportedFile;
473
+ /**
474
+ * Add group to the default suite
475
+ */
476
+ activeSuite.add(activeGroup);
477
+ callback(activeGroup);
478
+ activeGroup = undefined;
479
+ };
@@ -44,16 +44,24 @@ export declare type BaseConfig = {
44
44
  refiner?: Refiner;
45
45
  forceExit?: boolean;
46
46
  } & Partial<RunnerHooks>;
47
+ /**
48
+ * Type for the "config.files" property
49
+ */
50
+ export declare type ConfigFiles = string[] | (() => string[] | Promise<string[]>);
51
+ /**
52
+ * Type for the "config.suite" property
53
+ */
54
+ export declare type ConfigSuite = {
55
+ name: string;
56
+ files: string | string[] | (() => string[] | Promise<string[]>);
57
+ configure?: (suite: Suite) => void;
58
+ timeout?: number;
59
+ };
47
60
  /**
48
61
  * Configuration options
49
62
  */
50
63
  export declare type Config = BaseConfig & ({
51
- files: string[] | (() => string[] | Promise<string[]>);
64
+ files: ConfigFiles;
52
65
  } | {
53
- suites: {
54
- name: string;
55
- files: string | string[] | (() => string[] | Promise<string[]>);
56
- configure?: (suite: Suite) => void;
57
- timeout?: number;
58
- }[];
66
+ suites: ConfigSuite[];
59
67
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@japa/runner",
3
- "version": "2.0.7",
3
+ "version": "2.1.0",
4
4
  "description": "Runner for Japa testing framework",
5
5
  "main": "build/index.js",
6
6
  "files": [
@@ -15,7 +15,7 @@
15
15
  "mrm": "mrm --preset=@adonisjs/mrm-preset",
16
16
  "pretest": "npm run lint",
17
17
  "test": "node .bin/test.js",
18
- "clean": "del build",
18
+ "clean": "del-cli build",
19
19
  "compile": "npm run lint && npm run clean && tsc",
20
20
  "build": "npm run compile",
21
21
  "prepublishOnly": "npm run build",
@@ -35,22 +35,22 @@
35
35
  "license": "MIT",
36
36
  "devDependencies": {
37
37
  "@adonisjs/mrm-preset": "^5.0.3",
38
- "@adonisjs/require-ts": "^2.0.11",
39
- "@types/node": "^17.0.23",
40
- "commitizen": "^4.2.4",
38
+ "@adonisjs/require-ts": "^2.0.12",
39
+ "@types/node": "^18.7.14",
40
+ "commitizen": "^4.2.5",
41
41
  "cz-conventional-changelog": "^3.3.0",
42
- "del-cli": "^4.0.1",
43
- "eslint": "^8.12.0",
42
+ "del-cli": "^5.0.0",
43
+ "eslint": "^8.23.0",
44
44
  "eslint-config-prettier": "^8.5.0",
45
45
  "eslint-plugin-adonis": "^2.1.0",
46
- "eslint-plugin-prettier": "^4.0.0",
46
+ "eslint-plugin-prettier": "^4.2.1",
47
47
  "github-label-sync": "^2.2.0",
48
- "husky": "^7.0.4",
48
+ "husky": "^8.0.1",
49
49
  "japa": "^4.0.0",
50
- "mrm": "^4.0.0",
51
- "np": "^7.6.1",
52
- "prettier": "^2.6.2",
53
- "typescript": "^4.6.3"
50
+ "mrm": "^4.1.0",
51
+ "np": "^7.6.2",
52
+ "prettier": "^2.7.1",
53
+ "typescript": "^4.8.2"
54
54
  },
55
55
  "mrmConfig": {
56
56
  "core": false,
@@ -105,8 +105,9 @@
105
105
  "anyBranch": false
106
106
  },
107
107
  "dependencies": {
108
- "@japa/core": "^6.0.4",
109
- "@japa/errors-printer": "^1.3.7",
108
+ "@japa/core": "^7.0.0",
109
+ "@japa/errors-printer": "^1.3.10",
110
+ "@poppinss/cliui": "^3.0.2",
110
111
  "@poppinss/hooks": "^6.0.2-0",
111
112
  "fast-glob": "^3.2.11",
112
113
  "getopts": "^2.3.0",