@naturalcycles/nodejs-lib 15.114.0 → 15.115.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.
@@ -7,7 +7,10 @@ export type CliOptionType = 'string' | 'number' | 'boolean';
7
7
  * yargs `.options()` that we actually use.
8
8
  */
9
9
  export interface CliOption {
10
- type: CliOptionType;
10
+ /**
11
+ * Value type. Defaults to `'string'` when omitted.
12
+ */
13
+ type?: CliOptionType;
11
14
  /**
12
15
  * Accept the flag multiple times, collecting values into an array.
13
16
  * `--id a --id b` => `['a', 'b']`. Replaces yargs `type: 'array'`.
@@ -36,15 +39,34 @@ export interface CliOption {
36
39
  * Single-character alias, e.g. `short: 'v'` enables `-v`.
37
40
  */
38
41
  short?: string;
42
+ /**
43
+ * Transform the raw string value into the final value. The output type is
44
+ * inferred from the function's return type, so this is the way to produce
45
+ * branded types (e.g. `IsoDate`) or richer values (parsed numbers, JSON, ...).
46
+ *
47
+ * Applied per-element for `array` options. Receives the raw string token, so
48
+ * it fully owns conversion - built-in `number` coercion is not applied on top.
49
+ * NOT applied to `default` values: a `default` is taken to be in final form.
50
+ *
51
+ * @example transform: s => s as IsoDate
52
+ */
53
+ transform?: (value: string) => unknown;
39
54
  }
40
55
  export type CliOptions = Record<string, CliOption>;
41
56
  /**
42
- * Element value type of a single option, derived from `choices` (if present)
43
- * or `type`.
57
+ * Element value type of a single option, derived from `transform` (if present,
58
+ * its return type wins), then `choices`, then `type`. Defaults to `string` when
59
+ * none narrow it (so `type` is optional).
44
60
  */
45
61
  type ElemType<O extends CliOption> = O extends {
62
+ transform: (...args: any[]) => infer R;
63
+ } ? R : O extends {
46
64
  choices: readonly (infer C)[];
47
- } ? C : O['type'] extends 'string' ? string : O['type'] extends 'number' ? number : O['type'] extends 'boolean' ? boolean : never;
65
+ } ? C : O extends {
66
+ type: 'number';
67
+ } ? number : O extends {
68
+ type: 'boolean';
69
+ } ? boolean : string;
48
70
  /**
49
71
  * Full value type of a single option, applying `array` on top of the element
50
72
  * type.
@@ -116,11 +138,12 @@ export declare class ParseArgsError extends Error {
116
138
  * top of node's `util.parseArgs`.
117
139
  *
118
140
  * @example
119
- * const { dir, limit } = _parseArgs({
120
- * dir: { type: 'string', desc: 'Output directory' },
141
+ * const { dir, limit, date } = _parseArgs({
142
+ * dir: { desc: 'Output directory' }, // type defaults to 'string'
121
143
  * limit: { type: 'number', default: 100 },
144
+ * date: { transform: s => s as IsoDate }, // inferred + converted via transform
122
145
  * })
123
- * // dir?: string limit: number _: string[]
146
+ * // dir?: string limit: number date?: IsoDate _: string[]
124
147
  */
125
148
  export declare function _parseArgs<const O extends CliOptions>(options: O, opt?: ParseArgsOptions): InferCliArgs<O>;
126
149
  export {};
