@optique/core 1.3.0-dev.2581 → 1.3.0-dev.2583

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.
@@ -2750,6 +2750,178 @@ function url(options = {}) {
2750
2750
  };
2751
2751
  }
2752
2752
  /**
2753
+ * A host name that the WHATWG URL parser has canonicalized into a dotted-quad
2754
+ * IPv4 address. Matching the canonical form is enough, because the parser
2755
+ * turns every other spelling (`0x7f.1`, `2130706433`, …) into it.
2756
+ */
2757
+ const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/u;
2758
+ /**
2759
+ * Creates a {@link ValueParser} for web origins.
2760
+ *
2761
+ * An origin is a scheme, a host, and an optional port, and nothing else. The
2762
+ * parser returns a `URL` whose pathname is `/` and which carries no
2763
+ * credentials, query, or fragment.
2764
+ *
2765
+ * Input is canonicalized rather than rejected when it carries more than an
2766
+ * origin: the host is lowercased, internationalized names are converted to
2767
+ * punycode, and a default port is elided, so `HTTPS://Example.COM/`,
2768
+ * `https://example.com:443/path?query#fragment` all parse to
2769
+ * `https://example.com`. Set `extraComponents: "reject"` to fail on a path,
2770
+ * query, or fragment instead. Credentials are always rejected. Schemes whose
2771
+ * origin is opaque (`mailto:`, `data:`, `file:`) are rejected, as is `blob:`,
2772
+ * whose origin belongs to the URL it wraps.
2773
+ *
2774
+ * By default a trailing root-zone dot is stripped, so `example.com.` becomes
2775
+ * `example.com`. This differs from the WHATWG origin, which keeps the dot, so
2776
+ * the two strings are not interchangeable when an exact origin comparison
2777
+ * matters. Use `trailingDot: "preserve"` for the WHATWG behavior or
2778
+ * `trailingDot: "append"` to force a fully-qualified name.
2779
+ *
2780
+ * @param options Configuration options for the origin parser.
2781
+ * @returns A {@link ValueParser} that converts string input to `URL` objects
2782
+ * that name an origin.
2783
+ * @throws {TypeError} If any `allowedProtocols` entry is not a valid protocol
2784
+ * string ending with a colon (e.g., `"https:"`).
2785
+ * @since 1.3.0
2786
+ */
2787
+ function origin(options = {}) {
2788
+ const metavar$1 = options.metavar ?? "ORIGIN";
2789
+ ensureNonEmptyString(metavar$1);
2790
+ checkEnumOption(options, "extraComponents", ["strip", "reject"]);
2791
+ checkEnumOption(options, "trailingDot", [
2792
+ "strip",
2793
+ "preserve",
2794
+ "append"
2795
+ ]);
2796
+ const extraComponents = options.extraComponents ?? "strip";
2797
+ const trailingDot = options.trailingDot ?? "strip";
2798
+ const originalProtocolsList = [];
2799
+ const normalizedProtocolsList = [];
2800
+ if (options.allowedProtocols != null) {
2801
+ const seen = /* @__PURE__ */ new Set();
2802
+ for (const protocol of options.allowedProtocols) {
2803
+ if (typeof protocol !== "string" || !/^[a-z][a-z0-9+\-.]*:$/i.test(protocol)) {
2804
+ const rendered = typeof protocol === "string" ? JSON.stringify(protocol) : String(protocol);
2805
+ throw new TypeError(`Each allowed protocol must be a valid protocol ending with a colon (e.g., "https:"), got: ${rendered}.`);
2806
+ }
2807
+ const normalized = protocol.toLowerCase();
2808
+ if (seen.has(normalized)) continue;
2809
+ seen.add(normalized);
2810
+ originalProtocolsList.push(protocol);
2811
+ normalizedProtocolsList.push(normalized);
2812
+ }
2813
+ if (originalProtocolsList.length === 0) throw new TypeError("allowedProtocols must not be empty.");
2814
+ }
2815
+ const originalProtocols = options.allowedProtocols != null ? Object.freeze(originalProtocolsList) : void 0;
2816
+ const allowedProtocols = options.allowedProtocols != null ? Object.freeze(normalizedProtocolsList) : void 0;
2817
+ const invalidOrigin = options.errors?.invalidOrigin;
2818
+ const disallowedProtocol = options.errors?.disallowedProtocol;
2819
+ function invalidOriginError(input) {
2820
+ return invalidOrigin ? typeof invalidOrigin === "function" ? invalidOrigin(input) : invalidOrigin : message`Invalid origin: ${input}.`;
2821
+ }
2822
+ function disallowedProtocolError(protocol) {
2823
+ return disallowedProtocol ? typeof disallowedProtocol === "function" ? disallowedProtocol(protocol, originalProtocols) : disallowedProtocol : [
2824
+ {
2825
+ type: "text",
2826
+ text: "URL protocol "
2827
+ },
2828
+ {
2829
+ type: "value",
2830
+ value: protocol
2831
+ },
2832
+ {
2833
+ type: "text",
2834
+ text: " is not allowed. Allowed protocols: "
2835
+ },
2836
+ ...valueSet(originalProtocols, {
2837
+ fallback: "",
2838
+ locale: "en-US"
2839
+ }),
2840
+ {
2841
+ type: "text",
2842
+ text: "."
2843
+ }
2844
+ ];
2845
+ }
2846
+ function withTrailingDot(url$1) {
2847
+ if (trailingDot === "preserve") return url$1;
2848
+ const { hostname: hostname$1 } = url$1;
2849
+ if (hostname$1.startsWith("[") || IPV4_PATTERN.test(hostname$1)) return url$1;
2850
+ if (trailingDot === "strip") {
2851
+ const stripped = hostname$1.replace(/\.+$/u, "");
2852
+ if (stripped !== "") url$1.hostname = stripped;
2853
+ } else if (!hostname$1.endsWith(".")) url$1.hostname = `${hostname$1}.`;
2854
+ return url$1;
2855
+ }
2856
+ function resolve(url$1, label) {
2857
+ if (allowedProtocols != null && !allowedProtocols.includes(url$1.protocol)) return {
2858
+ success: false,
2859
+ error: disallowedProtocolError(url$1.protocol)
2860
+ };
2861
+ if (url$1.protocol === "blob:") return {
2862
+ success: false,
2863
+ error: message`The URL ${label} wraps another origin. Pass that origin instead.`
2864
+ };
2865
+ if (url$1.origin === "null") return {
2866
+ success: false,
2867
+ error: message`The URL ${label} has no origin.`
2868
+ };
2869
+ if (url$1.username !== "" || url$1.password !== "") return {
2870
+ success: false,
2871
+ error: message`An origin must not contain credentials: ${url$1.origin}`
2872
+ };
2873
+ if (extraComponents === "reject" && (url$1.pathname !== "/" || url$1.search !== "" || url$1.hash !== "")) return {
2874
+ success: false,
2875
+ error: message`Expected only an origin, but got: ${label}`
2876
+ };
2877
+ return {
2878
+ success: true,
2879
+ value: withTrailingDot(new URL(url$1.origin))
2880
+ };
2881
+ }
2882
+ return {
2883
+ mode: "sync",
2884
+ metavar: metavar$1,
2885
+ get placeholder() {
2886
+ return new URL(`${allowedProtocols?.[0] ?? "http:"}//0.invalid`);
2887
+ },
2888
+ parse(input) {
2889
+ if (!URL.canParse(input)) return {
2890
+ success: false,
2891
+ error: invalidOriginError(input)
2892
+ };
2893
+ return resolve(new URL(input), input);
2894
+ },
2895
+ validate(value) {
2896
+ if (!(value instanceof URL)) return {
2897
+ success: false,
2898
+ error: invalidOriginError(String(value))
2899
+ };
2900
+ return resolve(value, value.href);
2901
+ },
2902
+ format(value) {
2903
+ return value.origin;
2904
+ },
2905
+ normalize(value) {
2906
+ if (!(value instanceof URL)) return value;
2907
+ const result = resolve(value, value.href);
2908
+ return result.success ? result.value : value;
2909
+ },
2910
+ *suggest(prefix) {
2911
+ if (allowedProtocols && prefix.length > 0 && !prefix.includes(":")) for (const protocol of allowedProtocols) {
2912
+ const cleanProtocol = protocol.replace(/:+$/, "");
2913
+ if (cleanProtocol.startsWith(prefix.toLowerCase())) {
2914
+ const suffix = SPECIAL_URL_SCHEMES.has(cleanProtocol) ? "://" : ":";
2915
+ yield {
2916
+ kind: "literal",
2917
+ text: `${cleanProtocol}${suffix}`
2918
+ };
2919
+ }
2920
+ }
2921
+ }
2922
+ };
2923
+ }
2924
+ /**
2753
2925
  * Creates a {@link ValueParser} for locale values.
2754
2926
  *
2755
2927
  * This parser validates that the input is a well-formed locale identifier
@@ -6667,4 +6839,4 @@ function plainObjectsEqual(a, b) {
6667
6839
  }
6668
6840
 
6669
6841
  //#endregion
6670
- export { biject, checkBooleanOption, checkEnumOption, choice, cidr, color, cron, domain, email, ensureNonEmptyString, fileSize, firstOf, float, hostname, integer, ip, ipv4, ipv6, isNonEmptyString, isValueParser, json, keyValue, locale, macAddress, port, portRange, regExp, semVer, socketAddress, string, transform, url, uuid };
6842
+ export { biject, checkBooleanOption, checkEnumOption, choice, cidr, color, cron, domain, email, ensureNonEmptyString, fileSize, firstOf, float, hostname, integer, ip, ipv4, ipv6, isNonEmptyString, isValueParser, json, keyValue, locale, macAddress, origin, port, portRange, regExp, semVer, socketAddress, string, transform, url, uuid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.3.0-dev.2581",
3
+ "version": "1.3.0-dev.2583",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "sideEffects": false,
239
239
  "devDependencies": {
240
- "@optique/env": "1.3.0-dev.2581+50dd484c",
240
+ "@optique/env": "1.3.0-dev.2583+d30dbb53",
241
241
  "@types/node": "^24.0.0",
242
242
  "fast-check": "^4.7.0",
243
243
  "tsdown": "^0.13.0",
@@ -41,11 +41,11 @@ Core rules
41
41
  customize terminal output with `theme` or `messageFormatter`, preserving
42
42
  `initialWidth`, quoting, and width options. Keep canonical errors unthemed.
43
43
  - Use value parsers such as `integer()`, `choice()`, `biject()`, `regExp()`,
44
- `url()`, and `uuid()` instead of validating raw strings after parsing. Use
45
- `regExp({ flags })` for user-supplied regular expression sources,
46
- `biject()` for one-to-one string-to-value choices, and `transform()` when
47
- an existing value parser describes the accepted CLI spelling but your app
48
- needs a different result type. Use `path()` from
44
+ `url()`, `origin()`, and `uuid()` instead of validating raw strings after
45
+ parsing. Use `regExp({ flags })` for user-supplied sources, `biject()` for
46
+ one-to-one string-to-value choices, and `transform()` when an existing
47
+ value parser describes the accepted CLI spelling but your app needs a
48
+ different result type. Use `path()` from
49
49
  `@optique/run/valueparser` for file-system paths. Write a custom
50
50
  `{ mode, metavar, parse, format }` value parser only when the catalog does
51
51
  not cover the domain.