paramour 0.5.1 → 0.7.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/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 +1 -1
package/dist/path.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { codecShapeLabel } from "./describe.js";
|
|
2
|
+
import { describeType, ParamourError, ParamsDecodeError, ParseError, parseIssueReason, SerializeError, } from "./errors.js";
|
|
2
3
|
import { encodeComponent, readInputValue, serializeValue } from "./search.js";
|
|
3
|
-
// Anchored
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Anchored, like the wire-value grammars; the name charset excludes brackets
|
|
5
|
+
// so nesting can't smuggle through. Match order mirrors the type grammar:
|
|
6
|
+
// `[[...` before `[...` before `[`.
|
|
6
7
|
const OPTIONAL_CATCHALL_TOKEN = /^\[\[\.\.\.([^\][]+)\]\]$/;
|
|
7
8
|
const CATCHALL_TOKEN = /^\[\.\.\.([^\][]+)\]$/;
|
|
8
9
|
const SINGLE_TOKEN = /^\[([^\][]+)\]$/;
|
|
9
10
|
const GROUP_SEGMENT = /^\(.*\)$/;
|
|
10
11
|
/**
|
|
11
|
-
* Builds the path portion of an href
|
|
12
|
+
* Builds the path portion of an href: `/` plus the encoded segments
|
|
12
13
|
* joined with `/`. R2's element joining falls out of the same join as
|
|
13
14
|
* everything else; a fully-elided path (an optional catch-all at the root)
|
|
14
15
|
* yields "/".
|
|
@@ -17,7 +18,7 @@ export function buildPath(route, params) {
|
|
|
17
18
|
return `/${encodeParams(route, params).join("/")}`;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
* Decodes a params source against a route's codecs
|
|
21
|
+
* Decodes a params source against a route's codecs, the sync twin of
|
|
21
22
|
* `route.parseParams` — mirrors decodeSearch: per-key {@link Issue}
|
|
22
23
|
* aggregation into {@link ParamsDecodeError}. Shape validation is strict:
|
|
23
24
|
* `[id]` given an array, a catch-all given a string, a missing required key,
|
|
@@ -46,6 +47,12 @@ export function decodeParams(route, source, options) {
|
|
|
46
47
|
if (segment.kind === "static")
|
|
47
48
|
continue;
|
|
48
49
|
const codec = requireCodec(config, segment.name, route.path);
|
|
50
|
+
// One "expected" label per param, whatever the failure mode: a
|
|
51
|
+
// catch-all's codec describes ONE element, but the param's shape is the
|
|
52
|
+
// array, so every catch-all issue — missing, wrong shape, or a single
|
|
53
|
+
// failing element — cites the repeated form (integer[]), matching
|
|
54
|
+
// decodeSearch's array-element issues.
|
|
55
|
+
const expected = codecShapeLabel(codec, segment.kind !== "single");
|
|
49
56
|
// Own properties only: unknown keys are never read, and inherited
|
|
50
57
|
// Object.prototype members must not count as present values.
|
|
51
58
|
const value = Object.hasOwn(source, segment.name)
|
|
@@ -54,16 +61,20 @@ export function decodeParams(route, source, options) {
|
|
|
54
61
|
if (segment.kind === "single") {
|
|
55
62
|
if (value === undefined) {
|
|
56
63
|
issues.push({
|
|
64
|
+
expected,
|
|
57
65
|
key: segment.name,
|
|
58
66
|
message: "required route param is missing",
|
|
67
|
+
reason: "missing",
|
|
59
68
|
});
|
|
60
69
|
}
|
|
61
70
|
else if (typeof value !== "string") {
|
|
62
|
-
//
|
|
71
|
+
// Shape mismatches are recorded issues, never ParseErrors —
|
|
63
72
|
// .catch() cannot recover them.
|
|
64
73
|
issues.push({
|
|
74
|
+
expected,
|
|
65
75
|
key: segment.name,
|
|
66
76
|
message: `expected a single segment value, got ${Array.isArray(value) ? "an array" : typeof value}`,
|
|
77
|
+
reason: "shape",
|
|
67
78
|
});
|
|
68
79
|
}
|
|
69
80
|
else {
|
|
@@ -82,7 +93,18 @@ export function decodeParams(route, source, options) {
|
|
|
82
93
|
entries.push([segment.name, codec["~catchValue"]()]);
|
|
83
94
|
}
|
|
84
95
|
else if (error instanceof ParseError) {
|
|
85
|
-
issues.push({
|
|
96
|
+
issues.push({
|
|
97
|
+
expected,
|
|
98
|
+
key: segment.name,
|
|
99
|
+
message: error.message,
|
|
100
|
+
// "parse" vs "validate" comes from the ParseError's own
|
|
101
|
+
// selfDescribing flag — structural, never message sniffing.
|
|
102
|
+
reason: parseIssueReason(error),
|
|
103
|
+
// Issue.wire is the codec-grammar-layer value — the DECODED
|
|
104
|
+
// segment, not the raw URL text — matching decodeSearch,
|
|
105
|
+
// whose sources arrive platform-decoded.
|
|
106
|
+
wire: decoded,
|
|
107
|
+
});
|
|
86
108
|
}
|
|
87
109
|
else {
|
|
88
110
|
throw error;
|
|
@@ -98,25 +120,33 @@ export function decodeParams(route, source, options) {
|
|
|
98
120
|
}
|
|
99
121
|
else {
|
|
100
122
|
issues.push({
|
|
123
|
+
expected,
|
|
101
124
|
key: segment.name,
|
|
102
125
|
message: "required route param is missing",
|
|
126
|
+
reason: "missing",
|
|
103
127
|
});
|
|
104
128
|
}
|
|
105
129
|
continue;
|
|
106
130
|
}
|
|
107
131
|
if (!Array.isArray(value)) {
|
|
108
132
|
issues.push({
|
|
133
|
+
expected,
|
|
109
134
|
key: segment.name,
|
|
110
135
|
message: `expected catch-all values (an array), got ${typeof value}`,
|
|
136
|
+
reason: "shape",
|
|
111
137
|
});
|
|
112
138
|
continue;
|
|
113
139
|
}
|
|
114
140
|
if (segment.kind === "catchall" && value.length === 0) {
|
|
115
|
-
//
|
|
116
|
-
// hand-built props can — mirrors R3's encode-side stance.
|
|
141
|
+
// No URL produces a present-but-empty required catch-all; only
|
|
142
|
+
// hand-built props can — mirrors R3's encode-side stance. Reason
|
|
143
|
+
// "missing", not "shape": the key exists but its VALUES are missing,
|
|
144
|
+
// so pointing at the expected form is what helps.
|
|
117
145
|
issues.push({
|
|
146
|
+
expected,
|
|
118
147
|
key: segment.name,
|
|
119
148
|
message: "required catch-all received no segment values",
|
|
149
|
+
reason: "missing",
|
|
120
150
|
});
|
|
121
151
|
continue;
|
|
122
152
|
}
|
|
@@ -129,8 +159,10 @@ export function decodeParams(route, source, options) {
|
|
|
129
159
|
for (const [index, element] of elements.entries()) {
|
|
130
160
|
if (typeof element !== "string") {
|
|
131
161
|
issues.push({
|
|
162
|
+
expected,
|
|
132
163
|
key: segment.name,
|
|
133
164
|
message: `element ${String(index)}: expected a string, got ${typeof element}`,
|
|
165
|
+
reason: "shape",
|
|
134
166
|
});
|
|
135
167
|
failed = true;
|
|
136
168
|
continue;
|
|
@@ -143,7 +175,7 @@ export function decodeParams(route, source, options) {
|
|
|
143
175
|
parsed.push(codec["~parseElement"](decoded));
|
|
144
176
|
}
|
|
145
177
|
catch (error) {
|
|
146
|
-
// Element-wise recovery (
|
|
178
|
+
// Element-wise recovery (forced by D6): the codec describes ONE
|
|
147
179
|
// element, so a .catch() fallback is element-typed — each failing
|
|
148
180
|
// element recovers independently ("1","x","3" → 1, fallback, 3).
|
|
149
181
|
if (error instanceof ParseError && codec["~catchValue"] !== undefined) {
|
|
@@ -151,8 +183,13 @@ export function decodeParams(route, source, options) {
|
|
|
151
183
|
}
|
|
152
184
|
else if (error instanceof ParseError) {
|
|
153
185
|
issues.push({
|
|
186
|
+
expected,
|
|
154
187
|
key: segment.name,
|
|
155
188
|
message: `element ${String(index)}: ${error.message}`,
|
|
189
|
+
// Classified from the flag, as in the single-param branch.
|
|
190
|
+
reason: parseIssueReason(error),
|
|
191
|
+
// Grammar-layer (decoded) value, as in the single-param branch.
|
|
192
|
+
wire: decoded,
|
|
156
193
|
});
|
|
157
194
|
failed = true;
|
|
158
195
|
}
|
|
@@ -165,17 +202,17 @@ export function decodeParams(route, source, options) {
|
|
|
165
202
|
entries.push([segment.name, parsed]);
|
|
166
203
|
}
|
|
167
204
|
if (issues.length > 0) {
|
|
168
|
-
throw new ParamsDecodeError(issues);
|
|
205
|
+
throw new ParamsDecodeError(issues, route.path);
|
|
169
206
|
}
|
|
170
207
|
return Object.fromEntries(entries);
|
|
171
208
|
}
|
|
172
209
|
/**
|
|
173
210
|
* Encodes a params input into ordered, already-percent-encoded URL segment
|
|
174
|
-
* strings
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
211
|
+
* strings — ONE entry per emitted URL segment: a static segment is emitted
|
|
212
|
+
* verbatim, a single param contributes one entry (R1), a catch-all one per
|
|
213
|
+
* element (R2), an elided optional catch-all none (R3). Codec serialize
|
|
214
|
+
* errors and schema-refinement failures propagate unchanged, already branded
|
|
215
|
+
* at their own chokepoints.
|
|
179
216
|
*/
|
|
180
217
|
export function encodeParams(route, params) {
|
|
181
218
|
// The TS contract forbids non-object inputs, but plain-JS callers reach
|
|
@@ -189,8 +226,8 @@ export function encodeParams(route, params) {
|
|
|
189
226
|
const segments = [];
|
|
190
227
|
for (const segment of routeSegments(route)) {
|
|
191
228
|
if (segment.kind === "static") {
|
|
192
|
-
//
|
|
193
|
-
//
|
|
229
|
+
// The path literal is URL-shaped and emitted as-is — static segments
|
|
230
|
+
// are never re-encoded.
|
|
194
231
|
segments.push(segment.raw);
|
|
195
232
|
continue;
|
|
196
233
|
}
|
|
@@ -215,17 +252,17 @@ export function encodeParams(route, params) {
|
|
|
215
252
|
/**
|
|
216
253
|
* Encodes a params input into the per-param wire-value record the static
|
|
217
254
|
* generation surfaces expect: App Router `generateStaticParams` entries and
|
|
218
|
-
* Pages Router `getStaticPaths` `{ params }` objects
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
255
|
+
* Pages Router `getStaticPaths` `{ params }` objects. Same codec
|
|
256
|
+
* serialization and R1–R4 validation as {@link encodeParams}, with two
|
|
257
|
+
* deliberate differences: static segments are skipped (Next wants only the
|
|
258
|
+
* dynamic params, keyed by name), and values are NOT percent-encoded — Next
|
|
259
|
+
* percent-encodes static-params values itself when it materializes the
|
|
260
|
+
* concrete URLs, so pre-encoding here would double-encode (the encode-side
|
|
261
|
+
* mirror of R5's decode asymmetry). That also means the S7 lone-surrogate
|
|
262
|
+
* brand stays an encodeParams concern: strings are handed to Next verbatim.
|
|
263
|
+
* An elided optional catch-all (R3) OMITS its key — an absent key is the
|
|
264
|
+
* base-path variant on both routers (Pages also accepts undefined/[];
|
|
265
|
+
* omission is the one spelling valid on both).
|
|
229
266
|
*/
|
|
230
267
|
export function encodeStaticParams(route, params) {
|
|
231
268
|
// The TS contract forbids non-object inputs, but plain-JS callers reach
|
|
@@ -255,12 +292,12 @@ export function encodeStaticParams(route, params) {
|
|
|
255
292
|
}
|
|
256
293
|
/**
|
|
257
294
|
* Tokenizes a path literal into segments, throwing ParamourError on every
|
|
258
|
-
*
|
|
259
|
-
* and the R-rule runtime here, so encode/decode never re-derive
|
|
260
|
-
* kinds.
|
|
295
|
+
* rejected literal. Shared by the route constructors (define-time
|
|
296
|
+
* validation) and the R-rule runtime here, so encode/decode never re-derive
|
|
297
|
+
* segment kinds.
|
|
261
298
|
*/
|
|
262
299
|
export function tokenizePath(path) {
|
|
263
|
-
//
|
|
300
|
+
// Either would corrupt href's fixed path–query–fragment assembly.
|
|
264
301
|
if (path.includes("?")) {
|
|
265
302
|
throw new ParamourError(`route path must not contain "?": "${path}" (declare search params in the search config)`);
|
|
266
303
|
}
|
|
@@ -281,8 +318,8 @@ export function tokenizePath(path) {
|
|
|
281
318
|
if (raw === "") {
|
|
282
319
|
throw new ParamourError(`route path contains an empty segment: "${path}"`);
|
|
283
320
|
}
|
|
284
|
-
//
|
|
285
|
-
//
|
|
321
|
+
// Path literals are URL-shaped, so group/slot spellings are filesystem
|
|
322
|
+
// paths by definition — the most likely migration mistake.
|
|
286
323
|
if (GROUP_SEGMENT.test(raw)) {
|
|
287
324
|
throw new ParamourError(`route paths are URL-shaped: "${raw}" in "${path}" is a route-group folder name; use the URL path without it`);
|
|
288
325
|
}
|
|
@@ -291,7 +328,7 @@ export function tokenizePath(path) {
|
|
|
291
328
|
}
|
|
292
329
|
const segment = tokenizeSegment(raw, path);
|
|
293
330
|
if (segment.kind !== "static") {
|
|
294
|
-
//
|
|
331
|
+
// Not expressible as a compile error — the mapped type silently
|
|
295
332
|
// collapses duplicate keys.
|
|
296
333
|
if (seen.has(segment.name)) {
|
|
297
334
|
throw new ParamourError(`route path declares param "${segment.name}" more than once: "${path}"`);
|
|
@@ -301,7 +338,7 @@ export function tokenizePath(path) {
|
|
|
301
338
|
segments.push(segment);
|
|
302
339
|
}
|
|
303
340
|
segments.forEach((segment, index) => {
|
|
304
|
-
//
|
|
341
|
+
// Next itself requires catch-alls to be final.
|
|
305
342
|
if ((segment.kind === "catchall" || segment.kind === "optional-catchall") &&
|
|
306
343
|
index < segments.length - 1) {
|
|
307
344
|
throw new ParamourError(`catch-all segment "${segment.raw}" must be the final segment: "${path}"`);
|
|
@@ -385,9 +422,8 @@ function serializeDynamicSegment(codec, segment, value) {
|
|
|
385
422
|
// R2: each element is serialized independently; on the path surface an
|
|
386
423
|
// element containing "/" becomes %2F and round-trips as a single element —
|
|
387
424
|
// core's decodeParams restores it via percentDecodeSegment (R5), since Next
|
|
388
|
-
// hands the encoded value straight back on the params surface
|
|
389
|
-
//
|
|
390
|
-
// escaping there.
|
|
425
|
+
// hands the encoded value straight back on the params surface. The static
|
|
426
|
+
// surface passes the array whole, so "/" needs no escaping there.
|
|
391
427
|
return {
|
|
392
428
|
kind: "many",
|
|
393
429
|
values: value.map((element) => serializeSegmentValue(codec, segment.name, element)),
|
|
@@ -425,7 +461,7 @@ function tokenizeSegment(raw, path) {
|
|
|
425
461
|
if (!raw.startsWith("[...") && single?.[1] !== undefined) {
|
|
426
462
|
return { kind: "single", name: single[1], raw };
|
|
427
463
|
}
|
|
428
|
-
//
|
|
464
|
+
// The type layer lets these fall through as static text, and
|
|
429
465
|
// pre-generation there is no registry to catch them; href would otherwise
|
|
430
466
|
// emit the token verbatim.
|
|
431
467
|
if (raw.includes("[") || raw.includes("]")) {
|
package/dist/route.d.ts
CHANGED
|
@@ -3,23 +3,23 @@ import { type RouteDecodeError } from "./errors.js";
|
|
|
3
3
|
import { type ParamsSource, type PathSegment } from "./path.js";
|
|
4
4
|
import { type SearchOutputOf, type SearchSlot } from "./search.js";
|
|
5
5
|
/**
|
|
6
|
-
* `any` is deliberate (
|
|
6
|
+
* `any` is deliberate (same variance gotcha as AnyCodec): codec configs
|
|
7
7
|
* reach contravariant positions through the parse methods and `HrefArgs`;
|
|
8
8
|
* the `unknown` form would reject every concrete route.
|
|
9
9
|
*/
|
|
10
10
|
export type AnyAppRoute = AppRoute<string, any, any>;
|
|
11
|
-
/** Pages twin of {@link AnyAppRoute}
|
|
11
|
+
/** Pages twin of {@link AnyAppRoute}. */
|
|
12
12
|
export type AnyPagesRoute = PagesRoute<string, any, any>;
|
|
13
13
|
/**
|
|
14
|
-
* Router-agnostic
|
|
14
|
+
* Router-agnostic: matches both brands. This is the bound for
|
|
15
15
|
* everything that only needs the data core — `href()`, the standalone
|
|
16
16
|
* decoders, `InferRouteParams` — none of which differ by router.
|
|
17
17
|
*/
|
|
18
18
|
export type AnyRoute = Route<string, any, any>;
|
|
19
19
|
/**
|
|
20
|
-
* An App Router route
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* An App Router route: the async props-based parse surface — three surfaces
|
|
21
|
+
* × throwing/safe. Props may be promised (Next 15/16) and are awaited before
|
|
22
|
+
* any decode runs.
|
|
23
23
|
*/
|
|
24
24
|
export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> extends Route<Path, PC, SC, "app"> {
|
|
25
25
|
/**
|
|
@@ -31,9 +31,9 @@ export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC
|
|
|
31
31
|
params: ParamsOutput<Path, PC>;
|
|
32
32
|
search: SearchOutputOf<SC>;
|
|
33
33
|
}>;
|
|
34
|
-
/** Bare params object
|
|
34
|
+
/** Bare params object — layout props are structurally assignable. */
|
|
35
35
|
parseParams(props: ParamsPropsInput): Promise<ParamsOutput<Path, PC>>;
|
|
36
|
-
/** Bare search object
|
|
36
|
+
/** Bare search object — the search half alone. */
|
|
37
37
|
parseSearch(props: SearchPropsInput): Promise<SearchOutputOf<SC>>;
|
|
38
38
|
safeParse(props: RoutePropsInput): Promise<SafeResult<{
|
|
39
39
|
params: ParamsOutput<Path, PC>;
|
|
@@ -42,18 +42,18 @@ export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC
|
|
|
42
42
|
safeParseParams(props: ParamsPropsInput): Promise<SafeResult<ParamsOutput<Path, PC>>>;
|
|
43
43
|
safeParseSearch(props: SearchPropsInput): Promise<SafeResult<SearchOutputOf<SC>>>;
|
|
44
44
|
}
|
|
45
|
-
/** Names of `[...name]` catch-all segments in the path literal
|
|
45
|
+
/** Names of `[...name]` catch-all segments in the path literal. */
|
|
46
46
|
export type CatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[...${infer Name}]` ? NonEmptyName<Name> : never : never;
|
|
47
47
|
/**
|
|
48
|
-
* Exact-key enforcement
|
|
48
|
+
* Exact-key enforcement: every excess key's value type becomes `never`,
|
|
49
49
|
* so a misspelled param fails to compile on its own property line while `PC`
|
|
50
50
|
* itself stays the naked inference site for `const` codec-literal retention.
|
|
51
51
|
*/
|
|
52
52
|
export type ConformParams<Path extends string, PC> = PC & Record<Exclude<keyof PC, PathParamNames<Path>>, never>;
|
|
53
|
-
/** Decoded params object type for a route
|
|
53
|
+
/** Decoded params object type for a route; see {@link ParamsOutput}. */
|
|
54
54
|
export type InferRouteParams<R extends AnyRoute> = ParamsOutput<R["path"], R["~params"]>;
|
|
55
55
|
/**
|
|
56
|
-
* Accepts promised props and plain objects alike
|
|
56
|
+
* Accepts promised props and plain objects alike. This width lives on
|
|
57
57
|
* the parse INPUT surface ({@link RoutePropsInput} and friends), not on the
|
|
58
58
|
* annotation types: every supported Next (peer `>=15`) delivers page props
|
|
59
59
|
* as promises, and Next 15.5's generated `.next/types` page check requires
|
|
@@ -63,32 +63,32 @@ export type InferRouteParams<R extends AnyRoute> = ParamsOutput<R["path"], R["~p
|
|
|
63
63
|
*/
|
|
64
64
|
export type MaybePromise<T> = Promise<T> | T;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* An empty name (`[]`, `[...]`, `[[...]]`) is not a token — it falls through
|
|
67
|
+
* as static text; the runtime malformed-bracket check is the backstop.
|
|
68
68
|
*/
|
|
69
69
|
export type NonEmptyName<Name extends string> = Name extends "" ? never : Name;
|
|
70
70
|
/**
|
|
71
|
-
* Names of `[[...name]]` optional catch-all segments
|
|
71
|
+
* Names of `[[...name]]` optional catch-all segments. The `infer S`
|
|
72
72
|
* indirection is load-bearing: conditionals distribute only over naked type
|
|
73
73
|
* parameters, and `Segments<Path>` is an alias application, not a parameter.
|
|
74
74
|
*/
|
|
75
75
|
export type OptionalCatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${infer Name}]]` ? NonEmptyName<Name> : never : never;
|
|
76
76
|
/**
|
|
77
|
-
* Structural context contract for the pages parse surface
|
|
77
|
+
* Structural context contract for the pages parse surface: the shape
|
|
78
78
|
* `getServerSideProps` and `getInitialProps` contexts share, with no
|
|
79
79
|
* `next/*` import (the ParamsProps/SearchProps precedent). `query` is
|
|
80
80
|
* REQUIRED: `GetStaticPropsContext` has no query string, so it fails to
|
|
81
81
|
* compose here by design — typed search at build time would be a lie; the
|
|
82
82
|
* static story is core's `decodeParams`/`safeDecodeParams`. Both
|
|
83
83
|
* assignability claims are pinned per supported Next major in
|
|
84
|
-
* `examples/next-compat/src/contexts.ts
|
|
84
|
+
* `examples/next-compat/src/contexts.ts`.
|
|
85
85
|
*/
|
|
86
86
|
export interface PagesContext {
|
|
87
87
|
readonly params?: ParamsSource | undefined;
|
|
88
88
|
readonly query: ParamsSource;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* A Pages Router route
|
|
91
|
+
* A Pages Router route: the sync context-based parse surface.
|
|
92
92
|
* `getServerSideProps` / `getInitialProps` hand params and query
|
|
93
93
|
* synchronously and pre-merged, so there is no promised-props machinery
|
|
94
94
|
* here — the context split (params authoritative, query minus path-param
|
|
@@ -101,25 +101,25 @@ export interface PagesRoute<Path extends string, PC extends ParamsConfig<Path>,
|
|
|
101
101
|
* present; when absent (`getInitialProps` — `NextPageContext` has no
|
|
102
102
|
* `params` even on dynamic routes) they are extracted from `query` by
|
|
103
103
|
* segment name, which is sound because Next's own merge gives route
|
|
104
|
-
* params precedence in `query
|
|
104
|
+
* params precedence in `query`.
|
|
105
105
|
*/
|
|
106
106
|
parseContext(context: PagesContext): {
|
|
107
107
|
params: ParamsOutput<Path, PC>;
|
|
108
108
|
search: SearchOutputOf<SC>;
|
|
109
109
|
};
|
|
110
|
-
/** {@link parseContext} in the safe shape — `safely`'s taxonomy
|
|
110
|
+
/** {@link parseContext} in the safe shape — `safely`'s taxonomy. */
|
|
111
111
|
safeParseContext(context: PagesContext): SafeResult<{
|
|
112
112
|
params: ParamsOutput<Path, PC>;
|
|
113
113
|
search: SearchOutputOf<SC>;
|
|
114
114
|
}>;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
* Augmented by codegen with per-router path unions
|
|
117
|
+
* Augmented by codegen with per-router path unions:
|
|
118
118
|
* `{ appRoutes: "/a" | …; pagesRoutes: "/x" | … }`. Each member is
|
|
119
|
-
* independently ABSENT when its scan is empty
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
119
|
+
* independently ABSENT when its scan is empty — absent, never `never` —
|
|
120
|
+
* preserving per-router world-A/B independence. The generated artifact is a
|
|
121
|
+
* pure `.d.ts` module augmentation — no runtime import, so tree-shaking is
|
|
122
|
+
* untouched.
|
|
123
123
|
*/
|
|
124
124
|
export interface ParamourRegister {
|
|
125
125
|
}
|
|
@@ -127,15 +127,15 @@ export interface ParamourRegister {
|
|
|
127
127
|
export type ParamOutput<PC, K extends PropertyKey> = K extends keyof PC ? PC[K] extends AnyCodec ? OutputOf<PC[K]> : never : never;
|
|
128
128
|
/**
|
|
129
129
|
* Params schema shape for a path: one codec per dynamic segment name. The
|
|
130
|
-
* codec describes ONE segment element (
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
130
|
+
* codec describes ONE segment element (D5/D6) — arrays come from the segment
|
|
131
|
+
* kind, and presence modifiers are compile errors (`ParamCodec`). It lives
|
|
132
|
+
* here rather than in path.ts so the whole path grammar sits in one module —
|
|
133
|
+
* path.ts consumes it via type-only imports, keeping runtime imports
|
|
134
|
+
* one-directional (route.ts → path.ts).
|
|
135
135
|
*/
|
|
136
136
|
export type ParamsConfig<Path extends string> = Readonly<Record<PathParamNames<Path>, ParamCodec>>;
|
|
137
137
|
/**
|
|
138
|
-
* Parse-output shape
|
|
138
|
+
* Parse-output shape: `[id]` → `Out`, `[...slug]` → `Out[]`,
|
|
139
139
|
* `[[...slug]]` → `Out[]` — every key REQUIRED on the output side; an absent
|
|
140
140
|
* optional catch-all normalizes to `[]` at decode time (D6), so no `?:`
|
|
141
141
|
* split exists here (that split is the href-input side's concern). Keyed by
|
|
@@ -146,7 +146,7 @@ export type ParamsOutput<Path extends string, PC> = {
|
|
|
146
146
|
[K in PathParamNames<Path>]: K extends CatchAllNames<Path> | OptionalCatchAllNames<Path> ? ParamOutput<PC, K>[] : ParamOutput<PC, K>;
|
|
147
147
|
};
|
|
148
148
|
/**
|
|
149
|
-
* Structural props contract for the params half
|
|
149
|
+
* Structural props contract for the params half: layout props are
|
|
150
150
|
* assignable, and a missing member decodes like an empty source
|
|
151
151
|
* (required-missing issues, never a crash). Deliberately NOT Next's
|
|
152
152
|
* generated `PageProps` global — core stays framework-agnostic, and that
|
|
@@ -156,52 +156,52 @@ export interface ParamsProps {
|
|
|
156
156
|
readonly params?: Promise<ParamsSource>;
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
|
-
* What `parseParams` ACCEPTS
|
|
159
|
+
* What `parseParams` ACCEPTS: {@link ParamsProps} plus plain sync
|
|
160
160
|
* objects — see {@link MaybePromise} for why the annotation type is
|
|
161
161
|
* promise-only while the parse input stays wide.
|
|
162
162
|
*/
|
|
163
163
|
export interface ParamsPropsInput {
|
|
164
164
|
readonly params?: MaybePromise<ParamsSource>;
|
|
165
165
|
}
|
|
166
|
-
/** Every dynamic segment name in the path literal
|
|
166
|
+
/** Every dynamic segment name in the path literal. */
|
|
167
167
|
export type PathParamNames<Path extends string> = CatchAllNames<Path> | OptionalCatchAllNames<Path> | SingleParamNames<Path>;
|
|
168
168
|
/**
|
|
169
169
|
* Pre-generation: ParamourRegister has no `appRoutes` member, so this
|
|
170
170
|
* resolves to `string` and any path literal is accepted (unverified).
|
|
171
171
|
* Post-generation it resolves to the union of filesystem-verified app-router
|
|
172
|
-
* paths
|
|
173
|
-
*
|
|
172
|
+
* paths. Per-router on purpose: an empty app scan keeps THIS fallback while
|
|
173
|
+
* `pagesRoutes` narrows, and vice versa.
|
|
174
174
|
*/
|
|
175
175
|
export type RegisteredAppRoutePaths = ParamourRegister extends {
|
|
176
176
|
appRoutes: infer R extends string;
|
|
177
177
|
} ? R : string;
|
|
178
|
-
/** Pages twin of {@link RegisteredAppRoutePaths}
|
|
178
|
+
/** Pages twin of {@link RegisteredAppRoutePaths}. */
|
|
179
179
|
export type RegisteredPagesRoutePaths = ParamourRegister extends {
|
|
180
180
|
pagesRoutes: infer R extends string;
|
|
181
181
|
} ? R : string;
|
|
182
182
|
/**
|
|
183
|
-
* Static-only subset of {@link RegisteredAppRoutePaths}
|
|
183
|
+
* Static-only subset of {@link RegisteredAppRoutePaths}: derived by
|
|
184
184
|
* syntactic filter, not emitted — dynamic-ness is a property of the path
|
|
185
185
|
* literal, so the registry format doesn't change. Same world-A `string`
|
|
186
186
|
* fallback as the full union (the filter passes `string` through).
|
|
187
187
|
*/
|
|
188
188
|
export type RegisteredStaticAppRoutePaths = StaticPathsOf<RegisteredAppRoutePaths>;
|
|
189
|
-
/** Pages twin of {@link RegisteredStaticAppRoutePaths}
|
|
189
|
+
/** Pages twin of {@link RegisteredStaticAppRoutePaths}. */
|
|
190
190
|
export type RegisteredStaticPagesRoutePaths = StaticPathsOf<RegisteredPagesRoutePaths>;
|
|
191
191
|
/**
|
|
192
192
|
* Every registered STATIC path across both routers — the string form of
|
|
193
|
-
* href's path argument
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
193
|
+
* href's path argument. Deliberately NOT the union of the two per-router
|
|
194
|
+
* types: each falls back to `string` when its registry member is absent, and
|
|
195
|
+
* in a single-router project the absent side's `string` would swallow the
|
|
196
|
+
* union and erase verification for the router that HAS routes. The
|
|
197
|
+
* permissive fallback applies only when NEITHER member is present (world A,
|
|
198
|
+
* where codegen has merged nothing into the registry).
|
|
199
199
|
*/
|
|
200
200
|
export type RegisteredStaticRoutePaths = [PresentRegisteredPaths] extends [
|
|
201
201
|
never
|
|
202
202
|
] ? string : StaticPathsOf<PresentRegisteredPaths>;
|
|
203
203
|
/**
|
|
204
|
-
* The router-agnostic core of a defined route
|
|
204
|
+
* The router-agnostic core of a defined route: path, configs, and the
|
|
205
205
|
* define-time token cache. The parse surface is router-specific and lives on
|
|
206
206
|
* {@link AppRoute} / {@link PagesRoute} — gating it via the interface split
|
|
207
207
|
* makes the wrong surface ABSENT, not just ill-typed. `~`-prefixed members
|
|
@@ -211,7 +211,7 @@ export type RegisteredStaticRoutePaths = [PresentRegisteredPaths] extends [
|
|
|
211
211
|
export interface Route<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot, R extends RouterKind = RouterKind> {
|
|
212
212
|
readonly path: Path;
|
|
213
213
|
readonly "~params": PC;
|
|
214
|
-
/** The router brand
|
|
214
|
+
/** The router brand — type-state, same discipline as Codec's P/C/A. */
|
|
215
215
|
readonly "~router": R;
|
|
216
216
|
readonly "~search": SC;
|
|
217
217
|
/**
|
|
@@ -222,10 +222,10 @@ export interface Route<Path extends string, PC extends ParamsConfig<Path>, SC ex
|
|
|
222
222
|
readonly "~segments": readonly PathSegment[];
|
|
223
223
|
}
|
|
224
224
|
/**
|
|
225
|
-
* Conditional on the path shape
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
225
|
+
* Conditional on the path shape: dynamic paths REQUIRE `params` with exactly
|
|
226
|
+
* the extracted segment names; static paths REJECT it (`?: never` — may be
|
|
227
|
+
* absent, may never be present, which under exactOptionalPropertyTypes holds
|
|
228
|
+
* even for non-fresh objects).
|
|
229
229
|
*/
|
|
230
230
|
export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> = [PathParamNames<Path>] extends [never] ? {
|
|
231
231
|
readonly params?: never;
|
|
@@ -235,7 +235,7 @@ export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC e
|
|
|
235
235
|
readonly search?: SC;
|
|
236
236
|
};
|
|
237
237
|
/**
|
|
238
|
-
* Full page-props contract
|
|
238
|
+
* Full page-props contract: the type a page annotates its props with.
|
|
239
239
|
* Next's `PageProps` is structurally assignable, and both members are
|
|
240
240
|
* promise-only so the annotation survives Next 15.5's generated page check
|
|
241
241
|
* (see {@link MaybePromise}). Deliberately NOT Next's generated `PageProps`
|
|
@@ -243,16 +243,16 @@ export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC e
|
|
|
243
243
|
*/
|
|
244
244
|
export interface RouteProps extends ParamsProps, SearchProps {
|
|
245
245
|
}
|
|
246
|
-
/** What `parse`/`safeParse` ACCEPT
|
|
246
|
+
/** What `parse`/`safeParse` ACCEPT: {@link RouteProps} plus sync props. */
|
|
247
247
|
export interface RoutePropsInput extends ParamsPropsInput, SearchPropsInput {
|
|
248
248
|
}
|
|
249
|
-
/** Which router a route belongs to
|
|
249
|
+
/** Which router a route belongs to — the value of the `~router` brand. */
|
|
250
250
|
export type RouterKind = "app" | "pages";
|
|
251
251
|
/**
|
|
252
|
-
* Status-discriminated result shape
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
252
|
+
* Status-discriminated result shape, unified with the pages hooks'
|
|
253
|
+
* `RouterResult` (which extends this union by one `pending` member):
|
|
254
|
+
* `if (result.status === "error")` narrows both arms, and both routers'
|
|
255
|
+
* results destructure identically.
|
|
256
256
|
*/
|
|
257
257
|
export type SafeResult<T> = {
|
|
258
258
|
data: T;
|
|
@@ -262,7 +262,7 @@ export type SafeResult<T> = {
|
|
|
262
262
|
status: "error";
|
|
263
263
|
};
|
|
264
264
|
/**
|
|
265
|
-
* Structural props contract for the search half
|
|
265
|
+
* Structural props contract for the search half. The wire record
|
|
266
266
|
* shape is the same as the params side's, hence the shared source type.
|
|
267
267
|
*/
|
|
268
268
|
export interface SearchProps {
|
|
@@ -274,19 +274,19 @@ export interface SearchPropsInput {
|
|
|
274
274
|
}
|
|
275
275
|
/**
|
|
276
276
|
* Distributes a path literal into the union of its `/`-separated segment
|
|
277
|
-
* literals. Malformed bracket tokens fall through as static text —
|
|
278
|
-
* type-level path linting
|
|
277
|
+
* literals. Malformed bracket tokens fall through as static text — there is
|
|
278
|
+
* no type-level path linting; tokenizePath is the runtime backstop.
|
|
279
279
|
*/
|
|
280
280
|
export type Segments<S extends string> = S extends `${infer Head}/${infer Rest}` ? Segments<Head> | Segments<Rest> : S;
|
|
281
281
|
/**
|
|
282
|
-
* Names of single `[name]` segments
|
|
282
|
+
* Names of single `[name]` segments. Conditional order is load-bearing
|
|
283
283
|
* and mirrors tokenizePath: both catch-all forms must be excluded first or
|
|
284
284
|
* `[...slug]` would extract as a single param named `"...slug"`.
|
|
285
285
|
*/
|
|
286
286
|
export type SingleParamNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${string}]]` ? never : S extends `[...${string}]` ? never : S extends `[${infer Name}]` ? NonEmptyName<Name> : never : never;
|
|
287
287
|
/**
|
|
288
288
|
* Union of the registry members that are actually PRESENT — `never` when
|
|
289
|
-
* neither router has generated routes. The input to
|
|
289
|
+
* neither router has generated routes. The input to the combined-union
|
|
290
290
|
* fallback rule; see {@link RegisteredStaticRoutePaths}.
|
|
291
291
|
*/
|
|
292
292
|
type PresentRegisteredPaths = (ParamourRegister extends {
|
|
@@ -295,28 +295,28 @@ type PresentRegisteredPaths = (ParamourRegister extends {
|
|
|
295
295
|
pagesRoutes: infer P extends string;
|
|
296
296
|
} ? P : never);
|
|
297
297
|
/**
|
|
298
|
-
* Filters a path union to its static members
|
|
298
|
+
* Filters a path union to its static members: any `[` marks a dynamic
|
|
299
299
|
* segment. `string` passes through (it doesn't extend the bracket template),
|
|
300
300
|
* which is exactly what keeps the world-A fallback intact. Note reachability
|
|
301
|
-
* ≠ staticness
|
|
302
|
-
*
|
|
301
|
+
* ≠ staticness: `/docs/[[...slug]]` serves `/docs`, but it carries a codec
|
|
302
|
+
* and decode expectations, so it is excluded here.
|
|
303
303
|
*/
|
|
304
304
|
type StaticPathsOf<P extends string> = P extends `${string}[${string}` ? never : P;
|
|
305
305
|
/**
|
|
306
|
-
* Defines an App Router route: the URL-shaped path literal
|
|
307
|
-
* param/search codec configs. Validates the literal eagerly
|
|
308
|
-
*
|
|
309
|
-
* serialization
|
|
306
|
+
* Defines an App Router route: the URL-shaped path literal plus its
|
|
307
|
+
* param/search codec configs. Validates the literal eagerly — fail-fast at
|
|
308
|
+
* config definition time, the same stance as eager `.default()`
|
|
309
|
+
* serialization. The router is a *declaration*, not an inference:
|
|
310
310
|
* pre-codegen the registry cannot distinguish routers, so an inferred brand
|
|
311
311
|
* would silently degrade in world A — the split constructor is what keeps
|
|
312
312
|
* the brand intact there.
|
|
313
313
|
*/
|
|
314
314
|
export declare function defineAppRoute<Path extends RegisteredAppRoutePaths & string, const PC extends ParamsConfig<Path> = ParamsConfig<Path>, const SC extends SearchSlot = Record<never, never>>(path: Path, config: RouteConfig<Path, PC, SC>): AppRoute<Path, PC, SC>;
|
|
315
315
|
/**
|
|
316
|
-
* Defines a Pages Router route
|
|
316
|
+
* Defines a Pages Router route — neither router is the default; see
|
|
317
317
|
* {@link defineAppRoute} for why the constructor is split rather than
|
|
318
|
-
* inferred
|
|
319
|
-
*
|
|
318
|
+
* inferred. Same eager literal validation; the parse surface is the sync
|
|
319
|
+
* context pair.
|
|
320
320
|
*/
|
|
321
321
|
export declare function definePagesRoute<Path extends RegisteredPagesRoutePaths & string, const PC extends ParamsConfig<Path> = ParamsConfig<Path>, const SC extends Readonly<Partial<Record<PathParamNames<Path>, never>>> & SearchSlot = Record<never, never>>(path: Path, config: RouteConfig<Path, PC, SC>): PagesRoute<Path, PC, SC>;
|
|
322
322
|
export {};
|