@optique/core 0.10.7 → 1.0.0-dev.1116

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.
Files changed (56) hide show
  1. package/README.md +4 -6
  2. package/dist/annotations.cjs +209 -1
  3. package/dist/annotations.d.cts +78 -1
  4. package/dist/annotations.d.ts +78 -1
  5. package/dist/annotations.js +201 -1
  6. package/dist/completion.cjs +194 -52
  7. package/dist/completion.js +194 -52
  8. package/dist/constructs.cjs +310 -78
  9. package/dist/constructs.d.cts +525 -644
  10. package/dist/constructs.d.ts +525 -644
  11. package/dist/constructs.js +311 -79
  12. package/dist/context.cjs +43 -3
  13. package/dist/context.d.cts +113 -5
  14. package/dist/context.d.ts +113 -5
  15. package/dist/context.js +41 -3
  16. package/dist/dependency.cjs +172 -66
  17. package/dist/dependency.d.cts +22 -2
  18. package/dist/dependency.d.ts +22 -2
  19. package/dist/dependency.js +172 -66
  20. package/dist/doc.cjs +46 -1
  21. package/dist/doc.d.cts +24 -0
  22. package/dist/doc.d.ts +24 -0
  23. package/dist/doc.js +46 -1
  24. package/dist/facade.cjs +702 -322
  25. package/dist/facade.d.cts +124 -190
  26. package/dist/facade.d.ts +124 -190
  27. package/dist/facade.js +703 -323
  28. package/dist/index.cjs +5 -0
  29. package/dist/index.d.cts +5 -5
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +3 -3
  32. package/dist/message.cjs +7 -4
  33. package/dist/message.js +7 -4
  34. package/dist/mode-dispatch.cjs +23 -1
  35. package/dist/mode-dispatch.d.cts +55 -0
  36. package/dist/mode-dispatch.d.ts +55 -0
  37. package/dist/mode-dispatch.js +21 -1
  38. package/dist/modifiers.cjs +210 -55
  39. package/dist/modifiers.js +211 -56
  40. package/dist/parser.cjs +80 -47
  41. package/dist/parser.d.cts +18 -3
  42. package/dist/parser.d.ts +18 -3
  43. package/dist/parser.js +82 -50
  44. package/dist/primitives.cjs +102 -37
  45. package/dist/primitives.d.cts +81 -24
  46. package/dist/primitives.d.ts +81 -24
  47. package/dist/primitives.js +103 -39
  48. package/dist/usage.cjs +88 -6
  49. package/dist/usage.d.cts +51 -13
  50. package/dist/usage.d.ts +51 -13
  51. package/dist/usage.js +85 -7
  52. package/dist/valueparser.cjs +391 -106
  53. package/dist/valueparser.d.cts +62 -10
  54. package/dist/valueparser.d.ts +62 -10
  55. package/dist/valueparser.js +391 -106
  56. package/package.json +10 -1
