paramour 0.0.0 → 0.1.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 +76 -0
- package/dist/codec.js +110 -0
- package/dist/describe.d.ts +63 -0
- package/dist/describe.js +75 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.js +128 -0
- package/dist/href.d.ts +59 -0
- package/dist/href.js +16 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/p.d.ts +23 -0
- package/dist/p.js +265 -0
- package/dist/path.d.ts +108 -0
- package/dist/path.js +439 -0
- package/dist/route.d.ts +253 -0
- package/dist/route.js +210 -0
- package/dist/safe-decode.d.ts +16 -0
- package/dist/safe-decode.js +42 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +16 -0
- package/dist/search.d.ts +139 -0
- package/dist/search.js +472 -0
- package/package.json +29 -5
- package/README.md +0 -28
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { AnyCodec, OutputOf, ParamCodec } from "./codec.js";
|
|
2
|
+
import { type RouteDecodeError } from "./errors.js";
|
|
3
|
+
import { type ParamsSource, type PathSegment } from "./path.js";
|
|
4
|
+
import { type SearchOutputOf, type SearchSlot } from "./search.js";
|
|
5
|
+
/**
|
|
6
|
+
* `any` is deliberate (RL4, same variance gotcha as AnyCodec): codec configs
|
|
7
|
+
* reach contravariant positions through the parse methods and `HrefArgs`;
|
|
8
|
+
* the `unknown` form would reject every concrete route.
|
|
9
|
+
*/
|
|
10
|
+
export type AnyAppRoute = AppRoute<string, any, any>;
|
|
11
|
+
/** Pages twin of {@link AnyAppRoute} (PR3). */
|
|
12
|
+
export type AnyPagesRoute = PagesRoute<string, any, any>;
|
|
13
|
+
/**
|
|
14
|
+
* Router-agnostic (PR3): matches both brands. This is the bound for
|
|
15
|
+
* everything that only needs the data core — `href()`, the standalone
|
|
16
|
+
* decoders, `InferRouteParams` — none of which differ by router.
|
|
17
|
+
*/
|
|
18
|
+
export type AnyRoute = Route<string, any, any>;
|
|
19
|
+
/**
|
|
20
|
+
* An App Router route (PR3/PR7): the async props-based parse surface —
|
|
21
|
+
* three surfaces × throwing/safe (RL1/RL6). Props may be promised (Next
|
|
22
|
+
* 15/16) and are awaited before any decode runs.
|
|
23
|
+
*/
|
|
24
|
+
export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> extends Route<Path, PC, SC, "app"> {
|
|
25
|
+
/**
|
|
26
|
+
* Decodes both props members. Awaits BOTH up front, then decodes params
|
|
27
|
+
* FIRST — a params grammar failure means the URL doesn't denote this
|
|
28
|
+
* route at all (morally a 404), so it throws before search is decoded.
|
|
29
|
+
*/
|
|
30
|
+
parse(props: RouteProps): Promise<{
|
|
31
|
+
params: ParamsOutput<Path, PC>;
|
|
32
|
+
search: SearchOutputOf<SC>;
|
|
33
|
+
}>;
|
|
34
|
+
/** Bare params object (RL6) — layout props are structurally assignable. */
|
|
35
|
+
parseParams(props: ParamsProps): Promise<ParamsOutput<Path, PC>>;
|
|
36
|
+
/** Bare search object (RL6) — the search half alone. */
|
|
37
|
+
parseSearch(props: SearchProps): Promise<SearchOutputOf<SC>>;
|
|
38
|
+
safeParse(props: RouteProps): Promise<SafeResult<{
|
|
39
|
+
params: ParamsOutput<Path, PC>;
|
|
40
|
+
search: SearchOutputOf<SC>;
|
|
41
|
+
}>>;
|
|
42
|
+
safeParseParams(props: ParamsProps): Promise<SafeResult<ParamsOutput<Path, PC>>>;
|
|
43
|
+
safeParseSearch(props: SearchProps): Promise<SafeResult<SearchOutputOf<SC>>>;
|
|
44
|
+
}
|
|
45
|
+
/** Names of `[...name]` catch-all segments in the path literal (RL3). */
|
|
46
|
+
export type CatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[...${infer Name}]` ? NonEmptyName<Name> : never : never;
|
|
47
|
+
/**
|
|
48
|
+
* Exact-key enforcement (RL1): every excess key's value type becomes `never`,
|
|
49
|
+
* so a misspelled param fails to compile on its own property line while `PC`
|
|
50
|
+
* itself stays the naked inference site for `const` codec-literal retention.
|
|
51
|
+
*/
|
|
52
|
+
export type ConformParams<Path extends string, PC> = PC & Record<Exclude<keyof PC, PathParamNames<Path>>, never>;
|
|
53
|
+
/** Decoded params object type for a route (RL3); see {@link ParamsOutput}. */
|
|
54
|
+
export type InferRouteParams<R extends AnyRoute> = ParamsOutput<R["path"], R["~params"]>;
|
|
55
|
+
/** Accepts Next 15/16's promised props and plain objects alike (RL6). */
|
|
56
|
+
export type MaybePromise<T> = Promise<T> | T;
|
|
57
|
+
/**
|
|
58
|
+
* RL3: an empty name (`[]`, `[...]`, `[[...]]`) is not a token — it falls
|
|
59
|
+
* through as static text; the runtime malformed-bracket check is the backstop.
|
|
60
|
+
*/
|
|
61
|
+
export type NonEmptyName<Name extends string> = Name extends "" ? never : Name;
|
|
62
|
+
/**
|
|
63
|
+
* Names of `[[...name]]` optional catch-all segments (RL3). The `infer S`
|
|
64
|
+
* indirection is load-bearing: conditionals distribute only over naked type
|
|
65
|
+
* parameters, and `Segments<Path>` is an alias application, not a parameter.
|
|
66
|
+
*/
|
|
67
|
+
export type OptionalCatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${infer Name}]]` ? NonEmptyName<Name> : never : never;
|
|
68
|
+
/**
|
|
69
|
+
* Structural context contract for the pages parse surface (PR10): the shape
|
|
70
|
+
* `getServerSideProps` and `getInitialProps` contexts share, with no
|
|
71
|
+
* `next/*` import (the ParamsProps/SearchProps precedent). `query` is
|
|
72
|
+
* REQUIRED: `GetStaticPropsContext` has no query string, so it fails to
|
|
73
|
+
* compose here by design — typed search at build time would be a lie; the
|
|
74
|
+
* static story is core's `decodeParams`/`safeDecodeParams`. Both
|
|
75
|
+
* assignability claims are pinned per supported Next major in
|
|
76
|
+
* `examples/next-compat/src/contexts.ts` (PR13).
|
|
77
|
+
*/
|
|
78
|
+
export interface PagesContext {
|
|
79
|
+
readonly params?: ParamsSource | undefined;
|
|
80
|
+
readonly query: ParamsSource;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A Pages Router route (PR3/PR10): the sync context-based parse surface.
|
|
84
|
+
* `getServerSideProps` / `getInitialProps` hand params and query
|
|
85
|
+
* synchronously and pre-merged, so there is no promised-props machinery
|
|
86
|
+
* here — the context split (params authoritative, query minus path-param
|
|
87
|
+
* names as search) is the whole job.
|
|
88
|
+
*/
|
|
89
|
+
export interface PagesRoute<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> extends Route<Path, PC, SC, "pages"> {
|
|
90
|
+
/**
|
|
91
|
+
* Decodes both context halves, params FIRST (same morally-a-404 rule as
|
|
92
|
+
* the app surface). `ctx.params` is authoritative for path params when
|
|
93
|
+
* present; when absent (`getInitialProps` — `NextPageContext` has no
|
|
94
|
+
* `params` even on dynamic routes) they are extracted from `query` by
|
|
95
|
+
* segment name, which is sound because Next's own merge gives route
|
|
96
|
+
* params precedence in `query` (PR10).
|
|
97
|
+
*/
|
|
98
|
+
parseContext(context: PagesContext): {
|
|
99
|
+
params: ParamsOutput<Path, PC>;
|
|
100
|
+
search: SearchOutputOf<SC>;
|
|
101
|
+
};
|
|
102
|
+
/** {@link parseContext} in the safe shape — `safely`'s taxonomy (PR12). */
|
|
103
|
+
safeParseContext(context: PagesContext): SafeResult<{
|
|
104
|
+
params: ParamsOutput<Path, PC>;
|
|
105
|
+
search: SearchOutputOf<SC>;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Augmented by codegen with per-router path unions (RL8/PR9):
|
|
110
|
+
* `{ appRoutes: "/a" | …; pagesRoutes: "/x" | … }`. Each member is
|
|
111
|
+
* independently ABSENT when its scan is empty (TR3's absent-not-`never`
|
|
112
|
+
* rule), preserving per-router world-A/B independence. The generated
|
|
113
|
+
* artifact is a pure `.d.ts` module augmentation — no runtime import, so
|
|
114
|
+
* tree-shaking is untouched (spike-01 lock-ins #3/#4).
|
|
115
|
+
*/
|
|
116
|
+
export interface ParamourRegister {
|
|
117
|
+
}
|
|
118
|
+
/** The decoded output type of the codec at key `K`, if one is declared. */
|
|
119
|
+
export type ParamOutput<PC, K extends PropertyKey> = K extends keyof PC ? PC[K] extends AnyCodec ? OutputOf<PC[K]> : never : never;
|
|
120
|
+
/**
|
|
121
|
+
* Params schema shape for a path: one codec per dynamic segment name. The
|
|
122
|
+
* codec describes ONE segment element (design-02 D5/D6) — arrays come from
|
|
123
|
+
* the segment kind, and presence modifiers are compile errors (`ParamCodec`).
|
|
124
|
+
* RL9 assigned this to path.ts; it stays here instead so the whole path
|
|
125
|
+
* grammar lives in one module — path.ts consumes it via type-only imports,
|
|
126
|
+
* keeping runtime imports one-directional (route.ts → path.ts).
|
|
127
|
+
*/
|
|
128
|
+
export type ParamsConfig<Path extends string> = Readonly<Record<PathParamNames<Path>, ParamCodec>>;
|
|
129
|
+
/**
|
|
130
|
+
* Parse-output shape (RL3): `[id]` → `Out`, `[...slug]` → `Out[]`,
|
|
131
|
+
* `[[...slug]]` → `Out[]` — every key REQUIRED on the output side; an absent
|
|
132
|
+
* optional catch-all normalizes to `[]` at decode time (D6), so no `?:`
|
|
133
|
+
* split exists here (that split is the href-input side's concern). Keyed by
|
|
134
|
+
* `Path`/`PC` so the route interfaces can name their own method return
|
|
135
|
+
* types; {@link InferRouteParams} is the route-object-facing alias.
|
|
136
|
+
*/
|
|
137
|
+
export type ParamsOutput<Path extends string, PC> = {
|
|
138
|
+
[K in PathParamNames<Path>]: K extends CatchAllNames<Path> | OptionalCatchAllNames<Path> ? ParamOutput<PC, K>[] : ParamOutput<PC, K>;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Structural props contract for the params half (RL6): layout props are
|
|
142
|
+
* assignable, and a missing member decodes like an empty source
|
|
143
|
+
* (required-missing issues, never a crash). Deliberately NOT Next's
|
|
144
|
+
* generated `PageProps` global — core stays framework-agnostic, and that
|
|
145
|
+
* global doesn't exist in fresh clones before `next dev` first runs.
|
|
146
|
+
*/
|
|
147
|
+
export interface ParamsProps {
|
|
148
|
+
readonly params?: MaybePromise<ParamsSource>;
|
|
149
|
+
}
|
|
150
|
+
/** Every dynamic segment name in the path literal (RL3). */
|
|
151
|
+
export type PathParamNames<Path extends string> = CatchAllNames<Path> | OptionalCatchAllNames<Path> | SingleParamNames<Path>;
|
|
152
|
+
/**
|
|
153
|
+
* Pre-generation: ParamourRegister has no `appRoutes` member, so this
|
|
154
|
+
* resolves to `string` and any path literal is accepted (unverified).
|
|
155
|
+
* Post-generation it resolves to the union of filesystem-verified app-router
|
|
156
|
+
* paths (RL8, spike-01). Per-router on purpose (PR9): an empty app scan
|
|
157
|
+
* keeps THIS fallback while `pagesRoutes` narrows, and vice versa.
|
|
158
|
+
*/
|
|
159
|
+
export type RegisteredAppRoutePaths = ParamourRegister extends {
|
|
160
|
+
appRoutes: infer R extends string;
|
|
161
|
+
} ? R : string;
|
|
162
|
+
/** Pages twin of {@link RegisteredAppRoutePaths} (PR9). */
|
|
163
|
+
export type RegisteredPagesRoutePaths = ParamourRegister extends {
|
|
164
|
+
pagesRoutes: infer R extends string;
|
|
165
|
+
} ? R : string;
|
|
166
|
+
/**
|
|
167
|
+
* The router-agnostic core of a defined route (PR3): path, configs, and the
|
|
168
|
+
* define-time token cache. The parse surface is router-specific and lives on
|
|
169
|
+
* {@link AppRoute} / {@link PagesRoute} — gating it via the interface split
|
|
170
|
+
* makes the wrong surface ABSENT, not just ill-typed. `~`-prefixed members
|
|
171
|
+
* are runtime-internal, not public API — same convention as codecs;
|
|
172
|
+
* `@paramour/next` is a blessed consumer, user code is not.
|
|
173
|
+
*/
|
|
174
|
+
export interface Route<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot, R extends RouterKind = RouterKind> {
|
|
175
|
+
readonly path: Path;
|
|
176
|
+
readonly "~params": PC;
|
|
177
|
+
/** The router brand (PR3) — type-state, same discipline as Codec's P/C/A. */
|
|
178
|
+
readonly "~router": R;
|
|
179
|
+
readonly "~search": SC;
|
|
180
|
+
/**
|
|
181
|
+
* `path`, tokenized once at define time so per-call encode/decode (href is
|
|
182
|
+
* per-link render work) never re-tokenizes. Per-route state, not a central
|
|
183
|
+
* registry — tree-shaking is untouched.
|
|
184
|
+
*/
|
|
185
|
+
readonly "~segments": readonly PathSegment[];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Conditional on the path shape (RL1, spike-01 lock-in #2): dynamic paths
|
|
189
|
+
* REQUIRE `params` with exactly the extracted segment names; static paths
|
|
190
|
+
* REJECT it (`?: never` — may be absent, may never be present, which under
|
|
191
|
+
* exactOptionalPropertyTypes holds even for non-fresh objects).
|
|
192
|
+
*/
|
|
193
|
+
export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> = [PathParamNames<Path>] extends [never] ? {
|
|
194
|
+
readonly params?: never;
|
|
195
|
+
readonly search?: SC;
|
|
196
|
+
} : {
|
|
197
|
+
readonly params: ConformParams<Path, PC>;
|
|
198
|
+
readonly search?: SC;
|
|
199
|
+
};
|
|
200
|
+
/** Full page-props contract (RL6): Next's `PageProps` is structurally assignable. */
|
|
201
|
+
export interface RouteProps extends ParamsProps, SearchProps {
|
|
202
|
+
}
|
|
203
|
+
/** Which router a route belongs to (PR3) — the value of the `~router` brand. */
|
|
204
|
+
export type RouterKind = "app" | "pages";
|
|
205
|
+
/**
|
|
206
|
+
* Status-discriminated result shape (RL6, design-06 PR12 — unified with the
|
|
207
|
+
* pages hooks' `RouterResult`, which extends this union by one `pending`
|
|
208
|
+
* member): `if (result.status === "error")` narrows both arms, and both
|
|
209
|
+
* routers' results destructure identically.
|
|
210
|
+
*/
|
|
211
|
+
export type SafeResult<T> = {
|
|
212
|
+
data: T;
|
|
213
|
+
status: "success";
|
|
214
|
+
} | {
|
|
215
|
+
error: RouteDecodeError;
|
|
216
|
+
status: "error";
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Structural props contract for the search half (RL6). The wire record
|
|
220
|
+
* shape is the same as the params side's, hence the shared source type.
|
|
221
|
+
*/
|
|
222
|
+
export interface SearchProps {
|
|
223
|
+
readonly searchParams?: MaybePromise<ParamsSource>;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Distributes a path literal into the union of its `/`-separated segment
|
|
227
|
+
* literals. Malformed bracket tokens fall through as static text — no
|
|
228
|
+
* type-level path linting (RL3); tokenizePath is the runtime backstop.
|
|
229
|
+
*/
|
|
230
|
+
export type Segments<S extends string> = S extends `${infer Head}/${infer Rest}` ? Segments<Head> | Segments<Rest> : S;
|
|
231
|
+
/**
|
|
232
|
+
* Names of single `[name]` segments (RL3). Conditional order is load-bearing
|
|
233
|
+
* and mirrors tokenizePath: both catch-all forms must be excluded first or
|
|
234
|
+
* `[...slug]` would extract as a single param named `"...slug"`.
|
|
235
|
+
*/
|
|
236
|
+
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;
|
|
237
|
+
/**
|
|
238
|
+
* Defines an App Router route: the URL-shaped path literal (RL2) plus its
|
|
239
|
+
* param/search codec configs. Validates the literal eagerly (RL1 —
|
|
240
|
+
* fail-fast at config definition time, same stance as eager `.default()`
|
|
241
|
+
* serialization). The router is a *declaration*, not an inference (PR7):
|
|
242
|
+
* pre-codegen the registry cannot distinguish routers, so an inferred brand
|
|
243
|
+
* would silently degrade in world A — the split constructor is what keeps
|
|
244
|
+
* the brand intact there.
|
|
245
|
+
*/
|
|
246
|
+
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>;
|
|
247
|
+
/**
|
|
248
|
+
* Defines a Pages Router route (PR7 — neither router is the default; see
|
|
249
|
+
* {@link defineAppRoute} for why the constructor is split rather than
|
|
250
|
+
* inferred). Same eager literal validation (RL1); the parse surface is the
|
|
251
|
+
* sync context pair (PR10).
|
|
252
|
+
*/
|
|
253
|
+
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>;
|
package/dist/route.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { foreignMessage, ParamourError, ParamsDecodeError, SearchDecodeError, } from "./errors.js";
|
|
2
|
+
import { decodeParams, tokenizePath, } from "./path.js";
|
|
3
|
+
import { decodeSearch, } from "./search.js";
|
|
4
|
+
/**
|
|
5
|
+
* Defines an App Router route: the URL-shaped path literal (RL2) plus its
|
|
6
|
+
* param/search codec configs. Validates the literal eagerly (RL1 —
|
|
7
|
+
* fail-fast at config definition time, same stance as eager `.default()`
|
|
8
|
+
* serialization). The router is a *declaration*, not an inference (PR7):
|
|
9
|
+
* pre-codegen the registry cannot distinguish routers, so an inferred brand
|
|
10
|
+
* would silently degrade in world A — the split constructor is what keeps
|
|
11
|
+
* the brand intact there.
|
|
12
|
+
*/
|
|
13
|
+
export function defineAppRoute(path, config) {
|
|
14
|
+
const route = {
|
|
15
|
+
...routeData("app", path, config),
|
|
16
|
+
async parse(props) {
|
|
17
|
+
const [paramsSource, searchSource] = await awaitProps(props);
|
|
18
|
+
// RL6: params first — a params failure throws before search decodes.
|
|
19
|
+
const decodedParams = decodeParams(route, paramsSource ?? {});
|
|
20
|
+
return {
|
|
21
|
+
params: decodedParams,
|
|
22
|
+
search: decodeSearch(route["~search"], searchSource ?? {}),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
async parseParams(props) {
|
|
26
|
+
const source = await awaitProp(props.params);
|
|
27
|
+
return decodeParams(route, source ?? {});
|
|
28
|
+
},
|
|
29
|
+
async parseSearch(props) {
|
|
30
|
+
const source = await awaitProp(props.searchParams);
|
|
31
|
+
return decodeSearch(route["~search"], source ?? {});
|
|
32
|
+
},
|
|
33
|
+
safeParse(props) {
|
|
34
|
+
return safely(() => route.parse(props));
|
|
35
|
+
},
|
|
36
|
+
safeParseParams(props) {
|
|
37
|
+
return safely(() => route.parseParams(props));
|
|
38
|
+
},
|
|
39
|
+
safeParseSearch(props) {
|
|
40
|
+
return safely(() => route.parseSearch(props));
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return route;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Defines a Pages Router route (PR7 — neither router is the default; see
|
|
47
|
+
* {@link defineAppRoute} for why the constructor is split rather than
|
|
48
|
+
* inferred). Same eager literal validation (RL1); the parse surface is the
|
|
49
|
+
* sync context pair (PR10).
|
|
50
|
+
*/
|
|
51
|
+
export function definePagesRoute(path, config) {
|
|
52
|
+
const data = routeData("pages", path, config);
|
|
53
|
+
// PR10: the query→params extraction and query→search subtraction both key
|
|
54
|
+
// on the dynamic-segment names; computed once at define time (the
|
|
55
|
+
// ~segments ethos — per-call parses never re-derive them).
|
|
56
|
+
const paramNames = new Set();
|
|
57
|
+
for (const segment of data["~segments"]) {
|
|
58
|
+
if (segment.kind !== "static")
|
|
59
|
+
paramNames.add(segment.name);
|
|
60
|
+
}
|
|
61
|
+
const route = {
|
|
62
|
+
...data,
|
|
63
|
+
parseContext(context) {
|
|
64
|
+
const [paramsSource, searchSource] = splitPagesContext(context, paramNames);
|
|
65
|
+
// PR10: params first — same morally-a-404 rule as the app surface.
|
|
66
|
+
// R5: pages sources (ctx.params / ctx.query) are already percent-decoded
|
|
67
|
+
// by Node's querystring layer, so skip core's decode to avoid a
|
|
68
|
+
// double-decode (App-Router's decodeParams keeps the default).
|
|
69
|
+
const decodedParams = decodeParams(route, paramsSource, {
|
|
70
|
+
percentDecode: false,
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
params: decodedParams,
|
|
74
|
+
search: decodeSearch(route["~search"], searchSource),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
safeParseContext(context) {
|
|
78
|
+
// safely's taxonomy (PR12), minus the await: only decode failures
|
|
79
|
+
// become the error arm; contract violations stay loud.
|
|
80
|
+
try {
|
|
81
|
+
return { data: route.parseContext(context), status: "success" };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof ParamsDecodeError ||
|
|
85
|
+
error instanceof SearchDecodeError) {
|
|
86
|
+
return { error, status: "error" };
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
return route;
|
|
93
|
+
}
|
|
94
|
+
/** Single-member twin of {@link awaitProps}, for the bare-surface methods. */
|
|
95
|
+
function awaitProp(value) {
|
|
96
|
+
return rebrandRejection(Promise.resolve(value));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Awaits BOTH props members before any decode runs (RL6): the
|
|
100
|
+
* params-before-search rule is about *decode* order, not await order —
|
|
101
|
+
* throwing while the searchParams promise is still pending would turn a
|
|
102
|
+
* rejecting props promise into an unhandled rejection.
|
|
103
|
+
*/
|
|
104
|
+
function awaitProps(props) {
|
|
105
|
+
return rebrandRejection(Promise.all([props.params, props.searchParams]));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Own enumerable properties of `source` whose keys are NOT in `keys` —
|
|
109
|
+
* the query→search subtraction (PR10). Entries → fromEntries so keys like
|
|
110
|
+
* "__proto__" stay ordinary own properties (decodeParams's ethos).
|
|
111
|
+
*/
|
|
112
|
+
function omitOwn(source, keys) {
|
|
113
|
+
return Object.fromEntries(Object.entries(source).filter(([key]) => !keys.has(key)));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Own properties of `source` at exactly `keys` — the query→params
|
|
117
|
+
* extraction (PR10). A name missing from the source is simply omitted, so
|
|
118
|
+
* it surfaces downstream as decodeParams's ordinary required-missing issue,
|
|
119
|
+
* never a crash here.
|
|
120
|
+
*/
|
|
121
|
+
function pickOwn(source, keys) {
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
if (Object.hasOwn(source, key))
|
|
125
|
+
entries.push([key, source[key]]);
|
|
126
|
+
}
|
|
127
|
+
return Object.fromEntries(entries);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A props promise is user/framework code — a rejection is branded at this
|
|
131
|
+
* chokepoint (paramour's own errors pass through), keeping the "every throw
|
|
132
|
+
* is a ParamourError" contract. ONE deliberate exception: Next's control-flow
|
|
133
|
+
* errors carry a string `digest` (`DYNAMIC_SERVER_USAGE`, `NEXT_REDIRECT`, …
|
|
134
|
+
* — the same convention `unstable_rethrow` keys on) and MUST propagate
|
|
135
|
+
* unwrapped. Next rejects the searchParams promise itself with the
|
|
136
|
+
* dynamic-usage sentinel during a `generateStaticParams` prerender; wrapping
|
|
137
|
+
* it hides the digest, and what should be a graceful bail-to-dynamic becomes
|
|
138
|
+
* a failed build.
|
|
139
|
+
*/
|
|
140
|
+
async function rebrandRejection(promise) {
|
|
141
|
+
try {
|
|
142
|
+
return await promise;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof ParamourError)
|
|
146
|
+
throw error;
|
|
147
|
+
if (typeof error === "object" &&
|
|
148
|
+
error !== null &&
|
|
149
|
+
typeof error.digest === "string") {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
throw new ParamourError(`route props promise rejected: ${foreignMessage(error)}`, { cause: error });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Shared define-time core of both constructors (PR7): validates the literal
|
|
157
|
+
* eagerly (RL1 — throws ParamourError on an invalid literal) and pins the
|
|
158
|
+
* data members both parse surfaces build on. The conditional RouteConfig is
|
|
159
|
+
* unresolved inside a generic body; the cast here is the one place its two
|
|
160
|
+
* branches are unified.
|
|
161
|
+
*/
|
|
162
|
+
function routeData(router, path, config) {
|
|
163
|
+
const segments = tokenizePath(path);
|
|
164
|
+
const { params, search } = config;
|
|
165
|
+
return {
|
|
166
|
+
path,
|
|
167
|
+
"~params": params ?? {},
|
|
168
|
+
"~router": router,
|
|
169
|
+
"~search": search ?? {},
|
|
170
|
+
"~segments": segments,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Wraps a throwing parse into the status-discriminated shape (RL6, PR12).
|
|
175
|
+
* Only decode failures become the `error` arm; source-contract violations
|
|
176
|
+
* and rebranded foreign errors stay loud.
|
|
177
|
+
*/
|
|
178
|
+
async function safely(run) {
|
|
179
|
+
try {
|
|
180
|
+
return { data: await run(), status: "success" };
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof ParamsDecodeError ||
|
|
184
|
+
error instanceof SearchDecodeError) {
|
|
185
|
+
return { error, status: "error" };
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Splits a pages context into its params/search decode sources (PR10).
|
|
192
|
+
* `params` is authoritative when present — handed to decodeParams whole,
|
|
193
|
+
* whose own contract check rejects a garbage member; absent, path params
|
|
194
|
+
* are extracted from `query` by name. Search is always `query` minus the
|
|
195
|
+
* path-param names. A missing `query` is a CONTRACT violation, not a decode
|
|
196
|
+
* issue: `getStaticProps` has no query string, so composing its context
|
|
197
|
+
* here would be a lie (PR10) — the error names the supported path instead.
|
|
198
|
+
*/
|
|
199
|
+
function splitPagesContext(context, paramNames) {
|
|
200
|
+
const untrusted = context;
|
|
201
|
+
if (typeof untrusted !== "object" || untrusted === null) {
|
|
202
|
+
throw new ParamourError(`pages context must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
|
|
203
|
+
}
|
|
204
|
+
const { params, query } = untrusted;
|
|
205
|
+
const untrustedQuery = query;
|
|
206
|
+
if (typeof untrustedQuery !== "object" || untrustedQuery === null) {
|
|
207
|
+
throw new ParamourError(`pages context has no query object (got ${untrustedQuery === null ? "null" : typeof untrustedQuery}): getStaticProps contexts carry no query string — decode ctx.params with safeDecodeParams instead (PR10)`);
|
|
208
|
+
}
|
|
209
|
+
return [params ?? pickOwn(query, paramNames), omitOwn(query, paramNames)];
|
|
210
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnyRoute, InferRouteParams, SafeResult } from "./route.js";
|
|
2
|
+
import { type DecodeParamsOptions, type ParamsSource } from "./path.js";
|
|
3
|
+
import { type SearchOutputOf, type SearchSource } from "./search.js";
|
|
4
|
+
/**
|
|
5
|
+
* Sync `SafeResult` twins of {@link decodeParams} / {@link decodeSearch}
|
|
6
|
+
* (RL6's stance at the standalone-function layer): the route methods'
|
|
7
|
+
* `safeParse*` surface awaits props, but sync callers — client hooks,
|
|
8
|
+
* middleware, route handlers — already hold a decoded-value-layer source.
|
|
9
|
+
* Same taxonomy as route.ts's `safely`: only a decode failure becomes the
|
|
10
|
+
* `error` arm; source-contract violations, rebranded foreign errors, and
|
|
11
|
+
* async-schema misuse (design-02 D7) stay loud and propagate unchanged.
|
|
12
|
+
*/
|
|
13
|
+
/** Decoded route params as a `SafeResult` (discriminated on `status`, PR12). */
|
|
14
|
+
export declare function safeDecodeParams<R extends AnyRoute>(route: R, source: ParamsSource, options?: DecodeParamsOptions): SafeResult<InferRouteParams<R>>;
|
|
15
|
+
/** Decoded search params as a `SafeResult` (discriminated on `status`, PR12). */
|
|
16
|
+
export declare function safeDecodeSearch<R extends AnyRoute>(route: R, source: SearchSource): SafeResult<SearchOutputOf<R["~search"]>>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ParamsDecodeError, SearchDecodeError } from "./errors.js";
|
|
2
|
+
import { decodeParams, } from "./path.js";
|
|
3
|
+
import { decodeSearch, } from "./search.js";
|
|
4
|
+
/**
|
|
5
|
+
* Sync `SafeResult` twins of {@link decodeParams} / {@link decodeSearch}
|
|
6
|
+
* (RL6's stance at the standalone-function layer): the route methods'
|
|
7
|
+
* `safeParse*` surface awaits props, but sync callers — client hooks,
|
|
8
|
+
* middleware, route handlers — already hold a decoded-value-layer source.
|
|
9
|
+
* Same taxonomy as route.ts's `safely`: only a decode failure becomes the
|
|
10
|
+
* `error` arm; source-contract violations, rebranded foreign errors, and
|
|
11
|
+
* async-schema misuse (design-02 D7) stay loud and propagate unchanged.
|
|
12
|
+
*/
|
|
13
|
+
/** Decoded route params as a `SafeResult` (discriminated on `status`, PR12). */
|
|
14
|
+
export function safeDecodeParams(route, source, options) {
|
|
15
|
+
try {
|
|
16
|
+
return { data: decodeParams(route, source, options), status: "success" };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof ParamsDecodeError)
|
|
20
|
+
return { error, status: "error" };
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Decoded search params as a `SafeResult` (discriminated on `status`, PR12). */
|
|
25
|
+
export function safeDecodeSearch(route, source) {
|
|
26
|
+
try {
|
|
27
|
+
// decodeSearch is keyed on SearchOutputOf (design-04 SS6) — the correct
|
|
28
|
+
// public type — but AnyRoute erases its SC to `any`, so for a still-
|
|
29
|
+
// generic R the call's value side reduces to `unknown` while the
|
|
30
|
+
// annotation side stays deferred. The cast bridges that inference gap to
|
|
31
|
+
// the SAME (correct) type.
|
|
32
|
+
return {
|
|
33
|
+
data: decodeSearch(route["~search"], source),
|
|
34
|
+
status: "success",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof SearchDecodeError)
|
|
39
|
+
return { error, status: "error" };
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
/**
|
|
3
|
+
* Runs a Standard Schema synchronously and returns the raw result. Standard
|
|
4
|
+
* Schema permits async validation, but URL parsing must be sync — an async
|
|
5
|
+
* schema is a documented runtime error (design-02 D7). Shared by `p.ts`
|
|
6
|
+
* (which joins `result.issues` into one message string) and the raw-search
|
|
7
|
+
* decode path (which needs structured `Issue[]` with `path`) — each call
|
|
8
|
+
* site maps issues its own way (plan-04 step 1).
|
|
9
|
+
*/
|
|
10
|
+
export declare function runStandardSchemaSync<Out>(schema: StandardSchemaV1<unknown, Out>, value: unknown): StandardSchemaV1.Result<Out>;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ParamourError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Runs a Standard Schema synchronously and returns the raw result. Standard
|
|
4
|
+
* Schema permits async validation, but URL parsing must be sync — an async
|
|
5
|
+
* schema is a documented runtime error (design-02 D7). Shared by `p.ts`
|
|
6
|
+
* (which joins `result.issues` into one message string) and the raw-search
|
|
7
|
+
* decode path (which needs structured `Issue[]` with `path`) — each call
|
|
8
|
+
* site maps issues its own way (plan-04 step 1).
|
|
9
|
+
*/
|
|
10
|
+
export function runStandardSchemaSync(schema, value) {
|
|
11
|
+
const result = schema["~standard"].validate(value);
|
|
12
|
+
if (result instanceof Promise) {
|
|
13
|
+
throw new ParamourError("Async Standard Schema validation is not supported: URL parsing must be synchronous");
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|