@naturalcycles/nodejs-lib 15.115.0 → 15.115.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.
@@ -43,6 +43,7 @@ export function _parseArgs(options, opt = {}) {
43
43
  options: nodeOptions,
44
44
  allowPositionals: true,
45
45
  allowNegative: true, // native `--no-flag` support for booleans
46
+ tokens: true, // needed to detect the ambiguous `--boolFlag value` space form
46
47
  // In non-strict mode, unknown options are collected into `values` but
47
48
  // ignored below, since we only read declared options. See `strict` docs.
48
49
  strict,
@@ -57,6 +58,7 @@ export function _parseArgs(options, opt = {}) {
57
58
  process.stdout.write(buildHelp(options, usage));
58
59
  process.exit(0);
59
60
  }
61
+ assertNoSpaceValuedBoolean(parsed.tokens, options);
60
62
  const result = { _: parsed.positionals };
61
63
  for (const [name, def] of Object.entries(options)) {
62
64
  let v = values[name];
@@ -78,10 +80,32 @@ export function _parseArgs(options, opt = {}) {
78
80
  if (def.array) {
79
81
  v = Array.isArray(v) ? v : [v];
80
82
  }
83
+ // A non-boolean option passed as a bare flag (`--out` with no value) comes back
84
+ // from node's parseArgs (in non-strict mode) as boolean `true`. Reject it: the
85
+ // user almost certainly forgot the value, and silently coercing `true` (to `1`
86
+ // for numbers, `"true"` for strings, or crashing a `transform`) would hide the
87
+ // mistake. Only arg-sourced values are checked; a boolean `default` is left
88
+ // alone. `def.type === 'boolean'` legitimately produces booleans, so skip it.
89
+ if (fromArgs && def.type !== 'boolean') {
90
+ const bareFlag = Array.isArray(v)
91
+ ? v.some(x => typeof x === 'boolean')
92
+ : typeof v === 'boolean';
93
+ if (bareFlag) {
94
+ throw new ParseArgsError(`Missing value for --${name}`);
95
+ }
96
+ }
81
97
  // `transform` owns conversion, so built-in number coercion is skipped for it
82
98
  if (def.type === 'number' && !def.transform) {
83
99
  v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name);
84
100
  }
101
+ // node's parseArgs (in non-strict mode) captures the inline value of
102
+ // `--flag=value` on a boolean option as a string ("false"/"true"), rather than
103
+ // rejecting it as strict mode does. Coerce known tokens so `--flag=false` means
104
+ // boolean false, not a truthy "false" string. Real booleans produced by
105
+ // `--flag` / `--no-flag` (and boolean defaults) pass through untouched.
106
+ if (def.type === 'boolean') {
107
+ v = Array.isArray(v) ? v.map(x => toBoolean(x, name)) : toBoolean(v, name);
108
+ }
85
109
  if (def.choices) {
86
110
  const list = Array.isArray(v) ? v : [v];
87
111
  for (const x of list) {
@@ -101,6 +125,28 @@ export function _parseArgs(options, opt = {}) {
101
125
  }
102
126
  return result;
103
127
  }
128
+ /**
129
+ * Reject the ambiguous `--boolFlag value` space form. node never consumes the
130
+ * next token as a boolean's value (getopt convention), so `--arg false` would
131
+ * silently yield `arg: true` and leak "false" into positionals. Unlike the
132
+ * `=value` form (handled by toBoolean) we can't recover the intended value here,
133
+ * so fail loudly. Only `true`/`false` tokens are treated as ambiguous; any other
134
+ * positional (e.g. a filename) is left as a genuine positional.
135
+ */
136
+ function assertNoSpaceValuedBoolean(tokens, options) {
137
+ for (let i = 0; i < tokens.length - 1; i++) {
138
+ const tok = tokens[i];
139
+ // `tok.value === undefined` => bare flag (no inline `=value`); applies to
140
+ // declared boolean options only (unknown options are ignored, see `strict`).
141
+ if (tok.kind !== 'option' || tok.value !== undefined || options[tok.name]?.type !== 'boolean') {
142
+ continue;
143
+ }
144
+ const next = tokens[i + 1];
145
+ if (next.kind === 'positional' && (next.value === 'true' || next.value === 'false')) {
146
+ throw new ParseArgsError(`Boolean option --${tok.name} does not take a space-separated value ("${next.value}"); use --${tok.name}=${next.value} or --${next.value === 'false' ? `no-${tok.name}` : tok.name}`);
147
+ }
148
+ }
149
+ }
104
150
  function toNumber(raw, name) {
105
151
  const n = Number(raw);
106
152
  if (Number.isNaN(n)) {
@@ -108,6 +154,15 @@ function toNumber(raw, name) {
108
154
  }
109
155
  return n;
110
156
  }
157
+ function toBoolean(raw, name) {
158
+ if (typeof raw === 'boolean')
159
+ return raw; // real boolean from --flag / --no-flag / default
160
+ if (raw === 'true')
161
+ return true;
162
+ if (raw === 'false')
163
+ return false;
164
+ throw new ParseArgsError(`Invalid boolean for --${name}: "${raw}"`);
165
+ }
111
166
  function buildHelp(options, usage) {
112
167
  const lines = [];
113
168
  if (usage)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.115.0",
4
+ "version": "15.115.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -15,8 +15,8 @@
15
15
  "yaml": "^2"
16
16
  },
17
17
  "devDependencies": {
18
- "typescript": "rc",
19
- "@naturalcycles/dev-lib": "18.4.2"
18
+ "typescript": "^7",
19
+ "@naturalcycles/dev-lib": "0.0.0"
20
20
  },
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
@@ -186,6 +186,7 @@ export function _parseArgs<const O extends CliOptions>(
186
186
  options: nodeOptions,
187
187
  allowPositionals: true,
188
188
  allowNegative: true, // native `--no-flag` support for booleans
189
+ tokens: true, // needed to detect the ambiguous `--boolFlag value` space form
189
190
  // In non-strict mode, unknown options are collected into `values` but
190
191
  // ignored below, since we only read declared options. See `strict` docs.
191
192
  strict,
@@ -202,6 +203,8 @@ export function _parseArgs<const O extends CliOptions>(
202
203
  process.exit(0)
203
204
  }
204
205
 
206
+ assertNoSpaceValuedBoolean(parsed.tokens, options)
207
+
205
208
  const result: Record<string, unknown> = { _: parsed.positionals }
206
209
 
207
210
  for (const [name, def] of Object.entries(options)) {
@@ -225,11 +228,35 @@ export function _parseArgs<const O extends CliOptions>(
225
228
  v = Array.isArray(v) ? v : [v]
226
229
  }
227
230
 
231
+ // A non-boolean option passed as a bare flag (`--out` with no value) comes back
232
+ // from node's parseArgs (in non-strict mode) as boolean `true`. Reject it: the
233
+ // user almost certainly forgot the value, and silently coercing `true` (to `1`
234
+ // for numbers, `"true"` for strings, or crashing a `transform`) would hide the
235
+ // mistake. Only arg-sourced values are checked; a boolean `default` is left
236
+ // alone. `def.type === 'boolean'` legitimately produces booleans, so skip it.
237
+ if (fromArgs && def.type !== 'boolean') {
238
+ const bareFlag = Array.isArray(v)
239
+ ? v.some(x => typeof x === 'boolean')
240
+ : typeof v === 'boolean'
241
+ if (bareFlag) {
242
+ throw new ParseArgsError(`Missing value for --${name}`)
243
+ }
244
+ }
245
+
228
246
  // `transform` owns conversion, so built-in number coercion is skipped for it
229
247
  if (def.type === 'number' && !def.transform) {
230
248
  v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name)
231
249
  }
232
250
 
251
+ // node's parseArgs (in non-strict mode) captures the inline value of
252
+ // `--flag=value` on a boolean option as a string ("false"/"true"), rather than
253
+ // rejecting it as strict mode does. Coerce known tokens so `--flag=false` means
254
+ // boolean false, not a truthy "false" string. Real booleans produced by
255
+ // `--flag` / `--no-flag` (and boolean defaults) pass through untouched.
256
+ if (def.type === 'boolean') {
257
+ v = Array.isArray(v) ? v.map(x => toBoolean(x, name)) : toBoolean(v, name)
258
+ }
259
+
233
260
  if (def.choices) {
234
261
  const list = Array.isArray(v) ? v : [v]
235
262
  for (const x of list) {
@@ -258,6 +285,34 @@ export function _parseArgs<const O extends CliOptions>(
258
285
  return result as InferCliArgs<O>
259
286
  }
260
287
 
288
+ /**
289
+ * Reject the ambiguous `--boolFlag value` space form. node never consumes the
290
+ * next token as a boolean's value (getopt convention), so `--arg false` would
291
+ * silently yield `arg: true` and leak "false" into positionals. Unlike the
292
+ * `=value` form (handled by toBoolean) we can't recover the intended value here,
293
+ * so fail loudly. Only `true`/`false` tokens are treated as ambiguous; any other
294
+ * positional (e.g. a filename) is left as a genuine positional.
295
+ */
296
+ function assertNoSpaceValuedBoolean(
297
+ tokens: NonNullable<ReturnType<typeof parseArgs>['tokens']>,
298
+ options: CliOptions,
299
+ ): void {
300
+ for (let i = 0; i < tokens.length - 1; i++) {
301
+ const tok = tokens[i]!
302
+ // `tok.value === undefined` => bare flag (no inline `=value`); applies to
303
+ // declared boolean options only (unknown options are ignored, see `strict`).
304
+ if (tok.kind !== 'option' || tok.value !== undefined || options[tok.name]?.type !== 'boolean') {
305
+ continue
306
+ }
307
+ const next = tokens[i + 1]!
308
+ if (next.kind === 'positional' && (next.value === 'true' || next.value === 'false')) {
309
+ throw new ParseArgsError(
310
+ `Boolean option --${tok.name} does not take a space-separated value ("${next.value}"); use --${tok.name}=${next.value} or --${next.value === 'false' ? `no-${tok.name}` : tok.name}`,
311
+ )
312
+ }
313
+ }
314
+ }
315
+
261
316
  function toNumber(raw: unknown, name: string): number {
262
317
  const n = Number(raw)
263
318
  if (Number.isNaN(n)) {
@@ -266,6 +321,13 @@ function toNumber(raw: unknown, name: string): number {
266
321
  return n
267
322
  }
268
323
 
324
+ function toBoolean(raw: unknown, name: string): boolean {
325
+ if (typeof raw === 'boolean') return raw // real boolean from --flag / --no-flag / default
326
+ if (raw === 'true') return true
327
+ if (raw === 'false') return false
328
+ throw new ParseArgsError(`Invalid boolean for --${name}: "${raw}"`)
329
+ }
330
+
269
331
  function buildHelp(options: CliOptions, usage?: string): string {
270
332
  const lines: string[] = []
271
333
  if (usage) lines.push(usage, '')