@optique/core 1.0.0-dev.1109 → 1.0.0-dev.1122

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.
@@ -41,6 +41,17 @@ function encodePattern(pattern) {
41
41
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
42
  }
43
43
  /**
44
+ * Replaces control characters that would corrupt shell completion protocols.
45
+ * Shell completion formats use tabs as field delimiters and newlines as record
46
+ * delimiters. Null bytes are used as delimiters in zsh's format.
47
+ * @param description The description string to sanitize.
48
+ * @returns The sanitized description with control characters replaced by spaces.
49
+ * @internal
50
+ */
51
+ function sanitizeDescription(description) {
52
+ return description.replace(/[\t\n\r\0]/g, " ");
53
+ }
54
+ /**
44
55
  * The Bash shell completion generator.
45
56
  * @since 0.6.0
46
57
  */
@@ -250,6 +261,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
250
261
  pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
251
262
  has_file_completion=1
252
263
 
264
+ # Enable glob_dots when hidden files are requested so that
265
+ # _files and _directories include dot-prefixed entries
266
+ local __was_glob_dots=0
267
+ [[ -o glob_dots ]] && __was_glob_dots=1
268
+ if [[ "\$hidden" == "1" ]]; then setopt glob_dots; fi
269
+
253
270
  # Use zsh's native file completion
254
271
  case "\$type" in
255
272
  file)
@@ -275,8 +292,8 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
275
292
  ;;
276
293
  esac
277
294
 
278
- # Note: zsh's _files and _directories handle hidden file filtering automatically
279
- # based on the completion context and user settings
295
+ # Restore glob_dots to its previous state
296
+ if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
280
297
  else
281
298
  # Regular literal completion
282
299
  if [[ -n "\$value" ]]; then
@@ -308,12 +325,12 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
308
325
  },