@@ -14,56 +14,129 @@ function isValueParser(object) {
14
14
  * Implementation of the choice parser for both string and number types.
15
15
  */
16
16
  function choice(choices, options = {}) {
17
+ if (choices.length < 1) throw new TypeError("Expected at least one choice, but got an empty array.");
18
+ if (choices.some((c) => c === "")) throw new TypeError("Empty strings are not allowed as choices.");
19
+ for (const c of choices) if (typeof c !== "string" && typeof c !== "number") throw new TypeError(`Expected every choice to be a string or number, but got ${typeof c}.`);
20
+ const isNumber = typeof choices[0] === "number";
21
+ for (const c of choices) if (isNumber ? typeof c !== "number" : typeof c !== "string") throw new TypeError(`Expected every choice to be the same type, but got both ${isNumber ? "number" : "string"} and ${typeof c}.`);
22
+ if (isNumber && choices.some((v) => Number.isNaN(v))) throw new TypeError("NaN is not allowed in number choices.");
17
23
  const metavar = options.metavar ?? "TYPE";
18
24
  ensureNonEmptyString(metavar);
19
- const isNumberChoice = choices.length > 0 && typeof choices[0] === "number";
20
- if (isNumberChoice) {
21
- const numberChoices = choices;
22
- const numberOptions = options;
25
+ if (isNumber) {
26
+ const numberChoices = (() => {
27
+ const seen = /* @__PURE__ */ new Set();
28
+ let hasPositiveZero = false;
29
+ let hasNegativeZero = false;
30
+ const result = [];
31
+ for (const v of choices) {
32
+ if (Object.is(v, -0)) {
33
+ if (hasNegativeZero) continue;
34
+ hasNegativeZero = true;
35
+ } else if (Object.is(v, 0)) {
36
+ if (hasPositiveZero) continue;
37
+ hasPositiveZero = true;
38
+ } else {
39
+ if (seen.has(v)) continue;
40
+ seen.add(v);
41
+ }
42
+ result.push(v);
43
+ }
44
+ return result;
45
+ })();
46
+ const numberInvalidChoice = options.errors?.invalidChoice;
47
+ const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
48
+ const frozenNumberChoices = Object.freeze(numberChoices);
23
49
  return {
24
50
  $mode: "sync",
25
51
  metavar,
26
- choices,
52
+ choices: frozenNumberChoices,
27
53
  parse(input) {
28
- const parsed = Number(input);
29
- if (Number.isNaN(parsed)) return {
30
- success: false,
31
- error: formatNumberChoiceError(input, numberChoices, numberOptions)
32
- };
33
- const index = numberChoices.indexOf(parsed);
34
- if (index < 0) return {
35
- success: false,
36
- error: formatNumberChoiceError(input, numberChoices, numberOptions)
37
- };
38
- return {
54
+ const index = numberStrings.indexOf(input);
55
+ if (index >= 0) return {
39
56
  success: true,
40
57
  value: numberChoices[index]
41
58
  };
59
+ if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(input)) {
60
+ const parsed = Number(input);
61
+ if (Number.isFinite(parsed)) {
62
+ const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
63
+ const normalizedInput = normalizeDecimal(input);
64
+ const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
65
+ if (normalizedInput === normalizedCanonical) {
66
+ const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
67
+ if (fallbackIndex >= 0) return {
68
+ success: true,
69
+ value: numberChoices[fallbackIndex]
70
+ };
71
+ }
72
+ if (parsed === 0 && normalizedInput.replace(/^-/, "") === "0" && !numberChoices.some((v) => Object.is(v, -0))) {
73
+ const zeroIndex = numberChoices.indexOf(0);
74
+ if (zeroIndex >= 0) return {
75
+ success: true,
76
+ value: numberChoices[zeroIndex]
77
+ };
78
+ }
79
+ }
80
+ }
81
+ if (/^[+-]?(\d+\.?\d*|\.\d+)[eE][+-]?\d+$/.test(input)) {
82
+ const parsed = Number(input);
83
+ if (Number.isFinite(parsed)) {
84
+ const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
85
+ if (/[eE]/.test(canonical)) {
86
+ const normalizedInput = normalizeDecimal(expandScientific(input));
87
+ const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
88
+ if (normalizedInput === normalizedCanonical) {
89
+ const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
90
+ if (fallbackIndex >= 0) return {
91
+ success: true,
92
+ value: numberChoices[fallbackIndex]
93
+ };
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return {
99
+ success: false,
100
+ error: formatNumberChoiceError(input, numberChoices, numberChoices, numberInvalidChoice)
101
+ };
42
102
  },
43
103
  format(value) {
44
- return String(value);
104
+ return Object.is(value, -0) ? "-0" : String(value);
45
105
  },
46
106
  suggest(prefix) {
47
- return numberChoices.map((value) => String(value)).filter((valueStr) => valueStr.startsWith(prefix)).map((valueStr) => ({
107
+ return numberStrings.filter((valueStr, i) => !Number.isNaN(numberChoices[i]) && valueStr.startsWith(prefix)).map((valueStr) => ({
48
108
  kind: "literal",
49
109
  text: valueStr
50
110
  }));
51
111
  }
52
112
  };
53
113
  }
54
- const stringChoices = choices;
114
+ const stringChoices = Object.freeze([...new Set(choices)]);
55
115
  const stringOptions = options;
56
- const normalizedValues = stringOptions.caseInsensitive ? stringChoices.map((v) => v.toLowerCase()) : stringChoices;
116
+ if (stringOptions.caseInsensitive !== void 0 && typeof stringOptions.caseInsensitive !== "boolean") throw new TypeError(`Expected caseInsensitive to be a boolean, but got ${typeof stringOptions.caseInsensitive}: ${String(stringOptions.caseInsensitive)}.`);
117
+ const caseInsensitive = stringOptions.caseInsensitive ?? false;
118
+ const normalizedValues = caseInsensitive ? stringChoices.map((v) => v.toLowerCase()) : stringChoices;
119
+ if (caseInsensitive) {
120
+ const seen = /* @__PURE__ */ new Map();
121
+ for (let i = 0; i < stringChoices.length; i++) {
122
+ const nv = normalizedValues[i];
123
+ const original = stringChoices[i];
124
+ const prev = seen.get(nv);
125
+ if (prev !== void 0 && prev !== original) throw new TypeError(`Ambiguous choices for case-insensitive matching: ${JSON.stringify(prev)} and ${JSON.stringify(original)} both normalize to ${JSON.stringify(nv)}.`);
126
+ seen.set(nv, original);
127
+ }
128
+ }
129
+ const stringInvalidChoice = stringOptions.errors?.invalidChoice;
57
130
  return {
58
131
  $mode: "sync",
59
132
  metavar,
60
- choices,
133
+ choices: stringChoices,
61
134
  parse(input) {
62
- const normalizedInput = stringOptions.caseInsensitive ? input.toLowerCase() : input;
135
+ const normalizedInput = caseInsensitive ? input.toLowerCase() : input;
63
136
  const index = normalizedValues.indexOf(normalizedInput);
64
137
  if (index < 0) return {
65
138
  success: false,
66
- error: formatStringChoiceError(input, stringChoices, stringOptions)
139
+ error: formatStringChoiceError(input, stringChoices, stringInvalidChoice)
67
140
  };
68
141
  return {
69
142
  success: true,
@@ -74,9 +147,9 @@ function choice(choices, options = {}) {
74
147
  return String(value);
75
148
  },
76
149
  suggest(prefix) {
77
- const normalizedPrefix = stringOptions.caseInsensitive ? prefix.toLowerCase() : prefix;
150
+ const normalizedPrefix = caseInsensitive ? prefix.toLowerCase() : prefix;
78
151
  return stringChoices.filter((value) => {
79
- const normalizedValue = stringOptions.caseInsensitive ? value.toLowerCase() : value;
152
+ const normalizedValue = caseInsensitive ? value.toLowerCase() : value;
80
153
  return normalizedValue.startsWith(normalizedPrefix);
81
154
  }).map((value) => ({
82
155
  kind: "literal",
@@ -86,24 +159,73 @@ function choice(choices, options = {}) {
86
159
  };
87
160
  }
88
161
  /**
162
+ * Expands a numeric string in scientific notation (e.g., `"1e+21"`,
163
+ * `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
164
+ * Used for both canonical `String(number)` output and user input.
165
+ * Returns the input unchanged if it does not contain scientific notation.
166
+ */
167
+ function expandScientific(s) {
168
+ const match = /^([+-]?)(\d+\.?\d*|\.\d+)[eE]([+-]?\d+)$/.exec(s);
169
+ if (!match) return s;
170
+ const [, rawSign, mantissa, expStr] = match;
171
+ const sign = rawSign === "-" ? "-" : "";
172
+ const exp = parseInt(expStr, 10);
173
+ const dotPos = mantissa.indexOf(".");
174
+ const digits = mantissa.replace(".", "");
175
+ const intLen = dotPos >= 0 ? dotPos : digits.length;
176
+ const newIntLen = intLen + exp;
177
+ let result;
178
+ if (newIntLen >= digits.length) result = digits + "0".repeat(newIntLen - digits.length);
179
+ else if (newIntLen <= 0) result = "0." + "0".repeat(-newIntLen) + digits;
180
+ else result = digits.slice(0, newIntLen) + "." + digits.slice(newIntLen);
181
+ return sign + result;
182
+ }
183
+ /**
184
+ * Normalizes a plain decimal string by stripping leading zeros from the
185
+ * integer part and trailing zeros from the fractional part, so that two
186
+ * strings representing the same mathematical value compare as equal.
187
+ */
188
+ function normalizeDecimal(s) {
189
+ let sign = "";
190
+ let str = s;
191
+ if (str.startsWith("-") || str.startsWith("+")) {
192
+ if (str[0] === "-") sign = "-";
193
+ str = str.slice(1);
194
+ }
195
+ const dot = str.indexOf(".");
196
+ let int;
197
+ let frac;
198
+ if (dot >= 0) {
199
+ int = str.slice(0, dot);
200
+ frac = str.slice(dot + 1);
201
+ } else {
202
+ int = str;
203
+ frac = "";
204
+ }
205
+ int = int.replace(/^0+/, "") || "0";
206
+ frac = frac.replace(/0+$/, "");
207
+ return sign + (frac ? int + "." + frac : int);
208
+ }
209
+ /**
89
210
  * Formats error message for string choice parser.
90
211
  */
91
- function formatStringChoiceError(input, choices, options) {
92
- if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, choices) : options.errors.invalidChoice;
212
+ function formatStringChoiceError(input, choices, invalidChoice) {
213
+ if (invalidChoice) return typeof invalidChoice === "function" ? invalidChoice(input, choices) : invalidChoice;
93
214
  return formatDefaultChoiceError(input, choices);
94
215
  }
95
216
  /**
96
217
  * Formats error message for number choice parser.
97
218
  */
98
- function formatNumberChoiceError(input, choices, options) {
99
- if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, choices) : options.errors.invalidChoice;
100
- return formatDefaultChoiceError(input, choices);
219
+ function formatNumberChoiceError(input, validChoices, allChoices, invalidChoice) {
220
+ if (invalidChoice) return typeof invalidChoice === "function" ? invalidChoice(input, validChoices) : invalidChoice;
221
+ return formatDefaultChoiceError(input, allChoices);
101
222
  }
102
223
  /**
103
224
  * Formats default error message for choice parser.
104
225
  */
105
226
  function formatDefaultChoiceError(input, choices) {
106
- const choiceStrings = choices.map((c) => String(c));
227
+ const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
228
+ if (choiceStrings.length === 0 && choices.length > 0) return message`No valid choices are configured, but got ${input}.`;
107
229
  return message`Expected one of ${valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
108
230
  }
109
231
  /**
@@ -119,18 +241,31 @@ function formatDefaultChoiceError(input, choices) {
119
241
  * @param options Configuration options for the string parser.
120
242
  * @returns A {@link ValueParser} that parses strings according to the
121
243
  * specified options.
244
+ * @throws {TypeError} If `options.pattern` is provided but is not a
245
+ * `RegExp` instance.
122
246
  */
123
247
  function string(options = {}) {
248
+ if (options.pattern != null && !(options.pattern instanceof RegExp)) throw new TypeError(`Expected pattern to be a RegExp, but got: ${Object.prototype.toString.call(options.pattern)}`);
124
249
  const metavar = options.metavar ?? "STRING";
125
250
  ensureNonEmptyString(metavar);
251
+ const patternSource = options.pattern?.source ?? null;
252
+ const patternFlags = options.pattern?.flags ?? null;
253
+ const patternMismatch = options.errors?.patternMismatch;
126
254
  return {
127
255
  $mode: "sync",
128
256
  metavar,
129
257
  parse(input) {
130
- if (options.pattern != null && !options.pattern.test(input)) return {
131
- success: false,
132
- error: options.errors?.patternMismatch ? typeof options.errors.patternMismatch === "function" ? options.errors.patternMismatch(input, options.pattern) : options.errors.patternMismatch : message`Expected a string matching pattern ${text(options.pattern.source)}, but got ${input}.`
133
- };
258
+ if (patternSource != null && patternFlags != null) {
259
+ const pattern = new RegExp(patternSource, patternFlags);
260
+ if (pattern.test(input)) return {
261
+ success: true,
262
+ value: input
263
+ };
264
+ return {
265
+ success: false,
266
+ error: patternMismatch ? typeof patternMismatch === "function" ? patternMismatch(input, pattern) : patternMismatch : message`Expected a string matching pattern ${text(patternSource)}, but got ${input}.`
267
+ };
268
+ }
134
269
  return {
135
270
  success: true,
136
271
  value: input
@@ -173,8 +308,16 @@ function string(options = {}) {
173
308
  * @param options Configuration options specifying the type and constraints.
174
309
  * @returns A {@link ValueParser} that converts string input to the specified
175
310
  * integer type.
311
+ * @throws {TypeError} If `options.type` is provided but is neither `"number"`
312
+ * nor `"bigint"`.
176
313
  */
177
314
  function integer(options) {
315
+ if (options?.type !== void 0 && options.type !== "number" && options.type !== "bigint") throw new TypeError(`Expected type to be "number" or "bigint", but got: ${String(options.type)}.`);
316
+ if (options?.type !== "bigint") {
317
+ if (options?.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
318
+ if (options?.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
319
+ }
320
+ if (options?.min != null && options?.max != null && options.min > options.max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${options.min} and max: ${options.max}.`);
178
321
  if (options?.type === "bigint") {
179
322
  const metavar$1 = options.metavar ?? "INTEGER";
180
323
  ensureNonEmptyString(metavar$1);
@@ -182,16 +325,11 @@ function integer(options) {
182
325
  $mode: "sync",
183
326
  metavar: metavar$1,
184
327
  parse(input) {
185
- let value;
186
- try {
187
- value = BigInt(input);
188
- } catch (e) {
189
- if (e instanceof SyntaxError) return {
190
- success: false,
191
- error: options.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
192
- };
193
- throw e;
194
- }
328
+ if (!input.match(/^-?\d+$/)) return {
329
+ success: false,
330
+ error: options.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
331
+ };
332
+ const value = BigInt(input);
195
333
  if (options.min != null && value < options.min) return {
196
334
  success: false,
197
335
  error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, options.min) : options.errors.belowMinimum : message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
@@ -212,6 +350,15 @@ function integer(options) {
212
350
  }
213
351
  const metavar = options?.metavar ?? "INTEGER";
214
352
  ensureNonEmptyString(metavar);
353
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
354
+ const minSafe = BigInt(Number.MIN_SAFE_INTEGER);
355
+ const unsafeIntegerError = options?.errors?.unsafeInteger;
356
+ function makeUnsafeIntegerError(input) {
357
+ return {
358
+ success: false,
359
+ error: unsafeIntegerError ? typeof unsafeIntegerError === "function" ? unsafeIntegerError(input) : unsafeIntegerError : message`Expected a safe integer between ${text(Number.MIN_SAFE_INTEGER.toLocaleString("en"))} and ${text(Number.MAX_SAFE_INTEGER.toLocaleString("en"))}, but got ${input}. Use type: "bigint" for large values.`
360
+ };
361
+ }
215
362
  return {
216
363
  $mode: "sync",
217
364
  metavar,
@@ -220,7 +367,14 @@ function integer(options) {
220
367
  success: false,
221
368
  error: options?.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
222
369
  };
223
- const value = Number.parseInt(input);
370
+ let n;
371
+ try {
372
+ n = BigInt(input);
373
+ } catch {
374
+ return makeUnsafeIntegerError(input);
375
+ }
376
+ if (n > maxSafe || n < minSafe) return makeUnsafeIntegerError(input);
377
+ const value = Number(input);
224
378
  if (options?.min != null && value < options.min) return {
225
379
  success: false,
226
380
  error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, options.min) : options.errors.belowMinimum : message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
@@ -249,6 +403,9 @@ function integer(options) {
249
403
  * numbers.
250
404
  */
251
405
  function float(options = {}) {
406
+ if (options.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
407
+ if (options.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
408
+ if (options.min != null && options.max != null && options.min > options.max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${options.min} and max: ${options.max}.`);
252
409
  const floatRegex = /^[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.\d+))(?:[eE][+-]?\d+)?$/;
253
410
  const metavar = options.metavar ?? "NUMBER";
254
411
  ensureNonEmptyString(metavar);
@@ -256,6 +413,10 @@ function float(options = {}) {
256
413
  $mode: "sync",
257
414
  metavar,
258
415
  parse(input) {
416
+ const invalidNumber = (i) => ({
417
+ success: false,
418
+ error: options.errors?.invalidNumber ? typeof options.errors.invalidNumber === "function" ? options.errors.invalidNumber(i) : options.errors.invalidNumber : message`Expected a valid number, but got ${i}.`
419
+ });
259
420
  let value;
260
421
  const lowerInput = input.toLowerCase();
261
422
  if (lowerInput === "nan" && options.allowNaN) value = NaN;
@@ -263,14 +424,9 @@ function float(options = {}) {
263
424
  else if (lowerInput === "-infinity" && options.allowInfinity) value = -Infinity;
264
425
  else if (floatRegex.test(input)) {
265
426
  value = Number(input);
266
- if (Number.isNaN(value)) return {
267
- success: false,
268
- error: options.errors?.invalidNumber ? typeof options.errors.invalidNumber === "function" ? options.errors.invalidNumber(input) : options.errors.invalidNumber : message`Expected a valid number, but got ${input}.`
269
- };
270
- } else return {
271
- success: false,
272
- error: options.errors?.invalidNumber ? typeof options.errors.invalidNumber === "function" ? options.errors.invalidNumber(input) : options.errors.invalidNumber : message`Expected a valid number, but got ${input}.`
273
- };
427
+ if (!Number.isFinite(value) && !options.allowInfinity) return invalidNumber(input);
428
+ if (Number.isNaN(value)) return invalidNumber(input);
429
+ } else return invalidNumber(input);
274
430
  if (options.min != null && value < options.min) return {
275
431
  success: false,
276
432
  error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, options.min) : options.errors.belowMinimum : message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
@@ -299,21 +455,24 @@ function float(options = {}) {
299
455
  * @returns A {@link ValueParser} that converts string input to `URL` objects.
300
456
  */
301
457
  function url(options = {}) {
302
- const allowedProtocols = options.allowedProtocols?.map((p) => p.toLowerCase());
458
+ const originalProtocols = options.allowedProtocols != null ? Object.freeze([...options.allowedProtocols]) : void 0;
459
+ const allowedProtocols = options.allowedProtocols != null ? Object.freeze(options.allowedProtocols.map((p) => p.toLowerCase())) : void 0;
303
460
  const metavar = options.metavar ?? "URL";
304
461
  ensureNonEmptyString(metavar);
462
+ const invalidUrl = options.errors?.invalidUrl;
463
+ const disallowedProtocol = options.errors?.disallowedProtocol;
305
464
  return {
306
465
  $mode: "sync",
307
466
  metavar,
308
467
  parse(input) {
309
468
  if (!URL.canParse(input)) return {
310
469
  success: false,
311
- error: options.errors?.invalidUrl ? typeof options.errors.invalidUrl === "function" ? options.errors.invalidUrl(input) : options.errors.invalidUrl : message`Invalid URL: ${input}.`
470
+ error: invalidUrl ? typeof invalidUrl === "function" ? invalidUrl(input) : invalidUrl : message`Invalid URL: ${input}.`
312
471
  };
313
472
  const url$1 = new URL(input);
314
473
  if (allowedProtocols != null && !allowedProtocols.includes(url$1.protocol)) return {
315
474
  success: false,
316
- error: options.errors?.disallowedProtocol ? typeof options.errors.disallowedProtocol === "function" ? options.errors.disallowedProtocol(url$1.protocol, options.allowedProtocols) : options.errors.disallowedProtocol : message`URL protocol ${url$1.protocol} is not allowed. Allowed protocols: ${allowedProtocols.join(", ")}.`
475
+ error: disallowedProtocol ? typeof disallowedProtocol === "function" ? disallowedProtocol(url$1.protocol, originalProtocols) : disallowedProtocol : message`URL protocol ${url$1.protocol} is not allowed. Allowed protocols: ${allowedProtocols.join(", ")}.`
317
476
  };
318
477
  return {
319
478
  success: true,
@@ -367,7 +526,7 @@ function locale(options = {}) {
367
526
  };
368
527
  },
369
528
  format(value) {
370
- return value.baseName;
529
+ return value.toString();
371
530
  },
372
531
  *suggest(prefix) {
373
532
  const commonLocales = [
@@ -617,24 +776,27 @@ function uuid(options = {}) {
617
776
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
618
777
  const metavar = options.metavar ?? "UUID";
619
778
  ensureNonEmptyString(metavar);
779
+ const allowedVersions = options.allowedVersions != null ? Object.freeze([...options.allowedVersions]) : null;
780
+ const invalidUuid = options.errors?.invalidUuid;
781
+ const disallowedVersion = options.errors?.disallowedVersion;
620
782
  return {
621
783
  $mode: "sync",
622
784
  metavar,
623
785
  parse(input) {
624
786
  if (!uuidRegex.test(input)) return {
625
787
  success: false,
626
- error: options.errors?.invalidUuid ? typeof options.errors.invalidUuid === "function" ? options.errors.invalidUuid(input) : options.errors.invalidUuid : message`Expected a valid UUID in format ${"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}, but got ${input}.`
788
+ error: invalidUuid ? typeof invalidUuid === "function" ? invalidUuid(input) : invalidUuid : message`Expected a valid UUID in format ${"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}, but got ${input}.`
627
789
  };
628
- if (options.allowedVersions != null && options.allowedVersions.length > 0) {
790
+ if (allowedVersions != null && allowedVersions.length > 0) {
629
791
  const versionChar = input.charAt(14);
630
792
  const version = parseInt(versionChar, 16);
631
- if (!options.allowedVersions.includes(version)) return {
793
+ if (!allowedVersions.includes(version)) return {
632
794
  success: false,
633
- error: options.errors?.disallowedVersion ? typeof options.errors.disallowedVersion === "function" ? options.errors.disallowedVersion(version, options.allowedVersions) : options.errors.disallowedVersion : (() => {
795
+ error: disallowedVersion ? typeof disallowedVersion === "function" ? disallowedVersion(version, allowedVersions) : disallowedVersion : (() => {
634
796
  let expectedVersions = message``;
635
797
  let i = 0;
636
- for (const v of options.allowedVersions) {
637
- expectedVersions = i < 1 ? message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >= options.allowedVersions.length ? message`${expectedVersions}, or ${v.toLocaleString("en")}` : message`${expectedVersions}, ${v.toLocaleString("en")}`;
798
+ for (const v of allowedVersions) {
799
+ expectedVersions = i < 1 ? message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >= allowedVersions.length ? message`${expectedVersions}, or ${v.toLocaleString("en")}` : message`${expectedVersions}, ${v.toLocaleString("en")}`;
638
800
  i++;
639
801
  }
640
802
  return message`Expected UUID version ${expectedVersions}, but got version ${version.toLocaleString("en")}.`;
@@ -686,28 +848,28 @@ function uuid(options = {}) {
686
848
  *
687
849
  * @param options Configuration options specifying the type and constraints.
688
850
  * @returns A {@link ValueParser} that converts string input to port numbers.
851
+ * @throws {TypeError} If `options.type` is provided but is neither `"number"`
852
+ * nor `"bigint"`.
689
853
  * @since 0.10.0
690
854
  */
691
855
  function port(options) {
856
+ if (options?.disallowWellKnown !== void 0 && typeof options.disallowWellKnown !== "boolean") throw new TypeError(`Expected disallowWellKnown to be a boolean, but got ${typeof options.disallowWellKnown}: ${String(options.disallowWellKnown)}.`);
857
+ if (options?.type !== void 0 && options.type !== "number" && options.type !== "bigint") throw new TypeError(`Expected type to be "number" or "bigint", but got: ${String(options.type)}.`);
692
858
  if (options?.type === "bigint") {
693
859
  const metavar$1 = options.metavar ?? "PORT";
694
860
  ensureNonEmptyString(metavar$1);
695
861
  const min$1 = options.min ?? 1n;
696
862
  const max$1 = options.max ?? 65535n;
863
+ if (min$1 > max$1) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${min$1} and max: ${max$1}.`);
697
864
  return {
698
865
  $mode: "sync",
699
866
  metavar: metavar$1,
700
867
  parse(input) {
701
- let value;
702
- try {
703
- value = BigInt(input);
704
- } catch (e) {
705
- if (e instanceof SyntaxError) return {
706
- success: false,
707
- error: options.errors?.invalidPort ? typeof options.errors.invalidPort === "function" ? options.errors.invalidPort(input) : options.errors.invalidPort : message`Expected a valid port number, but got ${input}.`
708
- };
709
- throw e;
710
- }
868
+ if (!input.match(/^-?\d+$/)) return {
869
+ success: false,
870
+ error: options.errors?.invalidPort ? typeof options.errors.invalidPort === "function" ? options.errors.invalidPort(input) : options.errors.invalidPort : message`Expected a valid port number, but got ${input}.`
871
+ };
872
+ const value = BigInt(input);
711
873
  if (value < min$1) return {
712
874
  success: false,
713
875
  error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, min$1) : options.errors.belowMinimum : message`Expected a port number greater than or equal to ${text(min$1.toLocaleString("en"))}, but got ${input}.`
@@ -730,10 +892,13 @@ function port(options) {
730
892
  }
731
893
  };
732
894
  }
895
+ if (options?.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
896
+ if (options?.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
733
897
  const metavar = options?.metavar ?? "PORT";
734
898
  ensureNonEmptyString(metavar);
735
899
  const min = options?.min ?? 1;
736
900
  const max = options?.max ?? 65535;
901
+ if (min > max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${min} and max: ${max}.`);
737
902
  return {
738
903
  $mode: "sync",
739
904
  metavar,
@@ -948,6 +1113,7 @@ function ipv4(options) {
948
1113
  *
949
1114
  * @param options - Options for hostname validation.
950
1115
  * @returns A value parser for hostnames.
1116
+ * @throws {RangeError} If `maxLength` is not a positive integer.
951
1117
  * @since 0.10.0
952
1118
  *
953
1119
  * @example
@@ -971,6 +1137,7 @@ function hostname(options) {
971
1137
  const allowUnderscore = options?.allowUnderscore ?? false;
972
1138
  const allowLocalhost = options?.allowLocalhost ?? true;
973
1139
  const maxLength = options?.maxLength ?? 253;
1140
+ if (!Number.isInteger(maxLength) || maxLength < 1) throw new RangeError("maxLength must be an integer greater than or equal to 1.");
974
1141
  return {
975
1142
  $mode: "sync",
976
1143
  metavar,
@@ -1079,14 +1246,28 @@ function email(options) {
1079
1246
  const allowMultiple = options?.allowMultiple ?? false;
1080
1247
  const allowDisplayName = options?.allowDisplayName ?? false;
1081
1248
  const lowercase = options?.lowercase ?? false;
1082
- const allowedDomains = options?.allowedDomains;
1249
+ const allowedDomains = options?.allowedDomains != null ? Object.freeze([...options.allowedDomains]) : void 0;
1250
+ if (allowedDomains != null) for (let i = 0; i < allowedDomains.length; i++) {
1251
+ const entry = allowedDomains[i];
1252
+ if (typeof entry !== "string") throw new TypeError(`allowedDomains[${i}] must be a string, got ${typeof entry}.`);
1253
+ if (entry !== entry.trim()) throw new TypeError(`allowedDomains[${i}] must not have leading or trailing whitespace: ${JSON.stringify(entry)}`);
1254
+ if (entry.startsWith("@")) throw new TypeError(`allowedDomains[${i}] must not start with "@": ${JSON.stringify(entry)}`);
1255
+ if (entry === "" || !entry.includes(".")) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1256
+ if (entry.startsWith(".") || entry.endsWith(".") || entry.startsWith("-") || entry.endsWith("-")) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1257
+ const labels = entry.split(".");
1258
+ for (const label of labels) if (label.length === 0 || label.length > 63 || label.startsWith("-") || label.endsWith("-") || !/^[a-zA-Z0-9-]+$/.test(label)) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1259
+ if (labels.length === 4 && labels.every((label) => /^[0-9]+$/.test(label))) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1260
+ }
1261
+ const invalidEmail = options?.errors?.invalidEmail;
1262
+ const domainNotAllowed = options?.errors?.domainNotAllowed;
1083
1263
  const atextRegex = /^[a-zA-Z0-9._+-]+$/;
1264
+ const encoder = new TextEncoder();
1084
1265
  function validateEmail(input) {
1085
1266
  const trimmed = input.trim();
1086
1267
  let emailAddr = trimmed;
1087
- if (allowDisplayName && trimmed.includes("<") && trimmed.endsWith(">")) {
1088
- const match = trimmed.match(/<([^>]+)>$/);
1089
- if (match) emailAddr = match[1].trim();
1268
+ if (allowDisplayName) {
1269
+ const displayNameMatch = trimmed.match(/^((?:"(?:[^"\\]|\\.)*"|[^<>"])+)\s*<([^<>]+)>$/);
1270
+ if (displayNameMatch && /\S/.test(displayNameMatch[1].replace(/"((?:[^"\\]|\\.)*)"/g, (_match, inner) => inner))) emailAddr = displayNameMatch[2].trim();
1090
1271
  }
1091
1272
  let atIndex = -1;
1092
1273
  if (emailAddr.startsWith("\"")) {
@@ -1108,6 +1289,7 @@ function email(options) {
1108
1289
  isValidLocal = localParts.length > 0 && localParts.every((part) => part.length > 0 && atextRegex.test(part));
1109
1290
  }
1110
1291
  if (!isValidLocal) return null;
1292
+ if (encoder.encode(localPart).length > 64) return null;
1111
1293
  if (!domain$1 || domain$1.length === 0) return null;
1112
1294
  if (!domain$1.includes(".")) return null;
1113
1295
  if (domain$1.startsWith(".") || domain$1.endsWith(".") || domain$1.startsWith("-") || domain$1.endsWith("-")) return null;
@@ -1117,32 +1299,64 @@ function email(options) {
1117
1299
  if (label.startsWith("-") || label.endsWith("-")) return null;
1118
1300
  if (!/^[a-zA-Z0-9-]+$/.test(label)) return null;
1119
1301
  }
1302
+ if (domainLabels.length === 4 && domainLabels.every((label) => /^[0-9]+$/.test(label))) return null;
1303
+ if (encoder.encode(emailAddr).length > 254) return null;
1120
1304
  const resultEmail = emailAddr;
1121
- return lowercase ? resultEmail.toLowerCase() : resultEmail;
1305
+ if (!lowercase) return resultEmail;
1306
+ const lastAt = resultEmail.lastIndexOf("@");
1307
+ return resultEmail.slice(0, lastAt) + resultEmail.slice(lastAt).toLowerCase();
1308
+ }
1309
+ /**
1310
+ * Splits an input string on commas, respecting quoted segments and
1311
+ * angle-bracket display-name syntax per RFC 5322.
1312
+ */
1313
+ function splitEmails(input) {
1314
+ const result = [];
1315
+ let current = "";
1316
+ let inQuotes = false;
1317
+ let inAngleBrackets = false;
1318
+ let escaped = false;
1319
+ for (const char of input) {
1320
+ if (escaped) escaped = false;
1321
+ else if (char === "\\" && inQuotes) escaped = true;
1322
+ else if (char === "\"" && !inAngleBrackets) {
1323
+ if (inQuotes) inQuotes = false;
1324
+ else if (current.trim() === "") inQuotes = true;
1325
+ } else if (char === "<" && !inQuotes) inAngleBrackets = true;
1326
+ else if (char === ">" && !inQuotes) inAngleBrackets = false;
1327
+ else if (char === "," && !inQuotes && !inAngleBrackets) {
1328
+ result.push(current);
1329
+ current = "";
1330
+ continue;
1331
+ }
1332
+ current += char;
1333
+ }
1334
+ result.push(current);
1335
+ return result;
1122
1336
  }
1123
1337
  return {
1124
1338
  $mode: "sync",
1125
1339
  metavar,
1126
1340
  parse(input) {
1127
1341
  if (allowMultiple) {
1128
- const emails = input.split(",").map((e) => e.trim());
1342
+ const emails = splitEmails(input).map((e) => e.trim());
1129
1343
  const validatedEmails = [];
1130
1344
  for (const email$1 of emails) {
1131
1345
  const validated = validateEmail(email$1);
1132
1346
  if (validated === null) {
1133
- const errorMsg = options?.errors?.invalidEmail;
1347
+ const errorMsg = invalidEmail;
1134
1348
  const msg = typeof errorMsg === "function" ? errorMsg(email$1) : errorMsg ?? message`Expected a valid email address, but got ${email$1}.`;
1135
1349
  return {
1136
1350
  success: false,
1137
1351
  error: msg
1138
1352
  };
1139
1353
  }
1140
- if (allowedDomains && allowedDomains.length > 0) {
1354
+ if (allowedDomains != null) {
1141
1355
  const atIndex = validated.indexOf("@");
1142
1356
  const domain$1 = validated.substring(atIndex + 1).toLowerCase();
1143
1357
  const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
1144
1358
  if (!isAllowed) {
1145
- const errorMsg = options?.errors?.domainNotAllowed;
1359
+ const errorMsg = domainNotAllowed;
1146
1360
  if (typeof errorMsg === "function") return {
1147
1361
  success: false,
1148
1362
  error: errorMsg(validated, allowedDomains)
@@ -1176,19 +1390,19 @@ function email(options) {
1176
1390
  } else {
1177
1391
  const validated = validateEmail(input);
1178
1392
  if (validated === null) {
1179
- const errorMsg = options?.errors?.invalidEmail;
1393
+ const errorMsg = invalidEmail;
1180
1394
  const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid email address, but got ${input}.`;
1181
1395
  return {
1182
1396
  success: false,
1183
1397
  error: msg
1184
1398
  };
1185
1399
  }
1186
- if (allowedDomains && allowedDomains.length > 0) {
1400
+ if (allowedDomains != null) {
1187
1401
  const atIndex = validated.indexOf("@");
1188
1402
  const domain$1 = validated.substring(atIndex + 1).toLowerCase();
1189
1403
  const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
1190
1404
  if (!isAllowed) {
1191
- const errorMsg = options?.errors?.domainNotAllowed;
1405
+ const errorMsg = domainNotAllowed;
1192
1406
  if (typeof errorMsg === "function") return {
1193
1407
  success: false,
1194
1408
  error: errorMsg(validated, allowedDomains)
@@ -1220,7 +1434,7 @@ function email(options) {
1220
1434
  }
1221
1435
  },
1222
1436
  format(value) {
1223
- if (Array.isArray(value)) return value.join(",");
1437
+ if (Array.isArray(value)) return value.join(", ");
1224
1438
  return value;
1225
1439
  }
1226
1440
  };
@@ -1237,6 +1451,8 @@ function email(options) {
1237
1451
  *
1238
1452
  * @param options - Options for socket address validation.
1239
1453
  * @returns A value parser for socket addresses.
1454
+ * @throws {TypeError} If `separator` contains digit characters, since digits
1455
+ * in the separator would cause ambiguous splitting of port input.
1240
1456
  * @since 0.10.0
1241
1457
  *
1242
1458
  * @example
@@ -1257,9 +1473,10 @@ function email(options) {
1257
1473
  * ```
1258
1474
  */
1259
1475
  function socketAddress(options) {
1260
- const metavar = options?.metavar ?? "HOST:PORT";
1261
- ensureNonEmptyString(metavar);
1262
1476
  const separator = options?.separator ?? ":";
1477
+ if (/\p{Nd}/u.test(separator)) throw new TypeError(`Expected separator to not contain digits, but got: ${JSON.stringify(separator)}.`);
1478
+ const metavar = options?.metavar ?? `HOST${separator}PORT`;
1479
+ ensureNonEmptyString(metavar);
1263
1480
  const defaultPort = options?.defaultPort;
1264
1481
  const requirePort = options?.requirePort ?? false;
1265
1482
  const hostType = options?.host?.type ?? "both";
@@ -1361,9 +1578,13 @@ function socketAddress(options) {
1361
1578
  };
1362
1579
  }
1363
1580
  function portRange(options) {
1364
- const metavar = options?.metavar ?? "PORT-PORT";
1365
- ensureNonEmptyString(metavar);
1581
+ if (options?.disallowWellKnown !== void 0 && typeof options.disallowWellKnown !== "boolean") throw new TypeError(`Expected disallowWellKnown to be a boolean, but got ${typeof options.disallowWellKnown}: ${String(options.disallowWellKnown)}.`);
1582
+ if (options?.allowSingle !== void 0 && typeof options.allowSingle !== "boolean") throw new TypeError(`Expected allowSingle to be a boolean, but got ${typeof options.allowSingle}: ${String(options.allowSingle)}.`);
1583
+ if (options?.type !== void 0 && options.type !== "number" && options.type !== "bigint") throw new TypeError(`Expected type to be "number" or "bigint", but got: ${String(options.type)}.`);
1366
1584
  const separator = options?.separator ?? "-";
1585
+ if (/\p{Nd}/u.test(separator)) throw new TypeError(`Expected separator to not contain digits, but got: ${JSON.stringify(separator)}.`);
1586
+ const metavar = options?.metavar ?? `PORT${separator}PORT`;
1587
+ ensureNonEmptyString(metavar);
1367
1588
  const allowSingle = options?.allowSingle ?? false;
1368
1589
  const isBigInt = options?.type === "bigint";
1369
1590
  const portParser = isBigInt ? port({
@@ -1609,6 +1830,13 @@ function macAddress(options) {
1609
1830
  *
1610
1831
  * @param options Parser options for domain validation.
1611
1832
  * @returns A parser that accepts valid domain names as strings.
1833
+ * @throws {RangeError} If `maxLength` is not a positive integer.
1834
+ * @throws {RangeError} If `minLabels` is not a positive integer.
1835
+ * @throws {TypeError} If any `allowedTlds` entry is not a string, is empty,
1836
+ * contains dots, has leading/trailing whitespace, or is not a valid DNS
1837
+ * label.
1838
+ * @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
1839
+ * greater than 2, since non-subdomain domains have exactly 2 labels.
1612
1840
  *
1613
1841
  * @example
1614
1842
  * ``` typescript
@@ -1622,7 +1850,7 @@ function macAddress(options) {
1622
1850
  * option("--root", domain({ allowSubdomains: false }))
1623
1851
  *
1624
1852
  * // Restrict to specific TLDs
1625
- * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
1853
+ * option("--domain", domain({ allowedTlds: ["com", "org", "net"] }))
1626
1854
  *
1627
1855
  * // Normalize to lowercase
1628
1856
  * option("--domain", domain({ lowercase: true }))
@@ -1633,17 +1861,44 @@ function macAddress(options) {
1633
1861
  function domain(options) {
1634
1862
  const metavar = options?.metavar ?? "DOMAIN";
1635
1863
  const allowSubdomains = options?.allowSubdomains ?? true;
1636
- const allowedTLDs = options?.allowedTLDs;
1864
+ const allowedTlds = options?.allowedTlds != null ? Object.freeze([...options.allowedTlds]) : void 0;
1865
+ const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
1866
+ if (allowedTlds !== void 0) for (const [i, tld] of allowedTlds.entries()) {
1867
+ if (typeof tld !== "string") {
1868
+ const actualType = Array.isArray(tld) ? "array" : typeof tld;
1869
+ throw new TypeError(`allowedTlds[${i}] must be a string, but got ${actualType}.`);
1870
+ }
1871
+ if (tld.length === 0) throw new TypeError(`allowedTlds[${i}] must not be an empty string.`);
1872
+ if (tld.includes(".")) throw new TypeError(`allowedTlds[${i}] must not contain dots: ${JSON.stringify(tld)}.`);
1873
+ if (tld !== tld.trim()) throw new TypeError(`allowedTlds[${i}] must not have leading or trailing whitespace: ${JSON.stringify(tld)}.`);
1874
+ if (!labelRegex.test(tld)) throw new TypeError(`allowedTlds[${i}] is not a valid DNS label: ${JSON.stringify(tld)}.`);
1875
+ }
1876
+ const allowedTldsLower = allowedTlds != null ? Object.freeze(allowedTlds.map((t) => t.toLowerCase())) : void 0;
1637
1877
  const minLabels = options?.minLabels ?? 2;
1878
+ const maxLength = options?.maxLength ?? 253;
1638
1879
  const lowercase = options?.lowercase ?? false;
1639
- const errors = options?.errors;
1640
- const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
1880
+ if (!Number.isInteger(maxLength) || maxLength < 1) throw new RangeError("maxLength must be an integer greater than or equal to 1.");
1881
+ if (!Number.isInteger(minLabels) || minLabels < 1) throw new RangeError("minLabels must be an integer greater than or equal to 1.");
1882
+ if (!allowSubdomains && minLabels > 2) throw new TypeError("allowSubdomains: false is incompatible with minLabels > 2, as non-subdomain domains have exactly 2 labels.");
1883
+ const invalidDomain = options?.errors?.invalidDomain;
1884
+ const tooLong = options?.errors?.tooLong;
1885
+ const tooFewLabels = options?.errors?.tooFewLabels;
1886
+ const subdomainsNotAllowed = options?.errors?.subdomainsNotAllowed;
1887
+ const tldNotAllowed = options?.errors?.tldNotAllowed;
1641
1888
  return {
1642
1889
  $mode: "sync",
1643
1890
  metavar,
1644
1891
  parse(input) {
1892
+ if (input.length > maxLength) {
1893
+ const errorMsg = tooLong;
1894
+ const msg = typeof errorMsg === "function" ? errorMsg(input, maxLength) : errorMsg ?? message`Domain ${input} is too long (maximum ${text(maxLength.toString())} characters).`;
1895
+ return {
1896
+ success: false,
1897
+ error: msg
1898
+ };
1899
+ }
1645
1900
  if (input.length === 0 || input.startsWith(".") || input.endsWith(".")) {
1646
- const errorMsg = errors?.invalidDomain;
1901
+ const errorMsg = invalidDomain;
1647
1902
  if (typeof errorMsg === "function") return {
1648
1903
  success: false,
1649
1904
  error: errorMsg(input)
@@ -1668,7 +1923,7 @@ function domain(options) {
1668
1923
  };
1669
1924
  }
1670
1925
  if (input.includes("..")) {
1671
- const errorMsg = errors?.invalidDomain;
1926
+ const errorMsg = invalidDomain;
1672
1927
  if (typeof errorMsg === "function") return {
1673
1928
  success: false,
1674
1929
  error: errorMsg(input)
@@ -1694,7 +1949,32 @@ function domain(options) {
1694
1949
  }
1695
1950
  const labels = input.split(".");
1696
1951
  for (const label of labels) if (!labelRegex.test(label)) {
1697
- const errorMsg = errors?.invalidDomain;
1952
+ const errorMsg = invalidDomain;
1953
+ if (typeof errorMsg === "function") return {
1954
+ success: false,
1955
+ error: errorMsg(input)
1956
+ };
1957
+ const msg = errorMsg ?? [
1958
+ {
1959
+ type: "text",
1960
+ text: "Expected a valid domain name, but got "
1961
+ },
1962
+ {
1963
+ type: "value",
1964
+ value: input
1965
+ },
1966
+ {
1967
+ type: "text",
1968
+ text: "."
1969
+ }
1970
+ ];
1971
+ return {
1972
+ success: false,
1973
+ error: msg
1974
+ };
1975
+ }
1976
+ if (labels.length >= 2 && labels.every((l) => /^[0-9]+$/.test(l))) {
1977
+ const errorMsg = invalidDomain;
1698
1978
  if (typeof errorMsg === "function") return {
1699
1979
  success: false,
1700
1980
  error: errorMsg(input)
@@ -1719,7 +1999,7 @@ function domain(options) {
1719
1999
  };
1720
2000
  }
1721
2001
  if (labels.length < minLabels) {
1722
- const errorMsg = errors?.tooFewLabels;
2002
+ const errorMsg = tooFewLabels;
1723
2003
  if (typeof errorMsg === "function") return {
1724
2004
  success: false,
1725
2005
  error: errorMsg(input, minLabels)
@@ -1744,7 +2024,7 @@ function domain(options) {
1744
2024
  };
1745
2025
  }
1746
2026
  if (!allowSubdomains && labels.length > 2) {
1747
- const errorMsg = errors?.subdomainsNotAllowed;
2027
+ const errorMsg = subdomainsNotAllowed;
1748
2028
  if (typeof errorMsg === "function") return {
1749
2029
  success: false,
1750
2030
  error: errorMsg(input)
@@ -1768,15 +2048,14 @@ function domain(options) {
1768
2048
  error: msg
1769
2049
  };
1770
2050
  }
1771
- if (allowedTLDs !== void 0) {
2051
+ if (allowedTlds !== void 0 && allowedTldsLower !== void 0) {
1772
2052
  const tld = labels[labels.length - 1];
1773
2053
  const tldLower = tld.toLowerCase();
1774
- const allowedTLDsLower = allowedTLDs.map((t) => t.toLowerCase());
1775
- if (!allowedTLDsLower.includes(tldLower)) {
1776
- const errorMsg = errors?.tldNotAllowed;
2054
+ if (!allowedTldsLower.includes(tldLower)) {
2055
+ const errorMsg = tldNotAllowed;
1777
2056
  if (typeof errorMsg === "function") return {
1778
2057
  success: false,
1779
- error: errorMsg(tld, allowedTLDs)
2058
+ error: errorMsg(tld, allowedTlds)
1780
2059
  };
1781
2060
  const msg = errorMsg ?? [
1782
2061
  {
@@ -1789,7 +2068,7 @@ function domain(options) {
1789
2068
  },
1790
2069
  {
1791
2070
  type: "text",
1792
- text: ` is not allowed. Allowed TLDs: ${allowedTLDs.join(", ")}.`
2071
+ text: ` is not allowed. Allowed TLDs: ${allowedTlds.join(", ")}.`
1793
2072
  }
1794
2073
  ];
1795
2074
  return {
@@ -2237,7 +2516,13 @@ function ip(options) {
2237
2516
  * @since 0.10.0
2238
2517
  */
2239
2518
  function cidr(options) {
2519
+ if (options?.minPrefix != null && !Number.isFinite(options.minPrefix)) throw new RangeError(`Expected minPrefix to be a finite number, but got: ${options.minPrefix}`);
2520
+ if (options?.maxPrefix != null && !Number.isFinite(options.maxPrefix)) throw new RangeError(`Expected maxPrefix to be a finite number, but got: ${options.maxPrefix}`);
2521
+ if (options?.minPrefix != null && options?.maxPrefix != null && options.minPrefix > options.maxPrefix) throw new RangeError(`Expected minPrefix to be less than or equal to maxPrefix, but got minPrefix: ${options.minPrefix} and maxPrefix: ${options.maxPrefix}.`);
2240
2522
  const version = options?.version ?? "both";
2523
+ const maxPrefixForVersion = version === 4 ? 32 : version === 6 ? 128 : 128;
2524
+ if (options?.minPrefix != null && (options.minPrefix < 0 || options.minPrefix > maxPrefixForVersion)) throw new RangeError(`Expected minPrefix to be between 0 and ${maxPrefixForVersion} for IPv${version === "both" ? "4/6" : version}, but got minPrefix: ${options.minPrefix}.`);
2525
+ if (options?.maxPrefix != null && (options.maxPrefix < 0 || options.maxPrefix > maxPrefixForVersion)) throw new RangeError(`Expected maxPrefix to be between 0 and ${maxPrefixForVersion} for IPv${version === "both" ? "4/6" : version}, but got maxPrefix: ${options.maxPrefix}.`);
2241
2526
  const minPrefix = options?.minPrefix;
2242
2527
  const maxPrefix = options?.maxPrefix;
2243
2528
  const errors = options?.errors;