Expand Node 24 dynamic argument parsing compatibility (#632)

* Expand Node 24 dynamic argument parsing compatibility

* Match Node argument error string truncation and quoting
This commit is contained in:
Chris Tate
2026-10-03 09:29:06 -05:00
committed by GitHub
parent 83b6156282
commit c76ac2b95f
16 changed files with 362 additions and 167 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "80b5ea587a01ac030822",
"artifactVersion": "3c31fba8150926a20ac8",
"rowCount": 3662
}
@@ -85436,7 +85436,7 @@
},
"dynamic": {
"status": "partial",
"detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.",
"detail": "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation.",
"verification": "test-backed"
}
},
@@ -85464,7 +85464,7 @@
},
"dynamic": {
"status": "partial",
"detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.",
"detail": "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation.",
"verification": "test-backed"
}
},
@@ -19,6 +19,7 @@
"wasi": "by-design"
},
"features": [
{ "chapter": "util", "symbols": ["util.parseArgs", "parseArgs"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-parseargs/main.ts"] },
{ "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] },
@@ -95432,8 +95432,8 @@
"confidence": "high",
"source": "surface-manifest:node-builtin.util.parseArgs",
"tests": [
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/2912-promisify-fs-readfile.ts"
"tests/corpus/2678-util-parseargs.ts",
"tests/corpus/node24-parseargs.js"
]
},
"dynamic": {
@@ -95441,9 +95441,10 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-export:node:util.parseArgs",
"source": "island-feature:util.util.parseArgs",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-parseargs/main.ts"
]
}
}
@@ -95470,8 +95471,8 @@
"confidence": "high",
"source": "surface-manifest:node-builtin.util.parseArgs",
"tests": [
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/2912-promisify-fs-readfile.ts"
"tests/corpus/2678-util-parseargs.ts",
"tests/corpus/node24-parseargs.js"
]
},
"dynamic": {
@@ -95479,9 +95480,10 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-export:node:util.parseArgs",
"source": "island-feature:util.parseArgs",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-parseargs/main.ts"
]
}
}
@@ -111022,11 +111022,13 @@
"tests/corpus/1473-promisify-execfile.ts",
"tests/corpus/1573-promisify-execfile-env-spread.ts",
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/2678-util-parseargs.ts",
"tests/corpus/2912-promisify-fs-readfile.ts",
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/node24-env-imports.cjs",
"tests/corpus/node24-env-parse-errors.ts",
"tests/corpus/node24-env-parse.ts",
"tests/corpus/node24-parseargs.js",
"tests/corpus/node24-text-codec-properties.ts",
"tests/corpus/node24-text-encode-into.ts",
"tests/corpus/text-codec-values/main.ts",
@@ -111041,6 +111043,7 @@
"evidence": "derived:descendants",
"tests": [
"tests/fixtures/npm/cases/node24-env/main.ts",
"tests/fixtures/npm/cases/node24-parseargs/main.ts",
"tests/fixtures/npm/cases/node24-text-encoding/main.ts",
"tests/fixtures/npm/cases/util-shims/main.ts"
]
@@ -112716,15 +112719,16 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.parseArgs",
"tests": [
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/2912-promisify-fs-readfile.ts"
"tests/corpus/2678-util-parseargs.ts",
"tests/corpus/node24-parseargs.js"
]
},
"dynamic": {
"status": "partial",
"evidence": "island-export:node:util.parseArgs",
"evidence": "island-feature:util.util.parseArgs",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-parseargs/main.ts"
]
},
"anchorSource": "exact"
@@ -112750,15 +112754,16 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.parseArgs",
"tests": [
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/2912-promisify-fs-readfile.ts"
"tests/corpus/2678-util-parseargs.ts",
"tests/corpus/node24-parseargs.js"
]
},
"dynamic": {
"status": "partial",
"evidence": "island-export:node:util.parseArgs",
"evidence": "island-feature:util.parseArgs",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-parseargs/main.ts"
]
},
"anchorSource": "exact"
+3
View File
@@ -678,6 +678,9 @@ function publicDetail(tier) {
return "No dynamic-island implementation exists for this Node API family yet.";
}
if (source.startsWith("island-feature:")) {
if (/^island-feature:util\.(?:util\.)?parseArgs$/.test(source)) {
return "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation.";
}
if (/^island-feature:buffer\.buffer\.(?:isAscii|isUtf8)$/.test(source)) {
return "Validates bytes in ArrayBuffer and typed-array views with Node-compatible argument errors.";
}
@@ -87,6 +87,7 @@
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"],
"node-builtin.url.pathToFileURL": ["tests/corpus/path-to-file-url-options.ts"],
"node-builtin.url.fileURLToPath": ["tests/corpus/file-url-percent-decoding.ts"],
"node-builtin.util.parseArgs": ["tests/corpus/2678-util-parseargs.ts", "tests/corpus/node24-parseargs.js"],
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts"],
@@ -13396,6 +13396,18 @@
"<repo>/tests/fixtures/npm/cases/node24-string-decoder/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/node24-parseargs.js": {
"order": [
"<repo>/tests/corpus/node24-parseargs.js"
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/node24-parseargs/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/node24-parseargs/main.ts"
],
"diags": []
}
}
}
+138 -134
View File
@@ -8374,147 +8374,151 @@ static const char isl_modules_bootstrap[] =
" }\n"
" return colorize ? left + text + right : text;\n"
" };\n"
" const parseArgs = (config) => {\n"
" config = config === undefined ? {} : config;\n"
" const args = config.args !== undefined ? config.args : env.argv.slice(2);\n"
" const strict = config.strict !== undefined ? !!config.strict : true;\n"
" const allowPositionals = config.allowPositionals !== undefined ? !!config.allowPositionals : !strict;\n"
" const allowNegative = !!config.allowNegative;\n"
" const returnTokens = !!config.tokens;\n"
" const options = config.options !== undefined ? config.options : {};\n"
" const result = { values: { __proto__: null }, positionals: [] };\n"
" const parseArgs = (config = {}) => {\n"
" if (config === null) throw new TypeError(\"Cannot convert undefined or null to object\");\n"
" const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);\n"
" const get = (o, k) => own(o, k) ? o[k] : undefined;\n"
" const args = get(config, 'args') ?? env.argv.slice(2);\n"
" const strict = get(config, 'strict') ?? true;\n"
" const allowPositionals = get(config, 'allowPositionals') ?? !strict;\n"
" const returnTokens = get(config, 'tokens') ?? false;\n"
" const allowNegative = get(config, 'allowNegative') ?? false;\n"
" const options = get(config, 'options') ?? Object.create(null);\n"
" const fail = (code, message) => { const e = new TypeError(message); e.code = code; throw e; };\n"
" const received = (v) => {\n"
" if (v === null || v === undefined) return String(v);\n"
" if (typeof v === 'object') return v.constructor && 'name' in v.constructor ? 'an instance of ' + v.constructor.name : inspect(v, { depth: -1 });\n"
" if (typeof v === 'function') return 'function ' + v.name;\n"
" if (typeof v === 'string') {\n"
" if (v.length > 28) v = v.slice(0, 25) + '...';\n"
" return 'type string (' + (v.includes(\"'\") ? JSON.stringify(v) : \"'\" + v + \"'\") + ')';\n"
" }\n"
" if (typeof v === 'number') return 'type number (' + (Object.is(v, -0) ? '-0' : String(v)) + ')';\n"
" if (typeof v === 'bigint') return 'type bigint (' + v + 'n)';\n"
" if (typeof v === 'symbol') return 'type symbol (' + String(v) + ')';\n"
" return 'type ' + typeof v + ' (' + String(v) + ')';\n"
" };\n"
" const validate = (v, name, expected, ok) => {\n"
" if (!ok) fail('ERR_INVALID_ARG_TYPE', 'The \"' + name + '\" ' + (name.startsWith('options.') ? 'property' : 'argument') + ' must be ' + expected + '. Received ' + received(v));\n"
" };\n"
" const object = (v, name) => validate(v, name, 'of type object', v !== null && typeof v === 'object' && !Array.isArray(v));\n"
" const boolean = (v, name) => validate(v, name, 'of type boolean', typeof v === 'boolean');\n"
" const string = (v, name) => validate(v, name, 'of type string', typeof v === 'string');\n"
" validate(args, 'args', 'an instance of Array', Array.isArray(args));\n"
" boolean(strict, 'strict');\n"
" boolean(allowPositionals, 'allowPositionals');\n"
" boolean(returnTokens, 'tokens');\n"
" boolean(allowNegative, 'allowNegative');\n"
" object(options, 'options');\n"
" for (const [name, option] of Object.entries(options)) {\n"
" const path = 'options.' + name;\n"
" object(option, path);\n"
" const type = get(option, 'type');\n"
" validate(type, path + '.type', \"('string|boolean')\", type === 'string' || type === 'boolean');\n"
" if (own(option, 'short')) {\n"
" const short = option.short;\n"
" string(short, path + '.short');\n"
" if (short.length !== 1) fail('ERR_INVALID_ARG_VALUE', \"The property '\" + path + \".short' must be a single character. Received \" + inspect(short));\n"
" }\n"
" const multiple = get(option, 'multiple');\n"
" if (own(option, 'multiple')) boolean(multiple, path + '.multiple');\n"
" const value = get(option, 'default');\n"
" if (value !== undefined) {\n"
" const check = type === 'string' ? string : boolean;\n"
" if (multiple) {\n"
" validate(value, path + '.default', 'an instance of Array', Array.isArray(value));\n"
" for (let i = 0; i < value.length; i++) check(value[i], path + '.default[' + i + ']');\n"
" } else check(value, path + '.default');\n"
" }\n"
" }\n"
" const optionGet = (name, key) => own(options, name) ? get(options[name], key) : undefined;\n"
" const shortName = (ch) => {\n"
" const entry = Object.entries(options).find(([, option]) => get(option, 'short') === ch);\n"
" return entry ? entry[0] : ch;\n"
" };\n"
" const tokens = [];\n"
" const shortOf = (ch) => {\n"
" for (const name of Object.keys(options)) {\n"
" if (options[name].short === ch) return name;\n"
" // Tokenize the full argument snapshot before checking usage, as Node does.\n"
" const remaining = Array.prototype.slice.call(args);\n"
" let index = -1;\n"
" let groupCount = 0;\n"
" while (remaining.length) {\n"
" const arg = remaining.shift();\n"
" if (groupCount > 0) groupCount--; else index++;\n"
" if (arg === '--') {\n"
" tokens.push({ kind: 'option-terminator', index });\n"
" for (const value of remaining) tokens.push({ kind: 'positional', index: ++index, value });\n"
" break;\n"
" }\n"
" return undefined;\n"
" };\n"
" const unknownError = (raw) => {\n"
" const e = new TypeError(\"Unknown option '\" + raw + \"'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- \\\"\" + raw + \"\\\"\");\n"
" e.code = \"ERR_PARSE_ARGS_UNKNOWN_OPTION\";\n"
" throw e;\n"
" };\n"
" const store = (name, value, raw) => {\n"
" const cfg = options[name] || {};\n"
" const type = cfg.type;\n"
" if (strict) {\n"
" if (options[name] === undefined) unknownError(raw);\n"
" if (type === \"string\" && value === undefined) {\n"
" const e = new TypeError(\"Option '\" + raw + (cfg.short && raw.startsWith(\"--\") === false ? \"\" : \"\") + \" <value>' argument missing\");\n"
" e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n"
" throw e;\n"
" }\n"
" if (type === \"boolean\" && value !== undefined) {\n"
" const e = new TypeError(\"Option '\" + raw + \"' does not take an argument\");\n"
" e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n"
" throw e;\n"
" }\n"
" }\n"
" const finalValue = value === undefined ? true : value;\n"
" tokens.push({ kind: \"option\", name, rawName: raw, index: tokenIndex, value: value === undefined ? undefined : value, inlineValue: inline });\n"
" if (cfg.multiple) {\n"
" if (result.values[name] === undefined) result.values[name] = [];\n"
" result.values[name].push(finalValue);\n"
" } else {\n"
" result.values[name] = finalValue;\n"
" }\n"
" };\n"
" let tokenIndex = -1;\n"
" let inline;\n"
" let afterDashDash = false;\n"
" for (let i = 0; i < args.length; i++) {\n"
" const arg = args[i];\n"
" tokenIndex = i;\n"
" inline = undefined;\n"
" if (afterDashDash) {\n"
" tokens.push({ kind: \"positional\", index: i, value: arg });\n"
" result.positionals.push(arg);\n"
" continue;\n"
" }\n"
" if (arg === \"--\") {\n"
" afterDashDash = true;\n"
" tokens.push({ kind: \"option-terminator\", index: i });\n"
" continue;\n"
" }\n"
" if (arg.startsWith(\"--\")) {\n"
" const eq = arg.indexOf(\"=\");\n"
" if (eq !== -1) {\n"
" const name = arg.slice(2, eq);\n"
" inline = true;\n"
" store(name, arg.slice(eq + 1), \"--\" + name);\n"
" // Reading length first deliberately preserves Node's nullish-argument error.\n"
" if (arg == null) throw new TypeError(\"Cannot read properties of \" + arg + \" (reading 'length')\");\n"
" const length = arg.length;\n"
" if (length === 2 && String.prototype.charAt.call(arg, 0) === '-' && String.prototype.charAt.call(arg, 1) !== '-') {\n"
" const name = shortName(String.prototype.charAt.call(arg, 1));\n"
" let value, inlineValue;\n"
" if (optionGet(name, 'type') === 'string' && remaining[0] != null) { value = remaining.shift(); inlineValue = false; }\n"
" tokens.push({ kind: 'option', name, rawName: arg, index, value, inlineValue });\n"
" if (value != null) index++;\n"
" } else if (length > 2 && String.prototype.charAt.call(arg, 0) === '-' && String.prototype.charAt.call(arg, 1) !== '-') {\n"
" const firstName = shortName(String.prototype.charAt.call(arg, 1));\n"
" if (optionGet(firstName, 'type') === 'string') {\n"
" tokens.push({ kind: 'option', name: firstName, rawName: '-' + String.prototype.charAt.call(arg, 1), index, value: String.prototype.slice.call(arg, 2), inlineValue: true });\n"
" } else {\n"
" const name = arg.slice(2);\n"
" const cfg = options[name];\n"
" if (cfg !== undefined && cfg.type === \"string\" && i + 1 < args.length) {\n"
" inline = false;\n"
" store(name, args[++i], arg);\n"
" } else if (allowNegative && name.startsWith(\"no-\") && (options[name.slice(3)] || {}).type === \"boolean\") {\n"
" const positive = name.slice(3);\n"
" tokens.push({ kind: \"option\", name: positive, rawName: arg, index: i, value: undefined, inlineValue: undefined });\n"
" result.values[positive] = false;\n"
" } else {\n"
" if (strict && cfg === undefined) unknownError(arg);\n"
" if (strict && cfg.type === \"string\") {\n"
" const e = new TypeError(\"Option '--\" + name + \" <value>' argument missing\");\n"
" e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n"
" throw e;\n"
" }\n"
" store(name, undefined, arg);\n"
" const expanded = [];\n"
" for (let i = 1; i < length; i++) {\n"
" const ch = String.prototype.charAt.call(arg, i);\n"
" const name = shortName(ch);\n"
" if (optionGet(name, 'type') === 'string' && i !== length - 1) { expanded.push('-' + String.prototype.slice.call(arg, i)); break; }\n"
" expanded.push('-' + ch);\n"
" }\n"
" remaining.unshift(...expanded);\n"
" groupCount = expanded.length;\n"
" }\n"
" continue;\n"
" }\n"
" if (arg.length > 1 && arg[0] === \"-\") {\n"
" const chars = arg.slice(1);\n"
" let consumed = false;\n"
" for (let c = 0; c < chars.length; c++) {\n"
" const ch = chars[c];\n"
" const name = shortOf(ch);\n"
" const raw = \"-\" + ch;\n"
" const cfg = name !== undefined ? options[name] : undefined;\n"
" if (cfg !== undefined && cfg.type === \"string\") {\n"
" if (c < chars.length - 1) {\n"
" inline = true;\n"
" store(name, chars.slice(c + 1), raw);\n"
" } else if (i + 1 < args.length) {\n"
" inline = false;\n"
" store(name, args[++i], raw);\n"
" } else if (strict) {\n"
" const e = new TypeError(\"Option '\" + raw + \", --\" + name + \" <value>' argument missing\");\n"
" e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n"
" throw e;\n"
" } else {\n"
" store(name, undefined, raw);\n"
" }\n"
" consumed = true;\n"
" break;\n"
" }\n"
" if (name === undefined) {\n"
" if (strict) unknownError(raw);\n"
" store(ch, undefined, raw);\n"
" } else {\n"
" store(name, undefined, raw);\n"
" }\n"
" }\n"
" void consumed;\n"
" continue;\n"
" }\n"
" if (strict && !allowPositionals) {\n"
" const e = new TypeError(\"Unexpected argument '\" + arg + \"'. This command does not take positional arguments\");\n"
" e.code = \"ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL\";\n"
" throw e;\n"
" }\n"
" tokens.push({ kind: \"positional\", index: i, value: arg });\n"
" result.positionals.push(arg);\n"
" }\n"
" for (const name of Object.keys(options)) {\n"
" const cfg = options[name];\n"
" if (cfg.default !== undefined && !(name in result.values)) {\n"
" result.values[name] = cfg.default;\n"
" }\n"
" } else if (length > 2 && String.prototype.startsWith.call(arg, '--')) {\n"
" const eq = String.prototype.indexOf.call(arg, '=', 3);\n"
" const name = String.prototype.slice.call(arg, 2, eq < 0 ? undefined : eq);\n"
" let value, inlineValue;\n"
" if (eq >= 0) { value = String.prototype.slice.call(arg, eq + 1); inlineValue = true; }\n"
" else if (optionGet(name, 'type') === 'string' && remaining[0] != null) { value = remaining.shift(); inlineValue = false; }\n"
" tokens.push({ kind: 'option', name, rawName: '--' + name, index, value, inlineValue });\n"
" if (eq < 0 && value != null) index++;\n"
" } else tokens.push({ kind: 'positional', index, value: arg });\n"
" }\n"
" const result = { values: Object.create(null), positionals: [] };\n"
" if (returnTokens) result.tokens = tokens;\n"
" for (const token of tokens) {\n"
" if (token.kind === 'positional') {\n"
" if (!allowPositionals) fail('ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL', \"Unexpected argument '\" + token.value + \"'. This command does not take positional arguments\");\n"
" result.positionals.push(token.value);\n"
" } else if (token.kind === 'option') {\n"
" let name = token.name;\n"
" if (strict) {\n"
" if (!own(options, name)) {\n"
" if (allowNegative && name.startsWith('no-') && own(options, name.slice(3)) && optionGet(name.slice(3), 'type') === 'boolean') name = name.slice(3);\n"
" else fail('ERR_PARSE_ARGS_UNKNOWN_OPTION', \"Unknown option '\" + token.rawName + \"'\" + (allowPositionals ? \". To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- \\\"\" + token.rawName + \"\\\"\" : ''));\n"
" }\n"
" const short = optionGet(name, 'short');\n"
" const label = (short ? '-' + short + ', ' : '') + '--' + name;\n"
" if (optionGet(name, 'type') === 'string' && typeof token.value !== 'string') fail('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', \"Option '\" + label + \" <value>' argument missing\");\n"
" if (optionGet(name, 'type') === 'boolean' && token.value != null) fail('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', \"Option '\" + label + \"' does not take an argument\");\n"
" if (!token.inlineValue && token.value != null && token.value.length > 1 && String.prototype.charAt.call(token.value, 0) === '-') {\n"
" const example = token.rawName.startsWith('--') ? \"'\" + token.rawName + \"=-XYZ'\" : \"'--\" + token.name + \"=-XYZ' or '\" + token.rawName + \"-XYZ'\";\n"
" fail('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', \"Option '\" + token.rawName + \"' argument is ambiguous.\\nDid you forget to specify the option argument for '\" + token.rawName + \"'?\\nTo specify an option argument starting with a dash use \" + example + '.');\n"
" }\n"
" }\n"
" name = token.name;\n"
" let value = token.value;\n"
" if (name === '__proto__') continue;\n"
" if (allowNegative && name.startsWith('no-') && value === undefined) { name = name.slice(3); token.name = name; value = false; }\n"
" value = value ?? true;\n"
" if (optionGet(name, 'multiple')) {\n"
" if (!result.values[name]) result.values[name] = [];\n"
" result.values[name].push(value);\n"
" } else result.values[name] = value;\n"
" }\n"
" }\n"
" for (const [name, option] of Object.entries(options)) {\n"
" if (name !== '__proto__' && get(option, 'default') !== undefined && result.values[name] === undefined) result.values[name] = get(option, 'default');\n"
" }\n"
" return result;\n"
" };\n"
" const toUSVString = (s) => String(s).replace(/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g, \"�\");\n"
+25 -14
View File
@@ -2357,19 +2357,30 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) {
break;
}
case SCR_DYN_STR: {
const ScrStr *sv = cb->v.str;
char insp[32];
size_t n = 0;
insp[n++] = '\'';
for (size_t i = 0; i < sv->len && n < 28; i++) insp[n++] = sv->data[i];
if (sv->len + 2 > 28) {
n = 25;
memcpy(insp + n, "...", 3);
n += 3;
} else {
insp[n++] = '\'';
ScrStr *value = scr_str_retain(cb->v.str);
if (scr_str_utf16_len(value) > 28) {
ScrStr *prefix = scr_str_slice(value, 0, 25);
ScrStr *dots = scr_str_new("...", 3);
scr_str_release(value);
value = scr_str_concat(prefix, dots);
scr_str_release(prefix);
scr_str_release(dots);
}
snprintf(detail, cap, "type string (%.*s)", (int)n, insp);
ScrJsonBuf shown;
scr_jb_init(&shown);
scr_jb_puts(&shown, "type string (");
if (memchr(value->data, '\'', value->len)) {
scr_jb_put_json_str(&shown, value);
} else {
scr_jb_putc(&shown, '\'');
scr_jb_put_str(&shown, value);
scr_jb_putc(&shown, '\'');
}
scr_jb_putc(&shown, ')');
ScrStr *text = scr_jb_finish(&shown);
snprintf(detail, cap, "%.*s", (int)text->len, text->data);
scr_str_release(text);
scr_str_release(value);
break;
}
default: d = "an instance of Object"; break;
@@ -2391,7 +2402,7 @@ void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const Scr
}
void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got) {
char detail[64];
char detail[256];
const char *d = scr_dyn_specific_type(got, detail, sizeof detail);
ScrJsonBuf b;
scr_jb_init(&b);
@@ -2412,7 +2423,7 @@ void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrD
* wording on the name, but every property-path caller here knows it is
* one). Same runtime-rendered Received tail; always throws catchably. */
void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn *got) {
char detail[64];
char detail[256];
const char *d = scr_dyn_specific_type(got, detail, sizeof detail);
ScrJsonBuf b;
scr_jb_init(&b);
+74
View File
@@ -0,0 +1,74 @@
import { parseArgs } from "node:util";
/** @param {any} config */
function show(config) {
try {
const result = parseArgs(config);
console.log(JSON.stringify(result));
console.log("null-prototype", Object.getPrototypeOf(result.values) === null);
} catch (error) {
console.log(error.name, error.code, error.message);
}
}
function report() {
const options = {
alpha: { type: "boolean", short: "a" },
beta: { type: "boolean", short: "b" },
name: { type: "string", short: "n" },
flag: { type: "boolean", short: "f", multiple: true },
};
const argsCases = [
[], ["-abnhello", "--name=last"], ["-abn", "hello", "tail"],
["--name", "-dash"], ["-n", "--flag"], ["--name=-dash"], ["-n-dash"],
["--name"], ["-n"], ["--flag=yes"], ["--unknown"], ["--unknown=value"],
["--", "--unknown"], ["-"], ["--flag", "--no-flag", "--flag"],
["--no-unknown"], ["--=odd"], ["--__proto__=x", "--constructor=y"],
];
for (const args of argsCases) {
for (const strict of [true, false]) {
for (const allowPositionals of [true, false]) {
show({ args, options, strict, allowPositionals, allowNegative: true, tokens: true });
}
}
}
for (const options of [
{ "no-x": { type: "boolean" }, x: { type: "boolean", multiple: true } },
{ x: { type: "boolean", multiple: true, default: [true] } },
{ x: { type: "string", default: "default" } },
{ "__proto__": null, constructor: { type: "boolean", default: true } },
]) show({ args: ["--no-x"], options, allowNegative: true, strict: false, tokens: true });
for (const config of [
null,
{ args: [], strict: "has'quote" }, { args: [], strict: "line\nend" },
{ args: [], strict: "x".repeat(28) }, { args: [], strict: "x".repeat(29) },
{ args: [], options: { x: { type: "invalid".repeat(8) } } },
{ args: "bad", strict: 1 }, { args: [], strict: 1 }, { args: [], allowPositionals: 1 },
{ args: [], tokens: "yes" }, { args: [], allowNegative: 0 }, { args: [], options: [] },
{ args: [], options: { x: null } }, { args: [], options: { x: {} } },
{ args: [], options: { x: { type: "number" } } },
{ args: [], options: { x: { type: "string", short: "ab" } } },
{ args: [], options: { x: { type: "string", short: undefined } } },
{ args: [], options: { x: { type: "boolean", multiple: null } } },
{ args: [], options: { x: { type: "string", default: 2 } } },
{ args: [], options: { x: { type: "boolean", multiple: true, default: [true, 2] } } },
{ args: [], options: { x: { type: "string", multiple: true, default: "bad" } } },
{ args: ["--unknown", null] }, { args: ["--", null], strict: false },
{ args: ["--name", null], options }, { args: [1, false, {}], strict: false },
{ args: [], strict: null, tokens: null, allowNegative: null, options: null },
]) show(config);
show(Object.assign(Object.create({ strict: false, options: { inherited: { type: "boolean" } } }), { args: ["--inherited"] }));
show({ args: ["--inherited"], options: Object.create({ inherited: { type: "boolean" } }) });
show({ args: [], options: { x: Object.create({ type: "boolean" }) } });
show({ args: [], options: { x: Object.assign(Object.create({ multiple: true, default: ["ignored"] }), { type: "string" }) } });
const protoOptions = Object.create(null);
protoOptions.__proto__ = { type: "string", default: "ignored" };
show({ args: ["--__proto__=ignored"], options: protoOptions, tokens: true });
const defaults = ["initial"];
const result = parseArgs({ args: [], options: { name: { type: "string", multiple: true, default: defaults } } });
console.log("default-identity", result.values.name === defaults);
defaults.push("updated");
console.log("default-live", JSON.stringify(result));
}
report();
+4
View File
@@ -0,0 +1,4 @@
// @dynamic
// @stderr
import { report } from "node24-parseargs-fixture";
report();
@@ -0,0 +1 @@
{"type":"module"}
+1
View File
@@ -0,0 +1 @@
export function report(): void;
+75
View File
@@ -0,0 +1,75 @@
import { parseArgs } from "node:util";
/** @param {any} config */
function show(config) {
try {
const result = parseArgs(config);
console.log(JSON.stringify(result));
console.log("null-prototype", Object.getPrototypeOf(result.values) === null);
} catch (error) {
console.log(error.name, error.code, error.message);
}
}
export function report() {
const options = {
alpha: { type: "boolean", short: "a" },
beta: { type: "boolean", short: "b" },
name: { type: "string", short: "n" },
flag: { type: "boolean", short: "f", multiple: true },
};
const argsCases = [
[], ["-abnhello", "--name=last"], ["-abn", "hello", "tail"],
["--name", "-dash"], ["-n", "--flag"], ["--name=-dash"], ["-n-dash"],
["--name"], ["-n"], ["--flag=yes"], ["--unknown"], ["--unknown=value"],
["--", "--unknown"], ["-"], ["--flag", "--no-flag", "--flag"],
["--no-unknown"], ["--=odd"], ["--__proto__=x", "--constructor=y"],
];
for (const args of argsCases) {
for (const strict of [true, false]) {
for (const allowPositionals of [true, false]) {
show({ args, options, strict, allowPositionals, allowNegative: true, tokens: true });
}
}
}
for (const options of [
{ "no-x": { type: "boolean" }, x: { type: "boolean", multiple: true } },
{ x: { type: "boolean", multiple: true, default: [true] } },
{ x: { type: "string", default: "default" } },
{ "__proto__": null, constructor: { type: "boolean", default: true } },
]) show({ args: ["--no-x"], options, allowNegative: true, strict: false, tokens: true });
for (const config of [
null,
{ args: [], strict: "has'quote" }, { args: [], strict: "line\nend" },
{ args: [], strict: "x".repeat(28) }, { args: [], strict: "x".repeat(29) },
{ args: [], options: { x: { type: "invalid".repeat(8) } } },
{ args: "bad", strict: 1 }, { args: [], strict: 1 }, { args: [], allowPositionals: 1 },
{ args: [], tokens: "yes" }, { args: [], allowNegative: 0 }, { args: [], options: [] },
{ args: [], options: { x: null } }, { args: [], options: { x: {} } },
{ args: [], options: { x: { type: "number" } } },
{ args: [], options: { x: { type: "string", short: "ab" } } },
{ args: [], options: { x: { type: "string", short: undefined } } },
{ args: [], options: { x: { type: "boolean", multiple: null } } },
{ args: [], options: { x: { type: "string", default: 2 } } },
{ args: [], options: { x: { type: "boolean", multiple: true, default: [true, 2] } } },
{ args: [], options: { x: { type: "string", multiple: true, default: "bad" } } },
{ args: ["--unknown", null] }, { args: ["--", null], strict: false },
{ args: ["--name", null], options }, { args: [1, false, {}], strict: false },
{ args: [], strict: null, tokens: null, allowNegative: null, options: null },
]) show(config);
show(Object.assign(Object.create({ strict: false, options: { inherited: { type: "boolean" } } }), { args: ["--inherited"] }));
show({ args: ["--inherited"], options: Object.create({ inherited: { type: "boolean" } }) });
show({ args: [], options: { x: Object.create({ type: "boolean" }) } });
show({ args: [], options: { x: Object.assign(Object.create({ multiple: true, default: ["ignored"] }), { type: "string" }) } });
const protoOptions = Object.create(null);
protoOptions.__proto__ = { type: "string", default: "ignored" };
show({ args: ["--__proto__=ignored"], options: protoOptions, tokens: true });
const argumentsWithIterator = ["--name=actual"];
argumentsWithIterator[Symbol.iterator] = function* () { yield "--unknown"; };
show({ args: argumentsWithIterator, options, tokens: true });
const defaults = ["initial"];
const result = parseArgs({ args: [], options: { name: { type: "string", multiple: true, default: defaults } } });
console.log("default-identity", result.values.name === defaults);
defaults.push("updated");
console.log("default-live", JSON.stringify(result));
}
@@ -0,0 +1 @@
{"name":"node24-parseargs-fixture","version":"1.0.0","type":"module","main":"index.js","types":"index.d.ts"}