@optique/core 1.1.0-dev.2148 → 1.1.0-dev.2160

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.
@@ -327,6 +327,323 @@ function string(options = {}) {
327
327
  }
328
328
  };
329
329
  }
330
+ function keyValue(options = {}) {
331
+ const separator = options.separator ?? "=";
332
+ if (typeof separator !== "string") throw new TypeError(`Expected separator to be a string, but got ${typeof separator}: ${String(separator)}.`);
333
+ ensureNonEmptyString(separator);
334
+ checkBooleanOption(options, "allowEmptyKey");
335
+ checkBooleanOption(options, "allowEmptyValue");
336
+ checkEnumOption(options, "split", ["first", "last"]);
337
+ const split = options.split ?? "first";
338
+ const allowEmptyKey = options.allowEmptyKey ?? false;
339
+ const allowEmptyValue = options.allowEmptyValue ?? true;
340
+ const metavar$1 = options.metavar ?? `KEY${separator}VALUE`;
341
+ ensureNonEmptyString(metavar$1);
342
+ const rawKeyParser = options.key ?? string({ placeholder: "KEY" });
343
+ const rawValueParser = options.value ?? string({ placeholder: allowEmptyValue ? "" : "VALUE" });
344
+ checkKeyValueChildParser("key", rawKeyParser);
345
+ checkKeyValueChildParser("value", rawValueParser);
346
+ const keyParser = rawKeyParser;
347
+ const valueParser = rawValueParser;
348
+ const hasNormalize = typeof keyParser.normalize === "function" || typeof valueParser.normalize === "function";
349
+ const hasSuggest = typeof keyParser.suggest === "function" || typeof valueParser.suggest === "function";
350
+ function findSeparator(input) {
351
+ return split === "last" ? input.lastIndexOf(separator) : input.indexOf(separator);
352
+ }
353
+ function emptyKeyError(input) {
354
+ const custom = options.errors?.emptyKey;
355
+ if (custom != null) return typeof custom === "function" ? custom(input) : custom;
356
+ return message`Expected a non-empty key in ${input}.`;
357
+ }
358
+ function emptyValueError(input) {
359
+ const custom = options.errors?.emptyValue;
360
+ if (custom != null) return typeof custom === "function" ? custom(input) : custom;
361
+ return message`Expected a non-empty value in ${input}.`;
362
+ }
363
+ function missingSeparatorError(input) {
364
+ const custom = options.errors?.missingSeparator;
365
+ if (custom != null) return typeof custom === "function" ? custom(input, separator) : custom;
366
+ return message`Expected ${separator} in ${input}.`;
367
+ }
368
+ function invalidKeyError(error) {
369
+ const custom = options.errors?.invalidKey;
370
+ if (custom != null) return typeof custom === "function" ? custom(error) : custom;
371
+ return [text("Invalid key: "), ...cloneMessage(error)];
372
+ }
373
+ function invalidValueError(error) {
374
+ const custom = options.errors?.invalidValue;
375
+ if (custom != null) return typeof custom === "function" ? custom(error) : custom;
376
+ return [text("Invalid value: "), ...cloneMessage(error)];
377
+ }
378
+ function validateParts(input, key, value) {
379
+ if (!allowEmptyKey && key === "") return {
380
+ success: false,
381
+ error: emptyKeyError(input)
382
+ };
383
+ if (!allowEmptyValue && value === "") return {
384
+ success: false,
385
+ error: emptyValueError(input)
386
+ };
387
+ const keyResult = keyParser.parse(key);
388
+ if (!keyResult.success) return {
389
+ success: false,
390
+ error: invalidKeyError(keyResult.error)
391
+ };
392
+ const valueResult = valueParser.parse(value);
393
+ if (!valueResult.success) return {
394
+ success: false,
395
+ error: invalidValueError(valueResult.error)
396
+ };
397
+ return validateResultParts(input, keyResult, valueResult);
398
+ }
399
+ function validateKeyForSuggestion(key) {
400
+ if (!allowEmptyKey && key === "") return false;
401
+ if (!partsRoundTrip(key, "")) return false;
402
+ const keyResult = keyParser.parse(key);
403
+ if (!keyResult.success) return false;
404
+ if (!allowEmptyKey && keyResult.value === "") return false;
405
+ const formattedKeyResult = formatValueParserValue(keyParser, keyResult.value);
406
+ if (!formattedKeyResult.success) return false;
407
+ const formattedKey = formattedKeyResult.value;
408
+ return formattedKey == null || partsRoundTrip(formattedKey, "");
409
+ }
410
+ function validateKeyPatternForSuggestion(key) {
411
+ if (!allowEmptyKey && key === "") return false;
412
+ return partsRoundTrip(key, "");
413
+ }
414
+ function fallbackInput(key, value) {
415
+ return `${typeof key === "string" ? key : ""}${separator}${typeof value === "string" ? value : ""}`;
416
+ }
417
+ function partsRoundTrip(key, value) {
418
+ const input = `${key}${separator}${value}`;
419
+ const index = findSeparator(input);
420
+ if (index < 0) return false;
421
+ return input.slice(0, index) === key && input.slice(index + separator.length) === value;
422
+ }
423
+ function validateResultParts(input, keyResult, valueResult) {
424
+ if (!allowEmptyKey && keyResult.value === "") return {
425
+ success: false,
426
+ error: emptyKeyError(input)
427
+ };
428
+ if (!allowEmptyValue && valueResult.value === "") return {
429
+ success: false,
430
+ error: emptyValueError(input)
431
+ };
432
+ const formattedKeyResult = formatValueParserValue(keyParser, keyResult.value);
433
+ if (!formattedKeyResult.success) return {
434
+ success: false,
435
+ error: invalidKeyError(formattedKeyResult.error)
436
+ };
437
+ const formattedValueResult = formatValueParserValue(valueParser, valueResult.value);
438
+ if (!formattedValueResult.success) return {
439
+ success: false,
440
+ error: invalidValueError(formattedValueResult.error)
441
+ };
442
+ const formattedKey = formattedKeyResult.value;
443
+ const formattedValue = formattedValueResult.value;
444
+ if (!allowEmptyKey && formattedKey === "") return {
445
+ success: false,
446
+ error: emptyKeyError(input)
447
+ };
448
+ if (!allowEmptyValue && formattedValue === "") return {
449
+ success: false,
450
+ error: emptyValueError(input)
451
+ };
452
+ if (split === "first" && formattedKey != null && formattedKey.includes(separator)) return {
453
+ success: false,
454
+ error: invalidKeyError(message`Expected a key without ${separator}, but got ${formattedKey}.`)
455
+ };
456
+ if (split === "last" && formattedValue != null && formattedValue.includes(separator)) return {
457
+ success: false,
458
+ error: invalidValueError(message`Expected a value without ${separator}, but got ${formattedValue}.`)
459
+ };
460
+ if (formattedKey != null && formattedValue != null) {
461
+ if (!partsRoundTrip(formattedKey, formattedValue)) return split === "first" ? {
462
+ success: false,
463
+ error: invalidKeyError(message`Expected a key that round-trips with ${separator}, but got ${formattedKey}.`)
464
+ } : {
465
+ success: false,
466
+ error: invalidValueError(message`Expected a value that round-trips with ${separator}, but got ${formattedValue}.`)
467
+ };
468
+ }
469
+ return makeKeyValueSuccess(keyResult, valueResult);
470
+ }
471
+ function validateTuple(value) {
472
+ if (!allowEmptyKey && value[0] === "") return {
473
+ success: false,
474
+ error: emptyKeyError(fallbackInput(value[0], value[1]))
475
+ };
476
+ if (!allowEmptyValue && value[1] === "") return {
477
+ success: false,
478
+ error: emptyValueError(fallbackInput(value[0], value[1]))
479
+ };
480
+ const keyResult = validateValueParserValue(keyParser, value[0]);
481
+ if (!keyResult.success) return {
482
+ success: false,
483
+ error: invalidKeyError(keyResult.error)
484
+ };
485
+ const valueResult = validateValueParserValue(valueParser, value[1]);
486
+ if (!valueResult.success) return {
487
+ success: false,
488
+ error: invalidValueError(valueResult.error)
489
+ };
490
+ return validateResultParts(fallbackInput(value[0], value[1]), keyResult, valueResult);
491
+ }
492
+ return {
493
+ mode: "sync",
494
+ metavar: metavar$1,
495
+ placeholder: [keyParser.placeholder, valueParser.placeholder],
496
+ parse(input) {
497
+ const index = findSeparator(input);
498
+ if (index < 0) return {
499
+ success: false,
500
+ error: missingSeparatorError(input)
501
+ };
502
+ const key = input.slice(0, index);
503
+ const value = input.slice(index + separator.length);
504
+ return validateParts(input, key, value);
505
+ },
506
+ format(value) {
507
+ return `${keyParser.format(value[0])}${separator}${valueParser.format(value[1])}`;
508
+ },
509
+ validate(value) {
510
+ if (!isKeyValueTuple(value)) return {
511
+ success: false,
512
+ error: message`Expected a key-value tuple.`
513
+ };
514
+ return validateTuple(value);
515
+ },
516
+ ...hasNormalize ? { normalize(value) {
517
+ if (!isKeyValueTuple(value)) return value;
518
+ const normalized = [typeof keyParser.normalize === "function" ? keyParser.normalize(value[0]) : value[0], typeof valueParser.normalize === "function" ? valueParser.normalize(value[1]) : value[1]];
519
+ return validateTuple(normalized).success ? normalized : value;
520
+ } } : {},
521
+ ...hasSuggest ? { *suggest(prefix) {
522
+ const index = findSeparator(prefix);
523
+ const suggestions = [];
524
+ if (index < 0 || split === "last") {
525
+ if (typeof keyParser.suggest === "function") for (const suggestion of keyParser.suggest(prefix)) {
526
+ const key = suggestionTextValue(suggestion);
527
+ if (suggestion.kind === "literal" ? !validateKeyForSuggestion(key) : !validateKeyPatternForSuggestion(key)) continue;
528
+ suggestions.push(prefixKeyValueSuggestion(suggestion, "", separator));
529
+ }
530
+ }
531
+ if (index < 0) {
532
+ yield* deduplicateSuggestions(suggestions);
533
+ return;
534
+ }
535
+ if (typeof valueParser.suggest === "function") {
536
+ const key = prefix.slice(0, index);
537
+ if (!validateKeyForSuggestion(key)) {
538
+ yield* deduplicateSuggestions(suggestions);
539
+ return;
540
+ }
541
+ const valuePrefix = prefix.slice(index + separator.length);
542
+ for (const suggestion of valueParser.suggest(valuePrefix)) {
543
+ const value = suggestionTextValue(suggestion);
544
+ if (!partsRoundTrip(key, value)) continue;
545
+ if (suggestion.kind === "literal" && !validateParts(`${key}${separator}${value}`, key, value).success) continue;
546
+ suggestions.push(prefixKeyValueSuggestion(suggestion, `${key}${separator}`));
547
+ }
548
+ }
549
+ yield* deduplicateSuggestions(suggestions);
550
+ } } : {}
551
+ };
552
+ }
553
+ function checkKeyValueChildParser(role, parser) {
554
+ if (!isValueParser(parser)) throw new TypeError(`The ${role} option for keyValue() must be a value parser.`);
555
+ if (parser.mode !== "sync") throw new TypeError(`keyValue() only supports sync ${role} parsers, but an async one was given.`);
556
+ if (isDerivedValueParser(parser)) throw new TypeError(`keyValue() does not support dependency-derived ${role} parsers (created via deriveFrom() or dependency().derive()); pass the derived parser directly to option() or argument() instead.`);
557
+ }
558
+ function validateValueParserValue(parser, value) {
559
+ if (typeof parser.validate === "function") return parser.validate(value);
560
+ let formatted;
561
+ try {
562
+ formatted = parser.format(value);
563
+ } catch {
564
+ return {
565
+ success: true,
566
+ value
567
+ };
568
+ }
569
+ if (typeof formatted !== "string") return {
570
+ success: false,
571
+ error: stringFormatError()
572
+ };
573
+ return parser.parse(formatted);
574
+ }
575
+ function formatValueParserValue(parser, value) {
576
+ let formatted;
577
+ try {
578
+ formatted = parser.format(value);
579
+ } catch {
580
+ return {
581
+ success: true,
582
+ value: void 0
583
+ };
584
+ }
585
+ if (typeof formatted !== "string") return {
586
+ success: false,
587
+ error: stringFormatError()
588
+ };
589
+ return {
590
+ success: true,
591
+ value: formatted
592
+ };
593
+ }
594
+ function stringFormatError() {
595
+ return message`Expected a value formatted as a string.`;
596
+ }
597
+ function suggestionTextValue(suggestion) {
598
+ return suggestion.kind === "literal" ? suggestion.text : suggestion.pattern ?? "";
599
+ }
600
+ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
601
+ const textValue = suggestionTextValue(suggestion);
602
+ const text$1 = `${prefix}${textValue}${suffix}`;
603
+ if (suggestion.kind === "file" && suffix === "") return {
604
+ ...suggestion,
605
+ pattern: text$1
606
+ };
607
+ return suggestion.description == null ? {
608
+ kind: "literal",
609
+ text: text$1
610
+ } : {
611
+ kind: "literal",
612
+ text: text$1,
613
+ description: suggestion.description
614
+ };
615
+ }
616
+ function isKeyValueTuple(value) {
617
+ return Array.isArray(value) && value.length === 2 && 0 in value && 1 in value;
618
+ }
619
+ function makeKeyValueSuccess(keyResult, valueResult) {
620
+ const deferredKeys = /* @__PURE__ */ new Map();
621
+ let hasDeferred = false;
622
+ collectKeyValueDeferredKeys(deferredKeys, 0, keyResult, () => {
623
+ hasDeferred = true;
624
+ });
625
+ collectKeyValueDeferredKeys(deferredKeys, 1, valueResult, () => {
626
+ hasDeferred = true;
627
+ });
628
+ return {
629
+ success: true,
630
+ value: [keyResult.value, valueResult.value],
631
+ ...hasDeferred ? {
632
+ deferred: true,
633
+ ...deferredKeys.size > 0 ? { deferredKeys } : {}
634
+ } : {}
635
+ };
636
+ }
637
+ function collectKeyValueDeferredKeys(deferredKeys, index, result, markDeferred) {
638
+ if (result.deferredKeys != null) {
639
+ markDeferred();
640
+ deferredKeys.set(index, result.deferredKeys);
641
+ return;
642
+ }
643
+ if (result.deferred !== true) return;
644
+ markDeferred();
645
+ deferredKeys.set(index, null);
646
+ }
330
647
  /**
331
648
  * Creates a ValueParser for parsing integer values from strings.
332
649
  *
@@ -3088,7 +3405,7 @@ function email(options) {
3088
3405
  * Creates a value parser for socket addresses in "host:port" format.
3089
3406
  *
3090
3407
  * Validates socket addresses with support for:
3091
- * - Hostnames and IPv4 addresses (IPv6 support coming in future versions)
3408
+ * - Hostnames, IPv4 addresses, and IPv6 addresses
3092
3409
  * - Configurable host:port separator
3093
3410
  * - Optional default port
3094
3411
  * - Host type filtering (hostname only, IP only, or both)
@@ -3099,6 +3416,8 @@ function email(options) {
3099
3416
  * @throws {TypeError} If `separator` is an empty string.
3100
3417
  * @throws {TypeError} If `separator` contains digit characters, since digits
3101
3418
  * in the separator would cause ambiguous splitting of port input.
3419
+ * @throws {TypeError} If `host.version` is provided but is not `4`, `6`, or
3420
+ * `"both"`.
3102
3421
  * @since 0.10.0
3103
3422
  *
3104
3423
  * @example
@@ -3128,12 +3447,25 @@ function socketAddress(options) {
3128
3447
  const defaultPort = options?.defaultPort;
3129
3448
  const requirePort = options?.requirePort ?? false;
3130
3449
  const hostType = options?.host?.type ?? "both";
3450
+ const rawHostVersion = options?.host?.version;
3451
+ if (rawHostVersion !== void 0 && rawHostVersion !== 4 && rawHostVersion !== 6 && rawHostVersion !== "both") throw new TypeError(`Expected host.version to be 4, 6, or "both", but got ${Array.isArray(rawHostVersion) ? "array" : typeof rawHostVersion}: ${String(rawHostVersion)}.`);
3452
+ const hasLegacyIpOptions = options?.host?.ip !== void 0;
3453
+ const hasNewIpOptions = rawHostVersion !== void 0 || options?.host?.ipv4 !== void 0 || options?.host?.ipv6 !== void 0;
3454
+ const hostVersion = rawHostVersion ?? (hasLegacyIpOptions && !hasNewIpOptions ? 4 : "both");
3131
3455
  const hostnameParser = hostname({
3132
3456
  ...options?.host?.hostname,
3133
3457
  metavar: "HOST"
3134
3458
  });
3135
- const ipParser = ipv4({
3459
+ const ipv4Options = {
3136
3460
  ...options?.host?.ip,
3461
+ ...options?.host?.ipv4
3462
+ };
3463
+ const ipv4Parser = ipv4({
3464
+ ...ipv4Options,
3465
+ metavar: "HOST"
3466
+ });
3467
+ const ipv6Parser = ipv6({
3468
+ ...options?.host?.ipv6,
3137
3469
  metavar: "HOST"
3138
3470
  });
3139
3471
  const disambiguationParser = hostType === "ip" ? hostname({
@@ -3151,6 +3483,15 @@ function socketAddress(options) {
3151
3483
  function looksLikeIpv4(input) {
3152
3484
  return /^\d+\.\d+\.\d+\.\d+$/.test(input);
3153
3485
  }
3486
+ function looksLikeIpv6(input) {
3487
+ const colonCount = input.match(/:/g)?.length ?? 0;
3488
+ return colonCount >= 2 && (input.includes("::") || /^[0-9a-fA-F:.]+$/.test(input));
3489
+ }
3490
+ function looksLikeBareIpv6(input) {
3491
+ if (!looksLikeIpv6(input)) return false;
3492
+ if (parseAndNormalizeIpv6(input) !== null) return true;
3493
+ return input.includes("::") || !input.includes(".");
3494
+ }
3154
3495
  function looksLikeAltIpv4Literal(input) {
3155
3496
  if (/^0[xX][0-9a-fA-F]+$/.test(input)) {
3156
3497
  const n = parseInt(input.slice(2), 16);
@@ -3176,6 +3517,22 @@ function socketAddress(options) {
3176
3517
  }
3177
3518
  return false;
3178
3519
  }
3520
+ function parseIpHost(hostInput) {
3521
+ if (hostVersion === 4) return ipv4Parser.parse(hostInput);
3522
+ if (hostVersion === 6) return ipv6Parser.parse(hostInput);
3523
+ if (looksLikeIpv6(hostInput)) {
3524
+ const result = ipv6Parser.parse(hostInput);
3525
+ if (result.success) {
3526
+ const mappedOctets = extractIpv4FromMapped(result.value);
3527
+ if (mappedOctets !== null) {
3528
+ const restrictionError = checkIpv4MappedRestrictions(mappedOctets, result.value, ipv4Options, void 0);
3529
+ if (restrictionError !== null) return restrictionError;
3530
+ }
3531
+ }
3532
+ return result;
3533
+ }
3534
+ return ipv4Parser.parse(hostInput);
3535
+ }
3179
3536
  function parseHost(hostInput) {
3180
3537
  if (hostType === "hostname") {
3181
3538
  if (looksLikeAltIpv4Literal(hostInput)) return {
@@ -3187,22 +3544,106 @@ function socketAddress(options) {
3187
3544
  error: message`Expected a valid hostname, but got ${hostInput}.`
3188
3545
  };
3189
3546
  return hostnameParser.parse(hostInput);
3190
- } else if (hostType === "ip") return ipParser.parse(hostInput);
3547
+ } else if (hostType === "ip") return parseIpHost(hostInput);
3191
3548
  else {
3192
3549
  if (looksLikeAltIpv4Literal(hostInput)) return {
3193
3550
  success: false,
3194
3551
  error: message`${hostInput} appears to be a non-standard IPv4 address notation.`
3195
3552
  };
3196
- if (looksLikeIpv4(hostInput)) return ipParser.parse(hostInput);
3553
+ if (looksLikeIpv4(hostInput)) return parseIpHost(hostInput);
3554
+ if (looksLikeIpv6(hostInput)) return parseIpHost(hostInput);
3197
3555
  return hostnameParser.parse(hostInput);
3198
3556
  }
3199
3557
  }
3200
- return {
3558
+ function makeInvalidFormatError(input) {
3559
+ const errorMsg = options?.errors?.invalidFormat;
3560
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a socket address in format ${text(formatExample)}, but got ${input}.`;
3561
+ return {
3562
+ success: false,
3563
+ error: msg
3564
+ };
3565
+ }
3566
+ function makeMissingPortError(input) {
3567
+ const errorMsg = options?.errors?.missingPort;
3568
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Port number is required but was not specified.`;
3569
+ return {
3570
+ success: false,
3571
+ error: msg
3572
+ };
3573
+ }
3574
+ function parseBracketedHost(input, trimmed, canOmitPort) {
3575
+ if (!trimmed.startsWith("[")) return void 0;
3576
+ const closingBracket = trimmed.indexOf("]");
3577
+ if (closingBracket === -1) return makeInvalidFormatError(input);
3578
+ const hostInput = trimmed.slice(1, closingBracket);
3579
+ if (!looksLikeBareIpv6(hostInput)) return makeInvalidFormatError(input);
3580
+ const rest = trimmed.slice(closingBracket + 1).trimStart();
3581
+ const hostResult = parseHost(hostInput);
3582
+ if (rest === "") {
3583
+ if (!hostResult.success) {
3584
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3585
+ return {
3586
+ success: false,
3587
+ error: hostResult.error
3588
+ };
3589
+ }
3590
+ if (canOmitPort) return {
3591
+ success: true,
3592
+ value: {
3593
+ host: hostResult.value,
3594
+ port: defaultPort
3595
+ }
3596
+ };
3597
+ return makeMissingPortError(input);
3598
+ }
3599
+ if (!rest.startsWith(separator)) return makeInvalidFormatError(input);
3600
+ const portPart = rest.slice(separator.length).trim();
3601
+ if (portPart === "") {
3602
+ if (!hostResult.success) {
3603
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3604
+ return {
3605
+ success: false,
3606
+ error: hostResult.error
3607
+ };
3608
+ }
3609
+ return makeMissingPortError(input);
3610
+ }
3611
+ const portResult = portParser.parse(portPart);
3612
+ if (!hostResult.success) {
3613
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3614
+ return {
3615
+ success: false,
3616
+ error: hostResult.error
3617
+ };
3618
+ }
3619
+ if (portResult.success) return {
3620
+ success: true,
3621
+ value: {
3622
+ host: hostResult.value,
3623
+ port: portResult.value
3624
+ }
3625
+ };
3626
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3627
+ if (/^[0-9]+$/.test(portPart)) return {
3628
+ success: false,
3629
+ error: portResult.error
3630
+ };
3631
+ return makeInvalidFormatError(input);
3632
+ }
3633
+ function isSocketAddressValue(value) {
3634
+ return typeof value === "object" && value !== null && "host" in value && typeof value.host === "string" && "port" in value && typeof value.port === "number";
3635
+ }
3636
+ function formatSocketAddressValue(value) {
3637
+ const ipv6Host = parseAndNormalizeIpv6(value.host);
3638
+ if (separator === ":" && ipv6Host !== null) return `[${ipv6Host}]${separator}${value.port}`;
3639
+ return `${value.host}${separator}${value.port}`;
3640
+ }
3641
+ const parser = {
3201
3642
  mode: "sync",
3202
3643
  metavar: metavar$1,
3203
3644
  get placeholder() {
3204
3645
  return {
3205
- host: hostType === "ip" ? ipParser.placeholder : hostnameParser.placeholder,
3646
+ host: hostType === "ip" ? hostVersion === 6 ? ipv6Parser.placeholder : ipv4Parser.placeholder : hostnameParser.placeholder,
3206
3647
  port: defaultPort ?? portParser.placeholder
3207
3648
  };
3208
3649
  },
@@ -3210,6 +3651,28 @@ function socketAddress(options) {
3210
3651
  const trimmed = input.trim();
3211
3652
  const searchInput = input.trimStart();
3212
3653
  const canOmitPort = defaultPort !== void 0 && !requirePort;
3654
+ if (separator === ":") {
3655
+ const bracketedResult = parseBracketedHost(input, trimmed, canOmitPort);
3656
+ if (bracketedResult !== void 0) return bracketedResult;
3657
+ if (looksLikeBareIpv6(trimmed)) {
3658
+ const hostResult = parseHost(trimmed);
3659
+ if (hostResult.success) {
3660
+ if (canOmitPort) return {
3661
+ success: true,
3662
+ value: {
3663
+ host: hostResult.value,
3664
+ port: defaultPort
3665
+ }
3666
+ };
3667
+ return makeMissingPortError(input);
3668
+ }
3669
+ if (options?.errors?.invalidFormat) return makeInvalidFormatError(input);
3670
+ return {
3671
+ success: false,
3672
+ error: hostResult.error
3673
+ };
3674
+ }
3675
+ }
3213
3676
  let firstHostError;
3214
3677
  let validHostInvalidPortError;
3215
3678
  let trailingSepHost;
@@ -3407,9 +3870,19 @@ function socketAddress(options) {
3407
3870
  };
3408
3871
  },
3409
3872
  format(value) {
3410
- return `${value.host}${separator}${value.port}`;
3873
+ return formatSocketAddressValue(value);
3874
+ },
3875
+ normalize(value) {
3876
+ if (!isSocketAddressValue(value)) return value;
3877
+ try {
3878
+ const result = parser.parse(formatSocketAddressValue(value));
3879
+ return result.success ? result.value : value;
3880
+ } catch {
3881
+ return value;
3882
+ }
3411
3883
  }
3412
3884
  };
3885
+ return parser;
3413
3886
  }
3414
3887
  function portRange(options) {
3415
3888
  checkBooleanOption(options, "disallowWellKnown");
@@ -4323,7 +4796,7 @@ function parseAndNormalizeIpv6(input) {
4323
4796
  const leftGroups = parts[0] ? parts[0].split(":") : [];
4324
4797
  const rightGroups = parts[1] ? parts[1].split(":") : [];
4325
4798
  const totalGroups = leftGroups.length + rightGroups.length;
4326
- if (totalGroups > 8) return null;
4799
+ if (totalGroups >= 8) return null;
4327
4800
  const zeroCount = 8 - totalGroups;
4328
4801
  const zeros = Array(zeroCount).fill("0");
4329
4802
  groups = [
@@ -4354,7 +4827,7 @@ function expandIpv6(input) {
4354
4827
  const leftGroups = parts[0] ? parts[0].split(":").filter((g) => g) : [];
4355
4828
  const rightGroups = parts[1] ? parts[1].split(":").filter((g) => g) : [];
4356
4829
  const totalGroups = leftGroups.length + rightGroups.length;
4357
- if (totalGroups > 8) return null;
4830
+ if (totalGroups >= 8) return null;
4358
4831
  const zeroCount = 8 - totalGroups;
4359
4832
  const zeros = Array(zeroCount).fill("0");
4360
4833
  const groups = [
@@ -5624,4 +6097,4 @@ function plainObjectsEqual(a, b) {
5624
6097
  }
5625
6098
 
5626
6099
  //#endregion
5627
- export { checkBooleanOption, checkEnumOption, choice, cidr, color, domain, email, ensureNonEmptyString, fileSize, firstOf, float, hostname, integer, ip, ipv4, ipv6, isNonEmptyString, isValueParser, json, locale, macAddress, port, portRange, semVer, socketAddress, string, url, uuid };
6100
+ export { checkBooleanOption, checkEnumOption, choice, cidr, color, domain, email, ensureNonEmptyString, fileSize, firstOf, float, hostname, integer, ip, ipv4, ipv6, isNonEmptyString, isValueParser, json, keyValue, locale, macAddress, port, portRange, semVer, socketAddress, string, url, uuid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.1.0-dev.2148",
3
+ "version": "1.1.0-dev.2160",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -200,7 +200,7 @@
200
200
  "fast-check": "^4.7.0",
201
201
  "tsdown": "^0.13.0",
202
202
  "typescript": "^5.8.3",
203
- "@optique/env": "1.1.0-dev.2148+ab56ac96"
203
+ "@optique/env": "1.1.0-dev.2160+4b3b85cc"
204
204
  },
205
205
  "scripts": {
206
206
  "build": "tsdown",