309
326
  *encodeSuggestions(suggestions) {
310
327
  for (const suggestion of suggestions) if (suggestion.kind === "literal") {
311
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
328
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
312
329
  yield `${suggestion.text}\0${description}\0`;
313
330
  } else {
314
331
  const extensions = suggestion.extensions?.join(",") || "";
315
332
  const hidden = suggestion.includeHidden ? "1" : "0";
316
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
333
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
317
334
  const pattern = encodePattern(suggestion.pattern ?? "");
318
335
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
319
336
  }
@@ -476,12 +493,12 @@ complete -c ${programName} -f -a '(${functionName})'
476
493
  for (const suggestion of suggestions) {
477
494
  if (i > 0) yield "\n";
478
495
  if (suggestion.kind === "literal") {
479
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
496
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
480
497
  yield `${suggestion.text}\t${description}`;
481
498
  } else {
482
499
  const extensions = suggestion.extensions?.join(",") || "";
483
500
  const hidden = suggestion.includeHidden ? "1" : "0";
484
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
501
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
485
502
  const pattern = encodePattern(suggestion.pattern ?? "");
486
503
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
487
504
  }
@@ -720,12 +737,12 @@ ${functionName}-external
720
737
  for (const suggestion of suggestions) {
721
738
  if (i > 0) yield "\n";
722
739
  if (suggestion.kind === "literal") {
723
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
740
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
724
741
  yield `${suggestion.text}\t${description}`;
725
742
  } else {
726
743
  const extensions = suggestion.extensions?.join(",") || "";
727
744
  const hidden = suggestion.includeHidden ? "1" : "0";
728
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
745
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
729
746
  const pattern = encodePattern(suggestion.pattern ?? "");
730
747
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
731
748
  }
@@ -890,12 +907,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
890
907
  for (const suggestion of suggestions) {
891
908
  if (i > 0) yield "\n";
892
909
  if (suggestion.kind === "literal") {
893
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
910
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
894
911
  yield `${suggestion.text}\t${suggestion.text}\t${description}`;
895
912
  } else {
896
913
  const extensions = suggestion.extensions?.join(",") || "";
897
914
  const hidden = suggestion.includeHidden ? "1" : "0";
898
- const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
915
+ const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
899
916
  const pattern = encodePattern(suggestion.pattern ?? "");
900
917
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
901
918
  }
@@ -41,6 +41,17 @@ function encodePattern(pattern) {
41
41
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
42
  }
43
43
  /**
44
+ * Replaces control characters that would corrupt shell completion protocols.
45
+ * Shell completion formats use tabs as field delimiters and newlines as record
46
+ * delimiters. Null bytes are used as delimiters in zsh's format.
47
+ * @param description The description string to sanitize.
48
+ * @returns The sanitized description with control characters replaced by spaces.
49
+ * @internal
50
+ */
51
+ function sanitizeDescription(description) {
52
+ return description.replace(/[\t\n\r\0]/g, " ");
53
+ }
54
+ /**
44
55
  * The Bash shell completion generator.
45
56
  * @since 0.6.0
46
57
  */
@@ -250,6 +261,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
250
261
  pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
251
262
  has_file_completion=1
252
263
 
264
+ # Enable glob_dots when hidden files are requested so that
265
+ # _files and _directories include dot-prefixed entries
266
+ local __was_glob_dots=0
267
+ [[ -o glob_dots ]] && __was_glob_dots=1
268
+ if [[ "\$hidden" == "1" ]]; then setopt glob_dots; fi
269
+
253
270
  # Use zsh's native file completion
254
271
  case "\$type" in
255
272
  file)
@@ -275,8 +292,8 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
275
292
  ;;
276
293
  esac
277
294
 
278
- # Note: zsh's _files and _directories handle hidden file filtering automatically
279
- # based on the completion context and user settings
295
+ # Restore glob_dots to its previous state
296
+ if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
280
297
  else
281
298
  # Regular literal completion
282
299
  if [[ -n "\$value" ]]; then
@@ -308,12 +325,12 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
308
325
  },
309
326
  *encodeSuggestions(suggestions) {
310
327
  for (const suggestion of suggestions) if (suggestion.kind === "literal") {
311
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
328
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
312
329
  yield `${suggestion.text}\0${description}\0`;
313
330
  } else {
314
331
  const extensions = suggestion.extensions?.join(",") || "";
315
332
  const hidden = suggestion.includeHidden ? "1" : "0";
316
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
333
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
317
334
  const pattern = encodePattern(suggestion.pattern ?? "");
318
335
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
319
336
  }
@@ -476,12 +493,12 @@ complete -c ${programName} -f -a '(${functionName})'
476
493
  for (const suggestion of suggestions) {
477
494
  if (i > 0) yield "\n";
478
495
  if (suggestion.kind === "literal") {
479
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
496
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
480
497
  yield `${suggestion.text}\t${description}`;
481
498
  } else {
482
499
  const extensions = suggestion.extensions?.join(",") || "";
483
500
  const hidden = suggestion.includeHidden ? "1" : "0";
484
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
501
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
485
502
  const pattern = encodePattern(suggestion.pattern ?? "");
486
503
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
487
504
  }
@@ -720,12 +737,12 @@ ${functionName}-external
720
737
  for (const suggestion of suggestions) {
721
738
  if (i > 0) yield "\n";
722
739
  if (suggestion.kind === "literal") {
723
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
740
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
724
741
  yield `${suggestion.text}\t${description}`;
725
742
  } else {
726
743
  const extensions = suggestion.extensions?.join(",") || "";
727
744
  const hidden = suggestion.includeHidden ? "1" : "0";
728
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
745
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
729
746
  const pattern = encodePattern(suggestion.pattern ?? "");
730
747
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
731
748
  }
@@ -890,12 +907,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
890
907
  for (const suggestion of suggestions) {
891
908
  if (i > 0) yield "\n";
892
909
  if (suggestion.kind === "literal") {
893
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
910
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
894
911
  yield `${suggestion.text}\t${suggestion.text}\t${description}`;
895
912
  } else {
896
913
  const extensions = suggestion.extensions?.join(",") || "";
897
914
  const hidden = suggestion.includeHidden ? "1" : "0";
898
- const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
915
+ const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
899
916
  const pattern = encodePattern(suggestion.pattern ?? "");
900
917
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
901
918
  }
@@ -1832,6 +1832,9 @@ function macAddress(options) {
1832
1832
  * @returns A parser that accepts valid domain names as strings.
1833
1833
  * @throws {RangeError} If `maxLength` is not a positive integer.
1834
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.
1835
1838
  * @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
1836
1839
  * greater than 2, since non-subdomain domains have exactly 2 labels.
1837
1840
  *
@@ -1847,7 +1850,7 @@ function macAddress(options) {
1847
1850
  * option("--root", domain({ allowSubdomains: false }))
1848
1851
  *
1849
1852
  * // Restrict to specific TLDs
1850
- * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
1853
+ * option("--domain", domain({ allowedTlds: ["com", "org", "net"] }))
1851
1854
  *
1852
1855
  * // Normalize to lowercase
1853
1856
  * option("--domain", domain({ lowercase: true }))
@@ -1858,7 +1861,19 @@ function macAddress(options) {
1858
1861
  function domain(options) {
1859
1862
  const metavar = options?.metavar ?? "DOMAIN";
1860
1863
  const allowSubdomains = options?.allowSubdomains ?? true;
1861
- const allowedTLDs = options?.allowedTLDs != null ? Object.freeze([...options.allowedTLDs]) : void 0;
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;
1862
1877
  const minLabels = options?.minLabels ?? 2;
1863
1878
  const maxLength = options?.maxLength ?? 253;
1864
1879
  const lowercase = options?.lowercase ?? false;
@@ -1870,7 +1885,6 @@ function domain(options) {
1870
1885
  const tooFewLabels = options?.errors?.tooFewLabels;
1871
1886
  const subdomainsNotAllowed = options?.errors?.subdomainsNotAllowed;
1872
1887
  const tldNotAllowed = options?.errors?.tldNotAllowed;
1873
- const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
1874
1888
  return {
1875
1889
  $mode: "sync",
1876
1890
  metavar,
@@ -2034,15 +2048,14 @@ function domain(options) {
2034
2048
  error: msg
2035
2049
  };
2036
2050
  }
2037
- if (allowedTLDs !== void 0) {
2051
+ if (allowedTlds !== void 0 && allowedTldsLower !== void 0) {
2038
2052
  const tld = labels[labels.length - 1];
2039
2053
  const tldLower = tld.toLowerCase();
2040
- const allowedTLDsLower = allowedTLDs.map((t) => t.toLowerCase());
2041
- if (!allowedTLDsLower.includes(tldLower)) {
2054
+ if (!allowedTldsLower.includes(tldLower)) {
2042
2055
  const errorMsg = tldNotAllowed;
2043
2056
  if (typeof errorMsg === "function") return {
2044
2057
  success: false,
2045
- error: errorMsg(tld, allowedTLDs)
2058
+ error: errorMsg(tld, allowedTlds)
2046
2059
  };
2047
2060
  const msg = errorMsg ?? [
2048
2061
  {
@@ -2055,7 +2068,7 @@ function domain(options) {
2055
2068
  },
2056
2069
  {
2057
2070
  type: "text",
2058
- text: ` is not allowed. Allowed TLDs: ${allowedTLDs.join(", ")}.`
2071
+ text: ` is not allowed. Allowed TLDs: ${allowedTlds.join(", ")}.`
2059
2072
  }
2060
2073
  ];
2061
2074
  return {
@@ -1479,7 +1479,7 @@ interface DomainOptions {
1479
1479
  * List of allowed top-level domains (e.g., ["com", "org", "net"]).
1480
1480
  * If specified, only domains with these TLDs are accepted.
1481
1481
  */
1482
- readonly allowedTLDs?: readonly string[];
1482
+ readonly allowedTlds?: readonly string[];
1483
1483
  /**
1484
1484
  * Minimum number of domain labels (parts separated by dots).
1485
1485
  *
@@ -1517,7 +1517,7 @@ interface DomainOptions {
1517
1517
  * Can be a static message or a function that receives the TLD
1518
1518
  * and allowed TLDs.
1519
1519
  */
1520
- tldNotAllowed?: Message | ((tld: string, allowedTLDs: readonly string[]) => Message);
1520
+ tldNotAllowed?: Message | ((tld: string, allowedTlds: readonly string[]) => Message);
1521
1521
  /**
1522
1522
  * Custom error message when domain has too few labels.
1523
1523
  * Can be a static message or a function that receives the domain
@@ -1544,6 +1544,9 @@ interface DomainOptions {
1544
1544
  * @returns A parser that accepts valid domain names as strings.
1545
1545
  * @throws {RangeError} If `maxLength` is not a positive integer.
1546
1546
  * @throws {RangeError} If `minLabels` is not a positive integer.
1547
+ * @throws {TypeError} If any `allowedTlds` entry is not a string, is empty,
1548
+ * contains dots, has leading/trailing whitespace, or is not a valid DNS
1549
+ * label.
1547
1550
  * @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
1548
1551
  * greater than 2, since non-subdomain domains have exactly 2 labels.
1549
1552
  *
@@ -1559,7 +1562,7 @@ interface DomainOptions {
1559
1562
  * option("--root", domain({ allowSubdomains: false }))
1560
1563
  *
1561
1564
  * // Restrict to specific TLDs
1562
- * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
1565
+ * option("--domain", domain({ allowedTlds: ["com", "org", "net"] }))
1563
1566
  *
1564
1567
  * // Normalize to lowercase
1565
1568
  * option("--domain", domain({ lowercase: true }))
@@ -1479,7 +1479,7 @@ interface DomainOptions {
1479
1479
  * List of allowed top-level domains (e.g., ["com", "org", "net"]).
1480
1480
  * If specified, only domains with these TLDs are accepted.
1481
1481
  */
1482
- readonly allowedTLDs?: readonly string[];
1482
+ readonly allowedTlds?: readonly string[];
1483
1483
  /**
1484
1484
  * Minimum number of domain labels (parts separated by dots).
1485
1485
  *
@@ -1517,7 +1517,7 @@ interface DomainOptions {
1517
1517
  * Can be a static message or a function that receives the TLD
1518
1518
  * and allowed TLDs.
1519
1519
  */
1520
- tldNotAllowed?: Message | ((tld: string, allowedTLDs: readonly string[]) => Message);
1520
+ tldNotAllowed?: Message | ((tld: string, allowedTlds: readonly string[]) => Message);
1521
1521
  /**
1522
1522
  * Custom error message when domain has too few labels.
1523
1523
  * Can be a static message or a function that receives the domain
@@ -1544,6 +1544,9 @@ interface DomainOptions {
1544
1544
  * @returns A parser that accepts valid domain names as strings.
1545
1545
  * @throws {RangeError} If `maxLength` is not a positive integer.
1546
1546
  * @throws {RangeError} If `minLabels` is not a positive integer.
1547
+ * @throws {TypeError} If any `allowedTlds` entry is not a string, is empty,
1548
+ * contains dots, has leading/trailing whitespace, or is not a valid DNS
1549
+ * label.
1547
1550
  * @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
1548
1551
  * greater than 2, since non-subdomain domains have exactly 2 labels.
1549
1552
  *
@@ -1559,7 +1562,7 @@ interface DomainOptions {
1559
1562
  * option("--root", domain({ allowSubdomains: false }))
1560
1563
  *
1561
1564
  * // Restrict to specific TLDs
1562
- * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
1565
+ * option("--domain", domain({ allowedTlds: ["com", "org", "net"] }))
1563
1566
  *
1564
1567
  * // Normalize to lowercase
1565
1568
  * option("--domain", domain({ lowercase: true }))
@@ -1832,6 +1832,9 @@ function macAddress(options) {
1832
1832
  * @returns A parser that accepts valid domain names as strings.
1833
1833
  * @throws {RangeError} If `maxLength` is not a positive integer.
1834
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.
1835
1838
  * @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
1836
1839
  * greater than 2, since non-subdomain domains have exactly 2 labels.
1837
1840
  *
@@ -1847,7 +1850,7 @@ function macAddress(options) {
1847
1850
  * option("--root", domain({ allowSubdomains: false }))
1848
1851
  *
1849
1852
  * // Restrict to specific TLDs
1850
- * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
1853
+ * option("--domain", domain({ allowedTlds: ["com", "org", "net"] }))
1851
1854
  *
1852
1855
  * // Normalize to lowercase
1853
1856
  * option("--domain", domain({ lowercase: true }))
@@ -1858,7 +1861,19 @@ function macAddress(options) {
1858
1861
  function domain(options) {
1859
1862
  const metavar = options?.metavar ?? "DOMAIN";
1860
1863
  const allowSubdomains = options?.allowSubdomains ?? true;
1861
- const allowedTLDs = options?.allowedTLDs != null ? Object.freeze([...options.allowedTLDs]) : void 0;
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;
1862
1877
  const minLabels = options?.minLabels ?? 2;
1863
1878
  const maxLength = options?.maxLength ?? 253;
1864
1879
  const lowercase = options?.lowercase ?? false;
@@ -1870,7 +1885,6 @@ function domain(options) {
1870
1885
  const tooFewLabels = options?.errors?.tooFewLabels;
1871
1886
  const subdomainsNotAllowed = options?.errors?.subdomainsNotAllowed;
1872
1887
  const tldNotAllowed = options?.errors?.tldNotAllowed;
1873
- const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
1874
1888
  return {
1875
1889
  $mode: "sync",
1876
1890
  metavar,
@@ -2034,15 +2048,14 @@ function domain(options) {
2034
2048
  error: msg
2035
2049
  };
2036
2050
  }
2037
- if (allowedTLDs !== void 0) {
2051
+ if (allowedTlds !== void 0 && allowedTldsLower !== void 0) {
2038
2052
  const tld = labels[labels.length - 1];
2039
2053
  const tldLower = tld.toLowerCase();
2040
- const allowedTLDsLower = allowedTLDs.map((t) => t.toLowerCase());
2041
- if (!allowedTLDsLower.includes(tldLower)) {
2054
+ if (!allowedTldsLower.includes(tldLower)) {
2042
2055
  const errorMsg = tldNotAllowed;
2043
2056
  if (typeof errorMsg === "function") return {
2044
2057
  success: false,
2045
- error: errorMsg(tld, allowedTLDs)
2058
+ error: errorMsg(tld, allowedTlds)
2046
2059
  };
2047
2060
  const msg = errorMsg ?? [
2048
2061
  {
@@ -2055,7 +2068,7 @@ function domain(options) {
2055
2068
  },
2056
2069
  {
2057
2070
  type: "text",
2058
- text: ` is not allowed. Allowed TLDs: ${allowedTLDs.join(", ")}.`
2071
+ text: ` is not allowed. Allowed TLDs: ${allowedTlds.join(", ")}.`
2059
2072
  }
2060
2073
  ];
2061
2074
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1109+fa132665",
3
+ "version": "1.0.0-dev.1122+85e536cf",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",