jackspeak 4.0.0 → 4.0.2

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/esm/index.js CHANGED
@@ -1,19 +1,54 @@
1
- import { inspect } from 'node:util';
2
- import { parseArgs } from './parse-args.js';
1
+ import { inspect, parseArgs, } from 'node:util';
3
2
  // it's a tiny API, just cast it inline, it's fine
4
3
  //@ts-ignore
5
4
  import cliui from '@isaacs/cliui';
6
5
  import { basename } from 'node:path';
7
- const width = Math.min((process && process.stdout && process.stdout.columns) || 80, 80);
6
+ export const isConfigType = (t) => typeof t === 'string' &&
7
+ (t === 'string' || t === 'number' || t === 'boolean');
8
+ const isValidValue = (v, type, multi) => {
9
+ if (multi) {
10
+ if (!Array.isArray(v))
11
+ return false;
12
+ return !v.some((v) => !isValidValue(v, type, false));
13
+ }
14
+ if (Array.isArray(v))
15
+ return false;
16
+ return typeof v === type;
17
+ };
18
+ const isValidOption = (v, vo) => !!vo &&
19
+ (Array.isArray(v) ? v.every(x => isValidOption(x, vo)) : vo.includes(v));
20
+ /**
21
+ * Determine whether an unknown object is a {@link ConfigOption} based only
22
+ * on its `type` and `multiple` property
23
+ */
24
+ export const isConfigOptionOfType = (o, type, multi) => !!o &&
25
+ typeof o === 'object' &&
26
+ isConfigType(o.type) &&
27
+ o.type === type &&
28
+ !!o.multiple === multi;
29
+ /**
30
+ * Determine whether an unknown object is a {@link ConfigOption} based on
31
+ * it having all valid properties
32
+ */
33
+ export const isConfigOption = (o, type, multi) => isConfigOptionOfType(o, type, multi) &&
34
+ undefOrType(o.short, 'string') &&
35
+ undefOrType(o.description, 'string') &&
36
+ undefOrType(o.hint, 'string') &&
37
+ undefOrType(o.validate, 'function') &&
38
+ (o.type === 'boolean' ?
39
+ o.validOptions === undefined
40
+ : undefOrTypeArray(o.validOptions, o.type)) &&
41
+ (o.default === undefined || isValidValue(o.default, type, multi));
42
+ const isHeading = (r) => r.type === 'heading';
43
+ const isDescription = (r) => r.type === 'description';
44
+ const width = Math.min(process?.stdout?.columns ?? 80, 80);
8
45
  // indentation spaces from heading level
9
46
  const indent = (n) => (n - 1) * 2;
