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.
@@ -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
+ }
@@ -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
+ }