@@ -11,11 +11,12 @@ export class ParseArgsError extends Error {
11
11
  * top of node's `util.parseArgs`.
12
12
  *
13
13
  * @example
14
- * const { dir, limit } = _parseArgs({
15
- * dir: { type: 'string', desc: 'Output directory' },
14
+ * const { dir, limit, date } = _parseArgs({
15
+ * dir: { desc: 'Output directory' }, // type defaults to 'string'
16
16
  * limit: { type: 'number', default: 100 },
17
+ * date: { transform: s => s as IsoDate }, // inferred + converted via transform
17
18
  * })
18
- * // dir?: string limit: number _: string[]
19
+ * // dir?: string limit: number date?: IsoDate _: string[]
19
20
  */
20
21
  export function _parseArgs(options, opt = {}) {
21
22
  const { args, minPositionals = 0, usage, strict = false } = opt;
@@ -59,6 +60,9 @@ export function _parseArgs(options, opt = {}) {
59
60
  const result = { _: parsed.positionals };
60
61
  for (const [name, def] of Object.entries(options)) {
61
62
  let v = values[name];
63
+ // `transform` is only applied to arg-sourced values; a `default` is taken
64
+ // to be in final form (see CliOption.transform docs).
65
+ const fromArgs = v !== undefined;
62
66
  if (v === undefined) {
63
67
  if (def.default !== undefined) {
64
68
  v = def.default;
@@ -74,7 +78,8 @@ export function _parseArgs(options, opt = {}) {
74
78
  if (def.array) {
75
79
  v = Array.isArray(v) ? v : [v];
76
80
  }
77
- if (def.type === 'number') {
81
+ // `transform` owns conversion, so built-in number coercion is skipped for it
82
+ if (def.type === 'number' && !def.transform) {
78
83
  v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name);
79
84
  }
80
85
  if (def.choices) {
@@ -85,6 +90,10 @@ export function _parseArgs(options, opt = {}) {
85
90
  }
86
91
  }
87
92
  }
93
+ if (def.transform && fromArgs) {
94
+ const { transform } = def;
95
+ v = Array.isArray(v) ? v.map(x => transform(x)) : transform(v);
96
+ }
88
97
  result[name] = v;
89
98
  }
90
99
  if (parsed.positionals.length < minPositionals) {
@@ -106,7 +115,8 @@ function buildHelp(options, usage) {
106
115
  lines.push('Options:');
107
116
  for (const [name, def] of Object.entries(options)) {
108
117
  const flag = def.short ? `-${def.short}, --${name}` : `--${name}`;
109
- const meta = [`[${def.array ? `${def.type}[]` : def.type}]`];
118
+ const type = def.type ?? 'string';
119
+ const meta = [`[${def.array ? `${type}[]` : type}]`];
110
120
  if (def.demandOption)
111
121
  meta.push('[required]');
112
122
  if (def.default !== undefined)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.114.0",
4
+ "version": "15.115.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "typescript": "rc",
19
- "@naturalcycles/dev-lib": "20.50.0"
19
+ "@naturalcycles/dev-lib": "18.4.2"
20
20
  },
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
@@ -10,7 +10,10 @@ export type CliOptionType = 'string' | 'number' | 'boolean'
10
10
  * yargs `.options()` that we actually use.
11
11
  */
12
12
  export interface CliOption {
13
- type: CliOptionType
13
+ /**
14
+ * Value type. Defaults to `'string'` when omitted.
15
+ */
16
+ type?: CliOptionType
14
17
  /**
15
18
  * Accept the flag multiple times, collecting values into an array.
16
19
  * `--id a --id b` => `['a', 'b']`. Replaces yargs `type: 'array'`.
@@ -39,23 +42,36 @@ export interface CliOption {
39
42
  * Single-character alias, e.g. `short: 'v'` enables `-v`.
40
43
  */
41
44
  short?: string
45
+ /**
46
+ * Transform the raw string value into the final value. The output type is
47
+ * inferred from the function's return type, so this is the way to produce
48
+ * branded types (e.g. `IsoDate`) or richer values (parsed numbers, JSON, ...).
49
+ *
50
+ * Applied per-element for `array` options. Receives the raw string token, so
51
+ * it fully owns conversion - built-in `number` coercion is not applied on top.
52
+ * NOT applied to `default` values: a `default` is taken to be in final form.
53
+ *
54
+ * @example transform: s => s as IsoDate
55
+ */
56
+ transform?: (value: string) => unknown
42
57
  }
43
58
 
44
59
  export type CliOptions = Record<string, CliOption>
45
60
 
46
61
  /**
47
- * Element value type of a single option, derived from `choices` (if present)
48
- * or `type`.
62
+ * Element value type of a single option, derived from `transform` (if present,
63
+ * its return type wins), then `choices`, then `type`. Defaults to `string` when
64
+ * none narrow it (so `type` is optional).
49
65
  */
50
- type ElemType<O extends CliOption> = O extends { choices: readonly (infer C)[] }
51
- ? C
52
- : O['type'] extends 'string'
53
- ? string
54
- : O['type'] extends 'number'
66
+ type ElemType<O extends CliOption> = O extends { transform: (...args: any[]) => infer R }
67
+ ? R
68
+ : O extends { choices: readonly (infer C)[] }
69
+ ? C
70
+ : O extends { type: 'number' }
55
71
  ? number
56
- : O['type'] extends 'boolean'
72
+ : O extends { type: 'boolean' }
57
73
  ? boolean
58
- : never
74
+ : string
59
75
 
60
76
  /**
61
77
  * Full value type of a single option, applying `array` on top of the element
@@ -132,11 +148,12 @@ export class ParseArgsError extends Error {
132
148
  * top of node's `util.parseArgs`.
133
149
  *
134
150
  * @example
135
- * const { dir, limit } = _parseArgs({
136
- * dir: { type: 'string', desc: 'Output directory' },
151
+ * const { dir, limit, date } = _parseArgs({
152
+ * dir: { desc: 'Output directory' }, // type defaults to 'string'
137
153
  * limit: { type: 'number', default: 100 },
154
+ * date: { transform: s => s as IsoDate }, // inferred + converted via transform
138
155
  * })
139
- * // dir?: string limit: number _: string[]
156
+ * // dir?: string limit: number date?: IsoDate _: string[]
140
157
  */
141
158
  export function _parseArgs<const O extends CliOptions>(
142
159
  options: O,
@@ -189,6 +206,9 @@ export function _parseArgs<const O extends CliOptions>(
189
206
 
190
207
  for (const [name, def] of Object.entries(options)) {
191
208
  let v = values[name]
209
+ // `transform` is only applied to arg-sourced values; a `default` is taken
210
+ // to be in final form (see CliOption.transform docs).
211
+ const fromArgs = v !== undefined
192
212
 
193
213
  if (v === undefined) {
194
214
  if (def.default !== undefined) {
@@ -205,7 +225,8 @@ export function _parseArgs<const O extends CliOptions>(
205
225
  v = Array.isArray(v) ? v : [v]
206
226
  }
207
227
 
208
- if (def.type === 'number') {
228
+ // `transform` owns conversion, so built-in number coercion is skipped for it
229
+ if (def.type === 'number' && !def.transform) {
209
230
  v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name)
210
231
  }
211
232
 
@@ -220,6 +241,11 @@ export function _parseArgs<const O extends CliOptions>(
220
241
  }
221
242
  }
222
243
 
244
+ if (def.transform && fromArgs) {
245
+ const { transform } = def
246
+ v = Array.isArray(v) ? v.map(x => transform(x as string)) : transform(v as string)
247
+ }
248
+
223
249
  result[name] = v
224
250
  }
225
251
 
@@ -246,7 +272,8 @@ function buildHelp(options: CliOptions, usage?: string): string {
246
272
  lines.push('Options:')
247
273
  for (const [name, def] of Object.entries(options)) {
248
274
  const flag = def.short ? `-${def.short}, --${name}` : `--${name}`
249
- const meta = [`[${def.array ? `${def.type}[]` : def.type}]`]
275
+ const type = def.type ?? 'string'
276
+ const meta = [`[${def.array ? `${type}[]` : type}]`]
250
277
  if (def.demandOption) meta.push('[required]')
251
278
  if (def.default !== undefined) meta.push(`[default: ${JSON.stringify(def.default)}]`)
252
279
  if (def.choices) meta.push(`[choices: ${def.choices.join(', ')}]`)