10
- const toEnvKey = (pref, key) => {
11
- return [pref, key.replace(/[^a-zA-Z0-9]+/g, ' ')]
12
- .join(' ')
13
- .trim()
14
- .toUpperCase()
15
- .replace(/ /g, '_');
16
- };
47
+ const toEnvKey = (pref, key) => [pref, key.replace(/[^a-zA-Z0-9]+/g, ' ')]
48
+ .join(' ')
49
+ .trim()
50
+ .toUpperCase()
51
+ .replace(/ /g, '_');
17
52
  const toEnvVal = (value, delim = '\n') => {
18
53
  const str = typeof value === 'string' ? value
19
54
  : typeof value === 'boolean' ?
@@ -35,254 +70,144 @@ const fromEnvVal = (env, type, multiple, delim = '\n') => (multiple ?
35
70
  : type === 'string' ? env
36
71
  : type === 'boolean' ? env === '1'
37
72
  : +env.trim());
38
- export const isConfigType = (t) => typeof t === 'string' &&
39
- (t === 'string' || t === 'number' || t === 'boolean');
40
73
  const undefOrType = (v, t) => v === undefined || typeof v === t;
41
74
  const undefOrTypeArray = (v, t) => v === undefined || (Array.isArray(v) && v.every(x => typeof x === t));
42
- const isValidOption = (v, vo) => Array.isArray(v) ? v.every(x => isValidOption(x, vo)) : vo.includes(v);
43
75
  // print the value type, for error message reporting
44
76
  const valueType = (v) => typeof v === 'string' ? 'string'
45
77
  : typeof v === 'boolean' ? 'boolean'
46
78
  : typeof v === 'number' ? 'number'
47
79
  : Array.isArray(v) ?
48
- joinTypes([...new Set(v.map(v => valueType(v)))]) + '[]'
80
+ `${joinTypes([...new Set(v.map(v => valueType(v)))])}[]`
49
81
  : `${v.type}${v.multiple ? '[]' : ''}`;
50
82
  const joinTypes = (types) => types.length === 1 && typeof types[0] === 'string' ?
51
83
  types[0]
52
84
  : `(${types.join('|')})`;
53
- const isValidValue = (v, type, multi) => {
54
- if (multi) {
55
- if (!Array.isArray(v))
56
- return false;
57
- return !v.some((v) => !isValidValue(v, type, false));
58
- }
59
- if (Array.isArray(v))
60
- return false;
61
- return typeof v === type;
62
- };
63
- export const isConfigOption = (o, type, multi) => !!o &&
64
- typeof o === 'object' &&
65
- isConfigType(o.type) &&
66
- o.type === type &&
67
- undefOrType(o.short, 'string') &&
68
- undefOrType(o.description, 'string') &&
69
- undefOrType(o.hint, 'string') &&
70
- undefOrType(o.validate, 'function') &&
71
- (o.type === 'boolean' ?
72
- o.validOptions === undefined
73
- : undefOrTypeArray(o.validOptions, o.type)) &&
74
- (o.default === undefined || isValidValue(o.default, type, multi)) &&
75
- !!o.multiple === multi;
76
- function num(o = {}) {
77
- const { default: def, validate: val, validOptions, ...rest } = o;
78
- if (def !== undefined && !isValidValue(def, 'number', false)) {
79
- throw new TypeError('invalid default value', {
80
- cause: {
81
- found: def,
82
- wanted: 'number',
83
- },
84
- });
85
- }
86
- if (!undefOrTypeArray(validOptions, 'number')) {
87
- throw new TypeError('invalid validOptions', {
88
- cause: {
89
- found: validOptions,
90
- wanted: 'number[]',
91
- },
92
- });
93
- }
94
- const validate = val ?
95
- val
96
- : undefined;
97
- return {
98
- ...rest,
99
- default: def,
100
- validate,
101
- validOptions,
102
- type: 'number',
103
- multiple: false,
104
- };
105
- }
106
- function numList(o = {}) {
107
- const { default: def, validate: val, validOptions, ...rest } = o;
108
- if (def !== undefined && !isValidValue(def, 'number', true)) {
109
- throw new TypeError('invalid default value', {
110
- cause: {
111
- found: def,
112
- wanted: 'number[]',
113
- },
114
- });
85
+ const validateFieldMeta = (field, fieldMeta) => {
86
+ if (fieldMeta) {
87
+ if (field.type !== undefined && field.type !== fieldMeta.type) {
88
+ throw new TypeError(`invalid type`, {
89
+ cause: {
90
+ found: field.type,
91
+ wanted: [fieldMeta.type, undefined],
92
+ },
93
+ });
94
+ }
95
+ if (field.multiple !== undefined &&
96
+ !!field.multiple !== fieldMeta.multiple) {
97
+ throw new TypeError(`invalid multiple`, {
98
+ cause: {
99
+ found: field.multiple,
100
+ wanted: [fieldMeta.multiple, undefined],
101
+ },
102
+ });
103
+ }
104
+ return fieldMeta;
115
105
  }
116
- if (!undefOrTypeArray(validOptions, 'number')) {
117
- throw new TypeError('invalid validOptions', {
106
+ if (!isConfigType(field.type)) {
107
+ throw new TypeError(`invalid type`, {
118
108
  cause: {
119
- found: validOptions,
120
- wanted: 'number[]',
109
+ found: field.type,
110
+ wanted: ['string', 'number', 'boolean'],
121
111
  },
122
112
  });
123
113
  }
124
- const validate = val ?
125
- val
126
- : undefined;
127
114
  return {
128
- ...rest,
129
- default: def,
130
- validate,
131
- validOptions,
132
- type: 'number',
133
- multiple: true,
115
+ type: field.type,
116
+ multiple: !!field.multiple,
134
117
  };
135
- }
136
- function opt(o = {}) {
137
- const { default: def, validate: val, validOptions, ...rest } = o;
138
- if (def !== undefined && !isValidValue(def, 'string', false)) {
139
- throw new TypeError('invalid default value', {
140
- cause: {
141
- found: def,
142
- wanted: 'string',
143
- },
144
- });
145
- }
146
- if (!undefOrTypeArray(validOptions, 'string')) {
147
- throw new TypeError('invalid validOptions', {
148
- cause: {
149
- found: validOptions,
150
- wanted: 'string[]',
151
- },
152
- });
153
- }
154
- const validate = val ?
155
- val
156
- : undefined;
157
- return {
158
- ...rest,
159
- default: def,
160
- validate,
161
- validOptions,
162
- type: 'string',
163
- multiple: false,
118
+ };
119
+ const validateField = (o, type, multiple) => {
120
+ const validateValidOptions = (def, validOptions) => {
121
+ if (!undefOrTypeArray(validOptions, type)) {
122
+ throw new TypeError('invalid validOptions', {
123
+ cause: {
124
+ found: validOptions,
125
+ wanted: valueType({ type, multiple: true }),
126
+ },
127
+ });
128
+ }
129
+ if (def !== undefined && validOptions !== undefined) {
130
+ const valid = Array.isArray(def) ?
131
+ def.every(v => validOptions.includes(v))
132
+ : validOptions.includes(def);
133
+ if (!valid) {
134
+ throw new TypeError('invalid default value not in validOptions', {
135
+ cause: {
136
+ found: def,
137
+ wanted: validOptions,
138
+ },
139
+ });
140
+ }
141
+ }
164
142
  };
165
- }
166
- function optList(o = {}) {
167
- const { default: def, validate: val, validOptions, ...rest } = o;
168
- if (def !== undefined && !isValidValue(def, 'string', true)) {
143
+ if (o.default !== undefined &&
144
+ !isValidValue(o.default, type, multiple)) {
169
145
  throw new TypeError('invalid default value', {
170
146
  cause: {
171
- found: def,
172
- wanted: 'string[]',
147
+ found: o.default,
148
+ wanted: valueType({ type, multiple }),
173
149
  },
174
150
  });
175
151
  }
176
- if (!undefOrTypeArray(validOptions, 'string')) {
177
- throw new TypeError('invalid validOptions', {
178
- cause: {
179
- found: validOptions,
180
- wanted: 'string[]',
181
- },
182
- });
152
+ if (isConfigOptionOfType(o, 'number', false) ||
153
+ isConfigOptionOfType(o, 'number', true)) {
154
+ validateValidOptions(o.default, o.validOptions);
183
155
  }
184
- const validate = val ?
185
- val
186
- : undefined;
187
- return {
188
- ...rest,
189
- default: def,
190
- validate,
191
- validOptions,
192
- type: 'string',
193
- multiple: true,
194
- };
195
- }
196
- function flag(o = {}) {
197
- const { hint, default: def, validate: val, ...rest } = o;
198
- delete rest.validOptions;
199
- if (def !== undefined && !isValidValue(def, 'boolean', false)) {
200
- throw new TypeError('invalid default value');
201
- }
202
- const validate = val ?
203
- val
204
- : undefined;
205
- if (hint !== undefined) {
206
- throw new TypeError('cannot provide hint for flag');
156
+ else if (isConfigOptionOfType(o, 'string', false) ||
157
+ isConfigOptionOfType(o, 'string', true)) {
158
+ validateValidOptions(o.default, o.validOptions);
207
159
  }
208
- return {
209
- ...rest,
210
- default: def,
211
- validate,
212
- type: 'boolean',
213
- multiple: false,
214
- };
215
- }
216
- function flagList(o = {}) {
217
- const { hint, default: def, validate: val, ...rest } = o;
218
- delete rest.validOptions;
219
- if (def !== undefined && !isValidValue(def, 'boolean', true)) {
220
- throw new TypeError('invalid default value');
221
- }
222
- const validate = val ?
223
- val
224
- : undefined;
225
- if (hint !== undefined) {
226
- throw new TypeError('cannot provide hint for flag list');
160
+ else if (isConfigOptionOfType(o, 'boolean', false) ||
161
+ isConfigOptionOfType(o, 'boolean', true)) {
162
+ if (o.hint !== undefined) {
163
+ throw new TypeError('cannot provide hint for flag');
164
+ }
165
+ if (o.validOptions !== undefined) {
166
+ throw new TypeError('cannot provide validOptions for flag');
167
+ }
227
168
  }
228
- return {
229
- ...rest,
230
- default: def,
231
- validate,
232
- type: 'boolean',
233
- multiple: true,
234
- };
235
- }
169
+ return o;
170
+ };
236
171
  const toParseArgsOptionsConfig = (options) => {
237
- const c = {};
238
- for (const longOption in options) {
239
- const config = options[longOption];
240
- /* c8 ignore start */
241
- if (!config) {
242
- throw new Error('config must be an object: ' + longOption);
243
- }
244
- /* c8 ignore start */
245
- if (isConfigOption(config, 'number', true)) {
246
- c[longOption] = {
247
- type: 'string',
248
- multiple: true,
249
- default: config.default?.map(c => String(c)),
250
- };
251
- }
252
- else if (isConfigOption(config, 'number', false)) {
253
- c[longOption] = {
254
- type: 'string',
255
- multiple: false,
256
- default: config.default === undefined ?
257
- undefined
258
- : String(config.default),
259
- };
172
+ return Object.entries(options).reduce((acc, [longOption, o]) => {
173
+ const p = {
174
+ type: 'string',
175
+ multiple: !!o.multiple,
176
+ ...(typeof o.short === 'string' ? { short: o.short } : undefined),
177
+ };
178
+ const setNoBool = () => {
179
+ if (!longOption.startsWith('no-') && !options[`no-${longOption}`]) {
180
+ acc[`no-${longOption}`] = {
181
+ type: 'boolean',
182
+ multiple: !!o.multiple,
183
+ };
184
+ }
185
+ };
186
+ const setDefault = (def, fn) => {
187
+ if (def !== undefined) {
188
+ p.default = fn(def);
189
+ }
190
+ };
191
+ if (isConfigOption(o, 'number', false)) {
192
+ setDefault(o.default, String);
260
193
  }
261
- else {
262
- const conf = config;
263
- c[longOption] = {
264
- type: conf.type,
265
- multiple: !!conf.multiple,
266
- default: conf.default,
267
- };
268
- }
269
- const clo = c[longOption];
270
- if (typeof config.short === 'string') {
271
- clo.short = config.short;
272
- }
273
- if (config.type === 'boolean' &&
274
- !longOption.startsWith('no-') &&
275
- !options[`no-${longOption}`]) {
276
- c[`no-${longOption}`] = {
277
- type: 'boolean',
278
- multiple: config.multiple,
279
- };
280
- }
281
- }
282
- return c;
194
+ else if (isConfigOption(o, 'number', true)) {
195
+ setDefault(o.default, d => d.map(v => String(v)));
196
+ }
197
+ else if (isConfigOption(o, 'string', false) ||
198
+ isConfigOption(o, 'string', true)) {
199
+ setDefault(o.default, v => v);
200
+ }
201
+ else if (isConfigOption(o, 'boolean', false) ||
202
+ isConfigOption(o, 'boolean', true)) {
203
+ p.type = 'boolean';
204
+ setDefault(o.default, v => v);
205
+ setNoBool();
206
+ }
207
+ acc[longOption] = p;
208
+ return acc;
209
+ }, {});
283
210
  };
284
- const isHeading = (r) => r.type === 'heading';
285
- const isDescription = (r) => r.type === 'description';
286
211
  /**
287
212
  * Class returned by the {@link jack} function and all configuration
288
213
  * definition methods. This is what gets chained together.
@@ -320,16 +245,12 @@ export class Jack {
320
245
  this.validate(values);
321
246
  }
322
247
  catch (er) {
323
- const e = er;
324
- if (source && e && typeof e === 'object') {
325
- if (e.cause && typeof e.cause === 'object') {
326
- Object.assign(e.cause, { path: source });
327
- }
328
- else {
329
- e.cause = { path: source };
330
- }
248
+ if (source && er instanceof Error) {
249
+ /* c8 ignore next */
250
+ const cause = typeof er.cause === 'object' ? er.cause : {};
251
+ er.cause = { ...cause, path: source };
331
252
  }
332
- throw e;
253
+ throw er;
333
254
  }
334
255
  for (const [field, value] of Object.entries(values)) {
335
256
  const my = this.#configSet[field];
@@ -392,10 +313,9 @@ export class Jack {
392
313
  if (args === process.argv) {
393
314
  args = args.slice(process._eval !== undefined ? 1 : 2);
394
315
  }
395
- const options = toParseArgsOptionsConfig(this.#configSet);
396
316
  const result = parseArgs({
397
317
  args,
398
- options,
318
+ options: toParseArgsOptionsConfig(this.#configSet),
399
319
  // always strict, but using our own logic
400
320
  strict: false,
401
321
  allowPositionals: this.#allowPositionals,
@@ -488,13 +408,10 @@ export class Jack {
488
408
  for (const [field, value] of Object.entries(p.values)) {
489
409
  const valid = this.#configSet[field]?.validate;
490
410
  const validOptions = this.#configSet[field]?.validOptions;
491
- let cause;
492
- if (validOptions && !isValidOption(value, validOptions)) {
493
- cause = { name: field, found: value, validOptions: validOptions };
494
- }
495
- if (valid && !valid(value)) {
496
- cause = cause || { name: field, found: value };
497
- }
411
+ const cause = validOptions && !isValidOption(value, validOptions) ?
412
+ { name: field, found: value, validOptions: validOptions }
413
+ : valid && !valid(value) ? { name: field, found: value }
414
+ : undefined;
498
415
  if (cause) {
499
416
  throw new Error(`Invalid value provided for --${field}: ${JSON.stringify(value)}`, { cause });
500
417
  }
@@ -547,18 +464,11 @@ export class Jack {
547
464
  },
548
465
  });
549
466
  }
550
- let cause;
551
- if (config.validOptions &&
552
- !isValidOption(value, config.validOptions)) {
553
- cause = {
554
- name: field,
555
- found: value,
556
- validOptions: config.validOptions,
557
- };
558
- }
559
- if (config.validate && !config.validate(value)) {
560
- cause = cause || { name: field, found: value };
561
- }
467
+ const cause = config.validOptions && !isValidOption(value, config.validOptions) ?
468
+ { name: field, found: value, validOptions: config.validOptions }
469
+ : config.validate && !config.validate(value) ?
470
+ { name: field, found: value }
471
+ : undefined;
562
472
  if (cause) {
563
473
  throw new Error(`Invalid config value for ${field}: ${value}`, {
564
474
  cause,
@@ -595,37 +505,37 @@ export class Jack {
595
505
  * Add one or more number fields.
596
506
  */
597
507
  num(fields) {
598
- return this.#addFields(fields, num);
508
+ return this.#addFieldsWith(fields, 'number', false);
599
509
  }
600
510
  /**
601
511
  * Add one or more multiple number fields.
602
512
  */
603
513
  numList(fields) {
604
- return this.#addFields(fields, numList);
514
+ return this.#addFieldsWith(fields, 'number', true);
605
515
  }
606
516
  /**
607
517
  * Add one or more string option fields.
608
518
  */
609
519
  opt(fields) {
610
- return this.#addFields(fields, opt);
520
+ return this.#addFieldsWith(fields, 'string', false);
611
521
  }
612
522
  /**
613
523
  * Add one or more multiple string option fields.
614
524
  */
615
525
  optList(fields) {
616
- return this.#addFields(fields, optList);
526
+ return this.#addFieldsWith(fields, 'string', true);
617
527
  }
618
528
  /**
619
529
  * Add one or more flag fields.
620
530
  */
621
531
  flag(fields) {
622
- return this.#addFields(fields, flag);
532
+ return this.#addFieldsWith(fields, 'boolean', false);
623
533
  }
624
534
  /**
625
535
  * Add one or more multiple flag fields.
626
536
  */
627
537
  flagList(fields) {
628
- return this.#addFields(fields, flagList);
538
+ return this.#addFieldsWith(fields, 'boolean', true);
629
539
  }
630
540
  /**
631
541
  * Generic field definition method. Similar to flag/flagList/number/etc,
@@ -633,29 +543,22 @@ export class Jack {
633
543
  * fields on each one, or Jack won't know how to define them.
634
544
  */
635
545
  addFields(fields) {
636
- const next = this;
637
- for (const [name, field] of Object.entries(fields)) {
638
- this.#validateName(name, field);
639
- next.#fields.push({
640
- type: 'config',
641
- name,
642
- value: field,
643
- });
644
- }
645
- Object.assign(next.#configSet, fields);
646
- return next;
546
+ return this.#addFields(this, fields);
547
+ }
548
+ #addFieldsWith(fields, type, multiple) {
549
+ return this.#addFields(this, fields, {
550
+ type,
551
+ multiple,
552
+ });
647
553
  }
648
- #addFields(fields, fn) {
649
- const next = this;
554
+ #addFields(next, fields, opt) {
650
555
  Object.assign(next.#configSet, Object.fromEntries(Object.entries(fields).map(([name, field]) => {
651
556
  this.#validateName(name, field);
652
- const option = fn(field);
653
- next.#fields.push({
654
- type: 'config',
655
- name,
656
- value: option,
657
- });
658
- return [name, option];
557
+ const { type, multiple } = validateFieldMeta(field, opt);
558
+ const value = { ...field, type, multiple };
559
+ validateField(value, type, multiple);
560
+ next.#fields.push({ type: 'config', name, value });
561
+ return [name, value];
659
562
  })));
660
563
  return next;
661
564
  }
@@ -691,6 +594,7 @@ export class Jack {
691
594
  if (this.#usage)
692
595
  return this.#usage;
693
596
  let headingLevel = 1;
597
+ //@ts-ignore
694
598
  const ui = cliui({ width });
695
599
  const first = this.#fields[0];
696
600
  let start = first?.type === 'heading' ? 1 : 0;
@@ -932,6 +836,10 @@ export class Jack {
932
836
  return `Jack ${inspect(this.toJSON(), options)}`;
933
837
  }
934
838
  }
839
+ /**
840
+ * Main entry point. Create and return a {@link Jack} object.
841
+ */
842
+ export const jack = (options = {}) => new Jack(options);
935
843
  // Unwrap and un-indent, so we can wrap description
936
844
  // strings however makes them look nice in the code.
937
845
  const normalize = (s, pre = false) => {
@@ -993,8 +901,4 @@ const normalizeOneLine = (s, pre = false) => {
993
901
  .trim();
994
902
  return pre ? `\`${n}\`` : n;
995
903
  };
996
- /**
997
- * Main entry point. Create and return a {@link Jack} object.
998
- */
999
- export const jack = (options = {}) => new Jack(options);
1000
904
  //# sourceMappingURL=index.js.map