paramour 0.5.0 → 0.6.0
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.
- package/README.md +44 -0
- package/dist/codec.d.ts +16 -18
- package/dist/describe.d.ts +14 -3
- package/dist/describe.js +20 -3
- package/dist/errors.d.ts +89 -10
- package/dist/errors.js +79 -13
- package/dist/href.d.ts +38 -41
- package/dist/href.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +4 -3
- package/dist/internal.js +4 -3
- package/dist/p.d.ts +11 -10
- package/dist/p.js +45 -37
- package/dist/path.d.ts +26 -26
- package/dist/path.js +78 -42
- package/dist/route.d.ts +73 -73
- package/dist/route.js +30 -30
- package/dist/safe-decode.d.ts +10 -9
- package/dist/safe-decode.js +12 -11
- package/dist/schema.d.ts +4 -4
- package/dist/schema.js +4 -4
- package/dist/search.d.ts +32 -28
- package/dist/search.js +76 -42
- package/dist/standard-schema.d.ts +15 -15
- package/dist/standard-schema.js +15 -15
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# paramour
|
|
2
|
+
|
|
3
|
+
Type-safe routing companion for the Next.js App Router: validated, typed
|
|
4
|
+
route and search params, type-checked path building, and a predictable,
|
|
5
|
+
human-readable URL wire format. Validation is bring-your-own via
|
|
6
|
+
[Standard Schema](https://github.com/standard-schema/standard-schema) —
|
|
7
|
+
paramour owns serialization (the part validators can't do), your validator
|
|
8
|
+
owns the rules.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add paramour @paramour-js/next
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineAppRoute, href, p } from "paramour";
|
|
16
|
+
|
|
17
|
+
export const productRoute = defineAppRoute("/product/[id]", {
|
|
18
|
+
params: { id: p.integer() },
|
|
19
|
+
search: { q: p.string().optional() },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// typed, validated, explicit: "/product/42?q=paramour"
|
|
23
|
+
href(productRoute, { params: { id: 42 }, search: { q: "paramour" } });
|
|
24
|
+
|
|
25
|
+
// a string into p.integer() fails to compile
|
|
26
|
+
href(productRoute, { params: { id: "42" } });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Routes are plain imported objects — no central registry, nothing to
|
|
30
|
+
tree-shake around. Codecs are bidirectional wire converters with a
|
|
31
|
+
type-state modifier API (`.optional()`, `.default()`, `.catch()`) where
|
|
32
|
+
illegal chains fail to compile, and every codec serializes by a
|
|
33
|
+
[published, numbered spec](https://paramour.dev/docs/reference/wire-format).
|
|
34
|
+
|
|
35
|
+
## Docs
|
|
36
|
+
|
|
37
|
+
- [Getting started](https://paramour.dev/docs/getting-started)
|
|
38
|
+
- [Core API reference](https://paramour.dev/docs/reference/core)
|
|
39
|
+
- [Wire-format spec & explorer](https://paramour.dev/docs/reference/wire-format)
|
|
40
|
+
- [Migrating from next-typesafe-url](https://paramour.dev/docs/migrate)
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT © Jason Paff
|
package/dist/codec.d.ts
CHANGED
|
@@ -11,11 +11,10 @@ export type Arity = "many" | "single";
|
|
|
11
11
|
*
|
|
12
12
|
* `Out` is the decoded in-memory type. `P`, `C`, `A`, and `E` are type-state:
|
|
13
13
|
* modifier methods are conditionally `never`, so illegal chains
|
|
14
|
-
* (`.optional().default()`, double `.catch()`) fail to compile
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* round-trip there.
|
|
14
|
+
* (`.optional().default()`, double `.catch()`) fail to compile. `E` carries
|
|
15
|
+
* `~defaultElides` as a literal after `.default()`. Presence modifiers are
|
|
16
|
+
* also `never` for arity-"many" codecs: absent and `[]` are the same wire
|
|
17
|
+
* state (S6/P6), so `.default()`/`.optional()` could never round-trip there.
|
|
19
18
|
*
|
|
20
19
|
* `.default()` and `.catch()` accept either a value or a zero-arg factory;
|
|
21
20
|
* factories are invoked per decode/encode, so reference-typed defaults can be
|
|
@@ -29,8 +28,8 @@ export type Arity = "many" | "single";
|
|
|
29
28
|
export interface Codec<Out, P extends Presence = "required", C extends boolean = false, A extends Arity = "single", E extends boolean = boolean> {
|
|
30
29
|
readonly catch: C extends false ? (fallback: (() => Out) | Out) => Codec<Out, P, true, A, E> : never;
|
|
31
30
|
/**
|
|
32
|
-
* Overloaded so the value/factory split is visible in type-state
|
|
33
|
-
*
|
|
31
|
+
* Overloaded so the value/factory split is visible in type-state: the
|
|
32
|
+
* factory overload comes FIRST and must stay first. The runtime
|
|
34
33
|
* {@link isFactory} check treats ANY function as a factory, so a function
|
|
35
34
|
* argument must either match the factory overload or fail to compile
|
|
36
35
|
* ({@link NonFactoryValue}) — E=true is only ever inferred for arguments
|
|
@@ -55,19 +54,18 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
|
|
|
55
54
|
* factory would elide an explicitly-passed value that later decodes as a
|
|
56
55
|
* different one.
|
|
57
56
|
*
|
|
58
|
-
* Literal-typed via `E`
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
62
|
-
*
|
|
63
|
-
* reading.
|
|
57
|
+
* Literal-typed via `E` so derived surfaces (`@paramour-js/nuqs`) can give
|
|
58
|
+
* value-defaulted keys non-nullable reads while keeping factory-defaulted
|
|
59
|
+
* keys honestly nullable. A hand-written `Codec<…, "defaulted">` leaves
|
|
60
|
+
* `E` at its `boolean` default, which consumers must treat as the factory
|
|
61
|
+
* (nullable) branch — the safe reading.
|
|
64
62
|
*/
|
|
65
63
|
readonly "~defaultElides": E;
|
|
66
64
|
/** Stored as a thunk regardless of the form passed to `.default()`. */
|
|
67
65
|
readonly "~defaultValue": (() => Out) | undefined;
|
|
68
66
|
/**
|
|
69
67
|
* Element codec of a composite list codec (`p.csv`, `p.array`) — the
|
|
70
|
-
* per-segment/per-key scalar; undefined for every non-composite kind
|
|
68
|
+
* per-segment/per-key scalar; undefined for every non-composite kind.
|
|
71
69
|
*/
|
|
72
70
|
readonly "~element": AnyCodec | undefined;
|
|
73
71
|
/** Members of a `p.enum` codec; undefined for every other kind. */
|
|
@@ -85,11 +83,11 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
|
|
|
85
83
|
readonly "~serializeElement": (value: unknown) => string;
|
|
86
84
|
}
|
|
87
85
|
export type OutputOf<C extends AnyCodec> = C["~out"];
|
|
88
|
-
/** Codecs legal in a `params:` config — no presence modifiers (
|
|
86
|
+
/** Codecs legal in a `params:` config — no presence modifiers (D5). */
|
|
89
87
|
export type ParamCodec = Codec<any, "required", boolean>;
|
|
90
88
|
/**
|
|
91
89
|
* Presence governs absence semantics and property optionality on both the
|
|
92
|
-
* parse-output and href-input sides (
|
|
90
|
+
* parse-output and href-input sides (D4). Catch is orthogonal:
|
|
93
91
|
* it recovers parse *failures*, never absence (D2).
|
|
94
92
|
*/
|
|
95
93
|
export type Presence = "defaulted" | "optional" | "required";
|
|
@@ -98,8 +96,8 @@ export type PresenceOf<C extends AnyCodec> = C["~presence"];
|
|
|
98
96
|
* Rejects value-form `.default()` arguments whose static type includes any
|
|
99
97
|
* function member: runtime {@link isFactory} would treat them as factories,
|
|
100
98
|
* so letting them infer the value branch would let type-state assert an
|
|
101
|
-
* elision (`E = true`) the runtime never performs
|
|
102
|
-
*
|
|
99
|
+
* elision (`E = true`) the runtime never performs. Non-distributive on
|
|
100
|
+
* purpose — a union with a function member is rejected whole, since its
|
|
103
101
|
* runtime branch is unknowable at compile time.
|
|
104
102
|
*/
|
|
105
103
|
type NonFactoryValue<V> = [Extract<V, (...args: never[]) => unknown>] extends [
|
package/dist/describe.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export interface CodecDescription {
|
|
|
23
23
|
readonly caught: boolean;
|
|
24
24
|
readonly defaultValue?: CodecDefaultDescription;
|
|
25
25
|
/**
|
|
26
|
-
* Nested description of a composite list codec's element scalar (
|
|
27
|
-
*
|
|
26
|
+
* Nested description of a composite list codec's element scalar (`p.csv`
|
|
27
|
+
* and `p.array`).
|
|
28
28
|
*/
|
|
29
29
|
readonly element?: CodecDescription;
|
|
30
30
|
readonly enumMembers?: readonly string[];
|
|
@@ -32,7 +32,7 @@ export interface CodecDescription {
|
|
|
32
32
|
readonly presence: Presence;
|
|
33
33
|
}
|
|
34
34
|
/** Rendering styles accepted by {@link formatCodecDescription}. */
|
|
35
|
-
export type CodecFormatStyle = "compact" | "verbose";
|
|
35
|
+
export type CodecFormatStyle = "compact" | "shape" | "verbose";
|
|
36
36
|
/** A param codec plus the dynamic-segment kind that hosts it. */
|
|
37
37
|
export interface ParamDescription extends CodecDescription {
|
|
38
38
|
readonly segmentKind: "catchall" | "optional-catchall" | "single";
|
|
@@ -57,6 +57,15 @@ export type SearchDescription = {
|
|
|
57
57
|
} | {
|
|
58
58
|
readonly kind: "raw";
|
|
59
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* A codec's `"shape"`-style label for decode-issue enrichment. `forceMany`
|
|
62
|
+
* renders the repeated form (`integer[]`) for segment-level catch-all
|
|
63
|
+
* issues, whose codec describes ONE element (the same forced-arity move as
|
|
64
|
+
* render.ts's catch-all params). Exported for path.ts/search.ts and — via
|
|
65
|
+
* the `paramour/internal` tooling entry — for devtools' synthesized issues,
|
|
66
|
+
* never from the package barrel.
|
|
67
|
+
*/
|
|
68
|
+
export declare function codecShapeLabel(codec: AnyCodec, forceMany?: boolean): string;
|
|
60
69
|
/**
|
|
61
70
|
* Reflects a codec into plain data. This is the public face of the
|
|
62
71
|
* `~`-prefixed metadata: user code reads descriptions, never the props.
|
|
@@ -77,6 +86,8 @@ export declare function describeRoute(route: AnyRoute): RouteDescription;
|
|
|
77
86
|
* - `"compact"`: `enum(asc|desc)? =asc catch`, `csv<enum(a|b)>`, `string[]`
|
|
78
87
|
* — `?` for optional presence, the default's wire form (`=3`) or `=ƒ()`
|
|
79
88
|
* for a factory default, bare `catch`.
|
|
89
|
+
* - `"shape"`: the bare shape label with no presence/default/catch
|
|
90
|
+
* annotations — the form decode errors cite as an issue's `expected`.
|
|
80
91
|
* - `"verbose"`: `enum(asc, desc) (optional) (default: asc) (catch)` —
|
|
81
92
|
* parenthesized annotations in fixed order: presence, default, catch.
|
|
82
93
|
*/
|
package/dist/describe.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A codec's `"shape"`-style label for decode-issue enrichment. `forceMany`
|
|
3
|
+
* renders the repeated form (`integer[]`) for segment-level catch-all
|
|
4
|
+
* issues, whose codec describes ONE element (the same forced-arity move as
|
|
5
|
+
* render.ts's catch-all params). Exported for path.ts/search.ts and — via
|
|
6
|
+
* the `paramour/internal` tooling entry — for devtools' synthesized issues,
|
|
7
|
+
* never from the package barrel.
|
|
8
|
+
*/
|
|
9
|
+
export function codecShapeLabel(codec, forceMany = false) {
|
|
10
|
+
const description = describeCodec(codec);
|
|
11
|
+
return formatCodecDescription(forceMany ? { ...description, arity: "many" } : description, "shape");
|
|
12
|
+
}
|
|
1
13
|
/**
|
|
2
14
|
* Reflects a codec into plain data. This is the public face of the
|
|
3
15
|
* `~`-prefixed metadata: user code reads descriptions, never the props.
|
|
@@ -31,8 +43,9 @@ export function describeRoute(route) {
|
|
|
31
43
|
if (segment.kind === "static")
|
|
32
44
|
continue;
|
|
33
45
|
const codec = paramsConfig[segment.name];
|
|
34
|
-
// Unreachable for routes built by the define constructors (
|
|
35
|
-
// exactly the extracted names); guards
|
|
46
|
+
// Unreachable for routes built by the define constructors (they require
|
|
47
|
+
// a codec for exactly the path's extracted param names); guards
|
|
48
|
+
// hand-assembled objects.
|
|
36
49
|
if (codec === undefined)
|
|
37
50
|
continue;
|
|
38
51
|
params[segment.name] = {
|
|
@@ -56,11 +69,13 @@ export function describeRoute(route) {
|
|
|
56
69
|
* - `"compact"`: `enum(asc|desc)? =asc catch`, `csv<enum(a|b)>`, `string[]`
|
|
57
70
|
* — `?` for optional presence, the default's wire form (`=3`) or `=ƒ()`
|
|
58
71
|
* for a factory default, bare `catch`.
|
|
72
|
+
* - `"shape"`: the bare shape label with no presence/default/catch
|
|
73
|
+
* annotations — the form decode errors cite as an issue's `expected`.
|
|
59
74
|
* - `"verbose"`: `enum(asc, desc) (optional) (default: asc) (catch)` —
|
|
60
75
|
* parenthesized annotations in fixed order: presence, default, catch.
|
|
61
76
|
*/
|
|
62
77
|
export function formatCodecDescription(description, style) {
|
|
63
|
-
const memberSeparator = style === "
|
|
78
|
+
const memberSeparator = style === "verbose" ? ", " : "|";
|
|
64
79
|
const kindLabel = (part) => part.enumMembers === undefined
|
|
65
80
|
? part.kind
|
|
66
81
|
: `enum(${part.enumMembers.join(memberSeparator)})`;
|
|
@@ -80,6 +95,8 @@ export function formatCodecDescription(description, style) {
|
|
|
80
95
|
return part.arity === "many" ? `${base}[]` : base;
|
|
81
96
|
};
|
|
82
97
|
let label = shapeLabel(description);
|
|
98
|
+
if (style === "shape")
|
|
99
|
+
return label;
|
|
83
100
|
if (style === "compact") {
|
|
84
101
|
if (description.presence === "optional")
|
|
85
102
|
label += "?";
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
|
-
/** One failed key in an aggregate decode error (shared by both surfaces
|
|
1
|
+
/** One failed key in an aggregate decode error (shared by both surfaces). */
|
|
2
2
|
export interface Issue {
|
|
3
|
+
/**
|
|
4
|
+
* Bare shape label of the codec the key expected (`integer`,
|
|
5
|
+
* `enum(asc|desc)`, `csv<integer>[]`). Absent when no codec owns the key:
|
|
6
|
+
* rawSearch schema issues and foreign throws carry only prose.
|
|
7
|
+
*/
|
|
8
|
+
readonly expected?: string;
|
|
3
9
|
readonly key: string;
|
|
4
10
|
readonly message: string;
|
|
11
|
+
/**
|
|
12
|
+
* What KIND of failure this issue records — the structured discriminant
|
|
13
|
+
* renderers key on instead of sniffing `message` prose (see
|
|
14
|
+
* {@link IssueReason} for the members). Core's decoders always set it;
|
|
15
|
+
* it is optional only so prose-only issues built outside core (derived
|
|
16
|
+
* tooling, hand-built test fixtures) remain representable — an absent
|
|
17
|
+
* reason means "unclassified", and renderers must not infer one.
|
|
18
|
+
*/
|
|
19
|
+
readonly reason?: IssueReason;
|
|
20
|
+
/**
|
|
21
|
+
* The offending value as the codec grammar saw it — the value-layer
|
|
22
|
+
* string AFTER byte-layer percent-decoding, not the raw URL text. Search
|
|
23
|
+
* sources (`URLSearchParams` / Next's `searchParams`) arrive
|
|
24
|
+
* platform-decoded; route params are decoded by core (R5) before the
|
|
25
|
+
* grammar runs — so a segment `1%20x` records `wire: "1 x"` on both
|
|
26
|
+
* surfaces. Present only when a single offending value exists: absent
|
|
27
|
+
* for missing keys, non-string source values, and the duplicate-scalar
|
|
28
|
+
* rejection — absence there is the point, not a data gap.
|
|
29
|
+
*/
|
|
30
|
+
readonly wire?: string;
|
|
5
31
|
}
|
|
6
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The failure kinds an {@link Issue} can record:
|
|
34
|
+
*
|
|
35
|
+
* - `"duplicate"` — a single-value param received multiple wire values (P5).
|
|
36
|
+
* - `"missing"` — a required key had no wire value at all (including a
|
|
37
|
+
* required catch-all whose array arrived empty: the values are missing
|
|
38
|
+
* even though the key exists).
|
|
39
|
+
* - `"parse"` — the codec's OWN wire grammar rejected the value; core's
|
|
40
|
+
* grammar messages quote the value and name the grammar themselves.
|
|
41
|
+
* - `"shape"` — the source value's shape doesn't match the param kind (an
|
|
42
|
+
* array where a single segment belongs, a non-string element, …).
|
|
43
|
+
* - `"validate"` — user-supplied code rejected the value (a Standard Schema
|
|
44
|
+
* validator, a custom codec's parse): its prose is not authored by core
|
|
45
|
+
* and may name neither the value nor the expected shape.
|
|
46
|
+
*/
|
|
47
|
+
export type IssueReason = "duplicate" | "missing" | "parse" | "shape" | "validate";
|
|
48
|
+
/** The single error type surfaced by a full route parse failure. */
|
|
7
49
|
export type RouteDecodeError = ParamsDecodeError | SearchDecodeError;
|
|
8
50
|
/** Base class for every error paramour throws. */
|
|
9
51
|
export declare class ParamourError extends Error {
|
|
@@ -12,10 +54,12 @@ export declare class ParamourError extends Error {
|
|
|
12
54
|
});
|
|
13
55
|
static [Symbol.hasInstance](value: unknown): value is ParamourError;
|
|
14
56
|
}
|
|
15
|
-
/** Aggregate failure for a whole route-params decode
|
|
57
|
+
/** Aggregate failure for a whole route-params decode. */
|
|
16
58
|
export declare class ParamsDecodeError extends ParamourError {
|
|
17
59
|
readonly issues: readonly Issue[];
|
|
18
|
-
|
|
60
|
+
/** The failed route's path pattern; null when decoded outside a route. */
|
|
61
|
+
readonly route: null | string;
|
|
62
|
+
constructor(issues: readonly Issue[], route?: null | string);
|
|
19
63
|
static [Symbol.hasInstance](value: unknown): value is ParamsDecodeError;
|
|
20
64
|
}
|
|
21
65
|
/**
|
|
@@ -23,20 +67,37 @@ export declare class ParamsDecodeError extends ParamourError {
|
|
|
23
67
|
* Thrown by element-level parsing; recoverable via `.catch()`.
|
|
24
68
|
*/
|
|
25
69
|
export declare class ParseError extends ParamourError {
|
|
70
|
+
/**
|
|
71
|
+
* True when the message follows core's grammar-authoring convention —
|
|
72
|
+
* it quotes the offending wire value and names the grammar it failed
|
|
73
|
+
* (`'"x" is not an integer'`). Only core's own grammar throw sites set
|
|
74
|
+
* it (via {@link grammarParseError}); schema-validation failures and
|
|
75
|
+
* rebranded foreign/custom throws stay false, the safe default: an
|
|
76
|
+
* unknown message is assumed to name neither, so renderers supply the
|
|
77
|
+
* expected-shape context themselves. This flag — never message sniffing
|
|
78
|
+
* — is what issue producers key `reason: "parse" | "validate"` on.
|
|
79
|
+
*/
|
|
80
|
+
readonly selfDescribing: boolean;
|
|
81
|
+
constructor(message: string, options?: {
|
|
82
|
+
cause?: unknown;
|
|
83
|
+
selfDescribing?: boolean;
|
|
84
|
+
});
|
|
26
85
|
static [Symbol.hasInstance](value: unknown): value is ParseError;
|
|
27
86
|
}
|
|
28
87
|
/** Aggregate failure for a whole search-params decode. */
|
|
29
88
|
export declare class SearchDecodeError extends ParamourError {
|
|
30
89
|
readonly issues: readonly Issue[];
|
|
31
|
-
|
|
90
|
+
/** The failed route's path pattern; null when decoded outside a route. */
|
|
91
|
+
readonly route: null | string;
|
|
92
|
+
constructor(issues: readonly Issue[], route?: null | string);
|
|
32
93
|
static [Symbol.hasInstance](value: unknown): value is SearchDecodeError;
|
|
33
94
|
}
|
|
34
95
|
/**
|
|
35
|
-
* A search source violated its wire-shape contract
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
96
|
+
* A search source violated its wire-shape contract: a non-object source, or
|
|
97
|
+
* a non-string / non-string[] value under a read key. Thrown by search.ts's
|
|
98
|
+
* source readers; distinct from {@link ParamourError} so the Standard Schema
|
|
99
|
+
* adapter can soften exactly these throws to issues while config-contract
|
|
100
|
+
* violations and rebranded validator throws stay loud.
|
|
40
101
|
*/
|
|
41
102
|
export declare class SearchSourceError extends ParamourError {
|
|
42
103
|
/** The offending source key, or null when the source itself is malformed. */
|
|
@@ -63,6 +124,24 @@ export declare function describeType(value: unknown): string;
|
|
|
63
124
|
* guard.
|
|
64
125
|
*/
|
|
65
126
|
export declare function foreignMessage(error: unknown): string;
|
|
127
|
+
/**
|
|
128
|
+
* A {@link ParseError} whose message follows core's grammar-authoring
|
|
129
|
+
* convention: it quotes the offending wire value and names the expected
|
|
130
|
+
* grammar (`'"x" is not an integer'`). The one sanctioned way to mint a
|
|
131
|
+
* self-describing ParseError — every `p.*` grammar throw site goes through
|
|
132
|
+
* it, so the convention is enforced by structure, not by prose review.
|
|
133
|
+
* Not exported from the package.
|
|
134
|
+
*/
|
|
135
|
+
export declare function grammarParseError(message: string): ParseError;
|
|
136
|
+
/**
|
|
137
|
+
* Maps a caught {@link ParseError} to its {@link Issue} reason: core's
|
|
138
|
+
* grammar-authored messages are `"parse"`, everything else — schema
|
|
139
|
+
* validators, rebranded custom-codec throws — is `"validate"`. Keyed on the
|
|
140
|
+
* structural `selfDescribing` flag, never on message sniffing; shared by
|
|
141
|
+
* search.ts and path.ts so both surfaces classify identically. Not exported
|
|
142
|
+
* from the package.
|
|
143
|
+
*/
|
|
144
|
+
export declare function parseIssueReason(error: ParseError): IssueReason;
|
|
66
145
|
/**
|
|
67
146
|
* Runs user (or platform) code, letting paramour's own errors pass through
|
|
68
147
|
* and branding any foreign throw via `wrap` — the shared chokepoint for the
|
package/dist/errors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cross-copy identity brands
|
|
2
|
+
* Cross-copy identity brands. `Symbol.for()` keys resolve in the
|
|
3
3
|
* realm-global symbol registry, so a second physical copy of this module
|
|
4
4
|
* (dual-package hazard, bundler duplication) mints the SAME symbols:
|
|
5
5
|
* `instanceof` recognizes instances across copies, while a structurally
|
|
@@ -29,15 +29,18 @@ export class ParamourError extends Error {
|
|
|
29
29
|
return hasBrand(value, paramourErrorBrand);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
/** Aggregate failure for a whole route-params decode
|
|
32
|
+
/** Aggregate failure for a whole route-params decode. */
|
|
33
33
|
export class ParamsDecodeError extends ParamourError {
|
|
34
34
|
static {
|
|
35
35
|
brandPrototype(this, paramsDecodeErrorBrand);
|
|
36
36
|
}
|
|
37
37
|
issues;
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/** The failed route's path pattern; null when decoded outside a route. */
|
|
39
|
+
route;
|
|
40
|
+
constructor(issues, route = null) {
|
|
41
|
+
super(formatDecodeMessage("route params", issues, route));
|
|
40
42
|
this.issues = issues;
|
|
43
|
+
this.route = route;
|
|
41
44
|
}
|
|
42
45
|
static [Symbol.hasInstance](value) {
|
|
43
46
|
return hasBrand(value, paramsDecodeErrorBrand);
|
|
@@ -51,6 +54,21 @@ export class ParseError extends ParamourError {
|
|
|
51
54
|
static {
|
|
52
55
|
brandPrototype(this, parseErrorBrand);
|
|
53
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* True when the message follows core's grammar-authoring convention —
|
|
59
|
+
* it quotes the offending wire value and names the grammar it failed
|
|
60
|
+
* (`'"x" is not an integer'`). Only core's own grammar throw sites set
|
|
61
|
+
* it (via {@link grammarParseError}); schema-validation failures and
|
|
62
|
+
* rebranded foreign/custom throws stay false, the safe default: an
|
|
63
|
+
* unknown message is assumed to name neither, so renderers supply the
|
|
64
|
+
* expected-shape context themselves. This flag — never message sniffing
|
|
65
|
+
* — is what issue producers key `reason: "parse" | "validate"` on.
|
|
66
|
+
*/
|
|
67
|
+
selfDescribing;
|
|
68
|
+
constructor(message, options) {
|
|
69
|
+
super(message, options);
|
|
70
|
+
this.selfDescribing = options?.selfDescribing ?? false;
|
|
71
|
+
}
|
|
54
72
|
static [Symbol.hasInstance](value) {
|
|
55
73
|
return hasBrand(value, parseErrorBrand);
|
|
56
74
|
}
|
|
@@ -61,20 +79,23 @@ export class SearchDecodeError extends ParamourError {
|
|
|
61
79
|
brandPrototype(this, searchDecodeErrorBrand);
|
|
62
80
|
}
|
|
63
81
|
issues;
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
/** The failed route's path pattern; null when decoded outside a route. */
|
|
83
|
+
route;
|
|
84
|
+
constructor(issues, route = null) {
|
|
85
|
+
super(formatDecodeMessage("search params", issues, route));
|
|
66
86
|
this.issues = issues;
|
|
87
|
+
this.route = route;
|
|
67
88
|
}
|
|
68
89
|
static [Symbol.hasInstance](value) {
|
|
69
90
|
return hasBrand(value, searchDecodeErrorBrand);
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
/**
|
|
73
|
-
* A search source violated its wire-shape contract
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
94
|
+
* A search source violated its wire-shape contract: a non-object source, or
|
|
95
|
+
* a non-string / non-string[] value under a read key. Thrown by search.ts's
|
|
96
|
+
* source readers; distinct from {@link ParamourError} so the Standard Schema
|
|
97
|
+
* adapter can soften exactly these throws to issues while config-contract
|
|
98
|
+
* violations and rebranded validator throws stay loud.
|
|
78
99
|
*/
|
|
79
100
|
export class SearchSourceError extends ParamourError {
|
|
80
101
|
static {
|
|
@@ -118,6 +139,28 @@ export function describeType(value) {
|
|
|
118
139
|
export function foreignMessage(error) {
|
|
119
140
|
return error instanceof Error ? error.message : showValue(error);
|
|
120
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* A {@link ParseError} whose message follows core's grammar-authoring
|
|
144
|
+
* convention: it quotes the offending wire value and names the expected
|
|
145
|
+
* grammar (`'"x" is not an integer'`). The one sanctioned way to mint a
|
|
146
|
+
* self-describing ParseError — every `p.*` grammar throw site goes through
|
|
147
|
+
* it, so the convention is enforced by structure, not by prose review.
|
|
148
|
+
* Not exported from the package.
|
|
149
|
+
*/
|
|
150
|
+
export function grammarParseError(message) {
|
|
151
|
+
return new ParseError(message, { selfDescribing: true });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Maps a caught {@link ParseError} to its {@link Issue} reason: core's
|
|
155
|
+
* grammar-authored messages are `"parse"`, everything else — schema
|
|
156
|
+
* validators, rebranded custom-codec throws — is `"validate"`. Keyed on the
|
|
157
|
+
* structural `selfDescribing` flag, never on message sniffing; shared by
|
|
158
|
+
* search.ts and path.ts so both surfaces classify identically. Not exported
|
|
159
|
+
* from the package.
|
|
160
|
+
*/
|
|
161
|
+
export function parseIssueReason(error) {
|
|
162
|
+
return error.selfDescribing ? "parse" : "validate";
|
|
163
|
+
}
|
|
121
164
|
/**
|
|
122
165
|
* Runs user (or platform) code, letting paramour's own errors pass through
|
|
123
166
|
* and branding any foreign throw via `wrap` — the shared chokepoint for the
|
|
@@ -152,8 +195,31 @@ function brandPrototype(ctor, brand) {
|
|
|
152
195
|
// the brand never leaks into JSON/spread and can't be reassigned.
|
|
153
196
|
Object.defineProperty(ctor.prototype, brand, { value: true });
|
|
154
197
|
}
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
/**
|
|
199
|
+
* The aggregate decode message: a route-anchored header plus one `✖` line
|
|
200
|
+
* per issue. Multi-line and pretty BY DEFAULT because the unhandled-throw
|
|
201
|
+
* surfaces that matter (Next's dev overlay, terminal stacks) render
|
|
202
|
+
* `error.message` verbatim — an opt-in `.pretty()` helper would never be
|
|
203
|
+
* reached there. `(expected …)` is keyed on the issue's structured `reason`,
|
|
204
|
+
* never on message/wire sniffing: it renders exactly where the message
|
|
205
|
+
* cannot name the expected shape itself — a `"missing"` key has no value to
|
|
206
|
+
* describe, and a `"validate"` failure carries foreign prose (schema
|
|
207
|
+
* validators, custom parsers) with no authoring convention. Core's own
|
|
208
|
+
* `"parse"` grammar messages already quote the value and name the grammar,
|
|
209
|
+
* a `"duplicate"` or `"shape"` message states a problem that isn't about
|
|
210
|
+
* the grammar at all, and a reason-less issue is unclassified prose — none
|
|
211
|
+
* of those take the suffix.
|
|
212
|
+
*/
|
|
213
|
+
function formatDecodeMessage(subject, issues, route) {
|
|
214
|
+
const target = route === null ? subject : `${subject} for ${route}`;
|
|
215
|
+
const lines = issues.map((issue) => {
|
|
216
|
+
const expected = issue.expected !== undefined &&
|
|
217
|
+
(issue.reason === "missing" || issue.reason === "validate")
|
|
218
|
+
? ` (expected ${issue.expected})`
|
|
219
|
+
: "";
|
|
220
|
+
return ` ✖ ${issue.key}: ${issue.message}${expected}`;
|
|
221
|
+
});
|
|
222
|
+
return [`Failed to decode ${target}:`, ...lines].join("\n");
|
|
157
223
|
}
|
|
158
224
|
function hasBrand(value, brand) {
|
|
159
225
|
if (typeof value !== "object" || value === null)
|
package/dist/href.d.ts
CHANGED
|
@@ -2,46 +2,44 @@ import type { AnyRoute, RegisteredStaticRoutePaths } from "./route.js";
|
|
|
2
2
|
import { type InferParamsInput } from "./path.js";
|
|
3
3
|
import { type SearchInputOf } from "./search.js";
|
|
4
4
|
/**
|
|
5
|
-
* Type-only brand carrier
|
|
6
|
-
*
|
|
5
|
+
* Type-only brand carrier: no runtime value ever exists — the brand is
|
|
6
|
+
* applied by a compile-time cast, so Href costs nothing at runtime.
|
|
7
7
|
*/
|
|
8
8
|
declare const HREF: unique symbol;
|
|
9
9
|
/**
|
|
10
|
-
* A paramour-built link
|
|
11
|
-
* `router.push`, `redirect`, `generateMetadata` consume it unchanged
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* A paramour-built link. Assignable TO `string`, so `next/link`,
|
|
11
|
+
* `router.push`, `redirect`, `generateMetadata` consume it unchanged; not
|
|
12
|
+
* assignable FROM `string`, which is the enabling substrate for the future
|
|
13
|
+
* "accept only paramour-built links" narrowing APIs. The brand is a
|
|
14
|
+
* permanent commitment — removing it later would be a breaking change.
|
|
15
15
|
*/
|
|
16
16
|
export type Href<P extends string = string> = string & {
|
|
17
17
|
[HREF]: P;
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
-
* href's variadic options tuple
|
|
21
|
-
*
|
|
20
|
+
* href's variadic options tuple: the entire argument is omittable when
|
|
21
|
+
* neither half has a required member — `href(aboutRoute)`.
|
|
22
22
|
*/
|
|
23
23
|
export type HrefArgs<R extends AnyRoute> = Record<never, never> extends InferHrefInput<R> ? [options?: InferHrefInput<R>] : [options: InferHrefInput<R>];
|
|
24
24
|
/**
|
|
25
|
-
* href's options object
|
|
26
|
-
*
|
|
27
|
-
* 2026-07-04, amending RL4's letter): a half may be omitted when its input
|
|
25
|
+
* href's options object: `{ params, search?, hash? }`. Property optionality
|
|
26
|
+
* is presence-driven on BOTH halves: a half may be omitted when its input
|
|
28
27
|
* type has no required key — for `params` that means static routes and
|
|
29
28
|
* routes whose only dynamic segment is an optional catch-all; for `search`
|
|
30
|
-
* it is
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* option.
|
|
29
|
+
* it is D4's rule surfacing at the property level. A half whose input has no
|
|
30
|
+
* keys AT ALL may not be passed even empty (see PartFor). `hash` implements
|
|
31
|
+
* S10 — fragments come only from an explicit caller option.
|
|
34
32
|
*/
|
|
35
33
|
export type InferHrefInput<R extends AnyRoute> = PartFor<"params", InferParamsInput<R>> & PartFor<"search", SearchInputOf<R["~search"]>> & {
|
|
36
34
|
hash?: string;
|
|
37
35
|
};
|
|
38
36
|
/**
|
|
39
|
-
* The string form's options
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
37
|
+
* The string form's options: hash only. `params` is meaningless on a static
|
|
38
|
+
* path, and a query string comes only from a defined route's search codecs —
|
|
39
|
+
* a raw-search escape hatch here would be an untyped side door around
|
|
40
|
+
* library-owned serialization. Both are banned outright (`?: never`) rather
|
|
41
|
+
* than merely omitted, so a non-fresh options object can't smuggle them past
|
|
42
|
+
* excess-property checking.
|
|
45
43
|
*/
|
|
46
44
|
export interface StaticHrefOptions {
|
|
47
45
|
hash?: string;
|
|
@@ -50,29 +48,28 @@ export interface StaticHrefOptions {
|
|
|
50
48
|
}
|
|
51
49
|
/**
|
|
52
50
|
* One options property whose presence follows its input type: required iff
|
|
53
|
-
* the input has at least one required key (the
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* checking; `?: never` mirrors RouteConfig's static-path `params?: never`.
|
|
51
|
+
* the input has at least one required key (the D4 `object extends` probe).
|
|
52
|
+
* An input with NO keys at all bans the property outright (`?: never`) — the
|
|
53
|
+
* bare `Partial<Record<Key, Input>>` form would accept arbitrary junk there,
|
|
54
|
+
* because the empty object type is exempt from excess-property checking;
|
|
55
|
+
* `?: never` mirrors RouteConfig's static-path `params?: never`.
|
|
59
56
|
*/
|
|
60
57
|
type PartFor<Key extends string, Input> = keyof Input extends never ? Partial<Record<Key, never>> : Record<never, never> extends Input ? Partial<Record<Key, Input>> : Record<Key, Input>;
|
|
61
58
|
/**
|
|
62
|
-
* Builds a link for a route: fixed path–`?query`–`#hash` assembly
|
|
63
|
-
* standalone function, not a route method
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
59
|
+
* Builds a link for a route: fixed path–`?query`–`#hash` assembly. A
|
|
60
|
+
* standalone function, not a route method: parse sites sit next to one
|
|
61
|
+
* route, while href sites import `{ href }` once and use it against many
|
|
62
|
+
* routes. Serialization failures are `SerializeError` at link-build time
|
|
63
|
+
* (the R-rules); config-contract violations from hand-built routes (a
|
|
64
|
+
* missing param codec or `~search` config) are base `ParamourError` — a JS
|
|
65
|
+
* caller omitting a required `search` half falls through to encodeSearch's
|
|
66
|
+
* own required-missing error.
|
|
70
67
|
*
|
|
71
|
-
* The string form
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
68
|
+
* The string form: a registered STATIC path stands in for the route object —
|
|
69
|
+
* same brand, same hash assembly, no route definition needed. The string
|
|
70
|
+
* overload sits first so the route-object overload is last: TS's "the last
|
|
71
|
+
* overload gave the following error" heuristic keeps route-object misuse
|
|
72
|
+
* diagnostics prominent.
|
|
76
73
|
*/
|
|
77
74
|
export declare function href<P extends RegisteredStaticRoutePaths>(path: P, options?: StaticHrefOptions): Href<P>;
|
|
78
75
|
export declare function href<R extends AnyRoute>(route: R, ...args: HrefArgs<R>): Href<R["path"]>;
|
package/dist/href.js
CHANGED
|
@@ -12,15 +12,15 @@ export function href(route, options) {
|
|
|
12
12
|
const hash = options?.hash;
|
|
13
13
|
const fragment = hash === undefined || hash === "" ? "" : `#${hash}`;
|
|
14
14
|
if (typeof route === "string") {
|
|
15
|
-
//
|
|
15
|
+
// Fail-fast backstop for JS callers and world-A typos of the
|
|
16
16
|
// dynamic-path variety — a bracket means "you need a route object", and
|
|
17
17
|
// query/hash never ride in the path string (query comes only from
|
|
18
18
|
// search codecs, hash only from the option).
|
|
19
19
|
if (!route.startsWith("/") || /[#?[\]]/.test(route)) {
|
|
20
20
|
throw new ParamourError(`href(path) requires a static route path, got ${JSON.stringify(route)}: dynamic segments need a route object, and query/hash never ride in the path string`);
|
|
21
21
|
}
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// Silently dropping a half a JS caller passed would build a wrong link —
|
|
23
|
+
// contract violations stay loud (never the safe-parse error arm).
|
|
24
24
|
if (options?.params !== undefined || options?.search !== undefined) {
|
|
25
25
|
throw new ParamourError(`href(path) takes no params/search — a static path has no params, and a query string needs a route with search codecs`);
|
|
26
26
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type AnyCodec, type Arity, type Codec, type OutputOf, type ParamCodec, type Presence, type PresenceOf, } from "./codec.js";
|
|
2
2
|
export { type CodecDefaultDescription, type CodecDescription, type CodecFormatStyle, describeCodec, describeRoute, formatCodecDescription, type ParamDescription, type RouteDescription, type SearchDescription, } from "./describe.js";
|
|
3
|
-
export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
3
|
+
export { type Issue, type IssueReason, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
4
4
|
export { href, type Href, type HrefArgs, type InferHrefInput, type StaticHrefOptions, } from "./href.js";
|
|
5
5
|
export { p } from "./p.js";
|
|
6
6
|
export { buildPath, decodeParams, type DecodeParamsOptions, encodeParams, encodeStaticParams, type InferStaticParams, type ParamsSource, } from "./path.js";
|