@paramour-js/next 0.8.0 → 0.9.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/app.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AnyAppRoute, type InferRouteParams, type SafeResult, type SearchOutputOf } from "paramour";
1
+ import { type AnyAppRoute, type InferRouteParams, type InferRouteSearch, ParamsDecodeError, type SafeResult, SearchDecodeError } from "paramour";
2
2
  import { type SelectOptions } from "./select.js";
3
3
  export type { SelectOptions } from "./select.js";
4
4
  /**
@@ -30,7 +30,7 @@ export type { SelectOptions } from "./select.js";
30
30
  * render, to the nearest client error boundary.
31
31
  *
32
32
  * Both read the route's blessed-internal `~search` / `~params` via the core
33
- * decoders — `@paramour/next` is a sanctioned consumer of those internals.
33
+ * decoders — `@paramour-js/next` is a sanctioned consumer of those internals.
34
34
  *
35
35
  * Every hook is gated to `AnyAppRoute`: a pages-branded route at one of
36
36
  * these call sites is a compile error, not a runtime surprise — these hooks
@@ -70,8 +70,8 @@ export type { SelectOptions } from "./select.js";
70
70
  * Core keeps its loud throw for genuinely non-object sources from plain-JS
71
71
  * callers; the null tolerance lives here at the adapter.
72
72
  */
73
- export declare function useRouteParams<R extends AnyAppRoute>(route: R): SafeResult<InferRouteParams<R>>;
74
- export declare function useRouteParams<R extends AnyAppRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): SafeResult<U>;
73
+ export declare function useRouteParams<R extends AnyAppRoute>(route: R): SafeResult<InferRouteParams<R>, ParamsDecodeError>;
74
+ export declare function useRouteParams<R extends AnyAppRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): SafeResult<U, ParamsDecodeError>;
75
75
  /**
76
76
  * Decoded route params, or a thrown {@link ParamsDecodeError} (→ nearest
77
77
  * client error boundary) on a malformed URL. Optionally projected through
@@ -87,12 +87,12 @@ export declare function useRouteParamsOrThrow<R extends AnyAppRoute, U>(route: R
87
87
  * Decoded search params as a `SafeResult` (discriminated on `status`),
88
88
  * optionally projected through `options.select`.
89
89
  */
90
- export declare function useSearch<R extends AnyAppRoute>(route: R): SafeResult<SearchOutputOf<R["~search"]>>;
91
- export declare function useSearch<R extends AnyAppRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): SafeResult<U>;
90
+ export declare function useSearch<R extends AnyAppRoute>(route: R): SafeResult<InferRouteSearch<R>, SearchDecodeError>;
91
+ export declare function useSearch<R extends AnyAppRoute, U>(route: R, options: SelectOptions<InferRouteSearch<R>, U>): SafeResult<U, SearchDecodeError>;
92
92
  /**
93
93
  * Decoded search params, or a thrown {@link SearchDecodeError} (→ nearest
94
94
  * client error boundary) on a malformed URL. Optionally projected through
95
95
  * `options.select`.
96
96
  */
97
- export declare function useSearchOrThrow<R extends AnyAppRoute>(route: R): SearchOutputOf<R["~search"]>;
98
- export declare function useSearchOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): U;
97
+ export declare function useSearchOrThrow<R extends AnyAppRoute>(route: R): InferRouteSearch<R>;
98
+ export declare function useSearchOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<InferRouteSearch<R>, U>): U;
package/dist/app.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { useParams, usePathname, useRouter, useSearchParams, } from "next/navigation";
3
3
  import { decodeParams, decodeSearch, ParamsDecodeError, safeDecodeParams, safeDecodeSearch, SearchDecodeError, } from "paramour";
4
4
  import { useContext } from "react";
5
- import { searchWireSnapshot } from "./devtools-seam.js";
5
+ import { searchWireSnapshot } from "./devtools-emit.js";
6
6
  import { AppNavigationContext, } from "./navigation-adapter.js";
7
7
  import { makeAppNavigate, useDevtoolsEmitter, } from "./observe.js";
8
8
  import { paramsFingerprint, searchParamsFingerprint, useSelectedResult, useSelectedValue, useStableResult, } from "./select.js";
@@ -127,21 +127,14 @@ export function useSearchOrThrow(route, options) {
127
127
  wire: () => searchWireSnapshot(searchParams),
128
128
  };
129
129
  const value = useStableResult(route, searchParamsFingerprint(route, searchParams),
130
- // decodeSearch is keyed on SearchOutputOf (SS6) — the correct
131
- // public type — but AnyAppRoute erases its SC to `any`, so for a still-
132
- // generic R the call's SearchOutputOf<R["~search"]> reduces to `unknown`
133
- // on the value side while staying deferred on the annotation side. The
134
- // cast bridges that inference gap to the SAME (correct) type, so a
135
- // rawSearch route now infers its schema output here, not a garbage
136
- // {~kind, ~schema} shape. The cast appears in both branches below — the
137
- // prod/dev split (and its duplicated decode call) is the price of
130
+ // The prod/dev split (and its duplicated decode call) is the price of
138
131
  // literal-zero prod cost; the bundler keeps exactly one branch.
139
132
  () => {
140
133
  if (process.env.NODE_ENV === "production") {
141
- return decodeSearch(route["~search"], searchParams, route.path);
134
+ return decodeSearch(route, searchParams);
142
135
  }
143
136
  try {
144
- const data = decodeSearch(route["~search"], searchParams, route.path);
137
+ const data = decodeSearch(route, searchParams);
145
138
  if (spec !== undefined) {
146
139
  emitter.observe(spec, { data, status: "success" });
147
140
  }
@@ -1,3 +1,4 @@
1
+ import { ParamourError } from "paramour";
1
2
  import { type ParamourConfig } from "./config.js";
2
3
  import { type GenerateInputs } from "./generate.js";
3
4
  /**
@@ -15,7 +16,8 @@ export interface InputFlags {
15
16
  * treat it like any other exit-2 error, but `init` downgrades it to a
16
17
  * warn-and-skip — a fresh project legitimately has no app/ or pages/ yet.
17
18
  */
18
- export declare class NoRouteDirsError extends Error {
19
+ export declare class NoRouteDirsError extends ParamourError {
20
+ readonly name: "NoRouteDirsError";
19
21
  }
20
22
  /**
21
23
  * Precedence lives in exactly this function: flags → config file → joint
@@ -26,6 +28,8 @@ export declare class NoRouteDirsError extends Error {
26
28
  * exists is that an error: app-only and pages-only projects are both fine.
27
29
  *
28
30
  * Commands that already loaded the config file (for fields beyond these,
29
- * e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once.
31
+ * e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once. The
32
+ * `withTypedRoutes` wrapper resolves through here too, with no flags, so
33
+ * the CLI and `next dev`/`next build` always agree on dirs and artifact.
30
34
  */
31
- export declare function resolveInputs(flags: InputFlags, projectRoot: string, preloaded?: ParamourConfig): Promise<GenerateInputs>;
35
+ export declare function resolveInputs(flags: InputFlags, projectRoot: string, preloaded?: ParamourConfig, pageExtensionsOverride?: readonly string[]): Promise<GenerateInputs>;
@@ -1,5 +1,6 @@
1
1
  import { statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { ParamourError } from "paramour";
3
4
  import { loadConfigFile } from "./config.js";
4
5
  import {} from "./generate.js";
5
6
  import { DEFAULT_PAGE_EXTENSIONS } from "./scan-app.js";
@@ -9,7 +10,8 @@ import { resolveRouteDirs } from "./scan.js";
9
10
  * treat it like any other exit-2 error, but `init` downgrades it to a
10
11
  * warn-and-skip — a fresh project legitimately has no app/ or pages/ yet.
11
12
  */
12
- export class NoRouteDirsError extends Error {
13
+ export class NoRouteDirsError extends ParamourError {
14
+ name = "NoRouteDirsError";
13
15
  }
14
16
  /**
15
17
  * Precedence lives in exactly this function: flags → config file → joint
@@ -20,11 +22,16 @@ export class NoRouteDirsError extends Error {
20
22
  * exists is that an error: app-only and pages-only projects are both fine.
21
23
  *
22
24
  * Commands that already loaded the config file (for fields beyond these,
23
- * e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once.
25
+ * e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once. The
26
+ * `withTypedRoutes` wrapper resolves through here too, with no flags, so
27
+ * the CLI and `next dev`/`next build` always agree on dirs and artifact.
24
28
  */
25
- export async function resolveInputs(flags, projectRoot, preloaded) {
29
+ export async function resolveInputs(flags, projectRoot, preloaded, pageExtensionsOverride) {
26
30
  const file = preloaded ?? (await loadConfigFile(projectRoot))?.config;
27
- const pageExtensions = parsePageExtensions(flags["page-extensions"]) ??
31
+ // The override is withTypedRoutes' Next-authoritative extension list:
32
+ // inside `next dev`/`next build`, what Next routes on wins over the file.
33
+ const pageExtensions = pageExtensionsOverride ??
34
+ parsePageExtensions(flags["page-extensions"]) ??
28
35
  file?.pageExtensions ??
29
36
  DEFAULT_PAGE_EXTENSIONS;
30
37
  const explicitAppDir = flags["app-dir"] ?? file?.appDir;
@@ -1,3 +1,4 @@
1
+ import { ParamourError } from "paramour";
1
2
  /** A scanned route path labeled with the router that produced it. */
2
3
  export interface ScannedRoute {
3
4
  path: string;
@@ -11,8 +12,9 @@ export interface ScannedRoute {
11
12
  * collision mid-`--watch` is usually a file mid-move, so the last good
12
13
  * artifact stays on disk).
13
14
  */
14
- export declare class RouteCollisionError extends Error {
15
- name: string;
15
+ export declare class RouteCollisionError extends ParamourError {
16
+ readonly name: "RouteCollisionError";
17
+ static [Symbol.hasInstance](value: unknown): value is RouteCollisionError;
16
18
  }
17
19
  /**
18
20
  * Structural collisions — same detection pass, non-equal strings. Two
@@ -1,3 +1,5 @@
1
+ import { ParamourError } from "paramour";
2
+ const routeCollisionErrorBrand = Symbol.for("paramour.errors.RouteCollisionError");
1
3
  /**
2
4
  * Route-collision failure mode: states Next itself refuses to build have no
3
5
  * valid artifact, so the scanners throw instead of emitting one. Composition
@@ -6,8 +8,21 @@
6
8
  * collision mid-`--watch` is usually a file mid-move, so the last good
7
9
  * artifact stays on disk).
8
10
  */
9
- export class RouteCollisionError extends Error {
11
+ export class RouteCollisionError extends ParamourError {
12
+ static {
13
+ // Same cross-copy identity brand scheme as core's error classes: a
14
+ // realm-global Symbol.for() key on the prototype, so `instanceof`
15
+ // recognizes instances from a second physical copy of this package.
16
+ Object.defineProperty(this.prototype, routeCollisionErrorBrand, {
17
+ value: true,
18
+ });
19
+ }
10
20
  name = "RouteCollisionError";
21
+ static [Symbol.hasInstance](value) {
22
+ return (typeof value === "object" &&
23
+ value !== null &&
24
+ value[routeCollisionErrorBrand] === true);
25
+ }
11
26
  }
12
27
  /**
13
28
  * `[[...name]]` → optional catch-all; used for the specificity check below.
package/dist/config.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Shape of `paramour.config.{ts,mjs,json}` — the CLI's config file. Every
3
- * field is optional; the CLI's precedence is flags → this file → inference.
4
- * `.ts`/`.mjs` files default-export this object.
2
+ * Shape of `paramour.config.{ts,mjs,json}` — read by the CLI and by
3
+ * `withTypedRoutes`. Every field is optional; the CLI's precedence is flags →
4
+ * this file → inference. `.ts`/`.mjs` files default-export this object.
5
5
  */
6
6
  export interface ParamourConfig {
7
7
  /** App dir, relative to the project root; default: joint discovery. */
@@ -0,0 +1,40 @@
1
+ import type { ParamsSource } from "paramour";
2
+ import type { ParamourDevtoolsSeam, ParamourObservation, ParamourSearchWire } from "./devtools-seam.js";
3
+ /**
4
+ * The hooks' side of the devtools seam: the emit helpers behind the
5
+ * contract in `devtools-seam.ts`. Internal — never exported from the
6
+ * package; the panel re-implements the attach side against the contract.
7
+ * The emitted JS here imports NOTHING (every import above is type-only) —
8
+ * load-bearing for the production erasure described in the contract.
9
+ */
10
+ /**
11
+ * 128: replay only needs the pre-panel-mount window. One observation per
12
+ * decode CHANGE per hook means even a long pre-open session is dozens
13
+ * of entries, not thousands; the panel keys on route, so depth beyond
14
+ * "every route seen recently" adds nothing — the cap mostly bounds how many
15
+ * live route/result references the buffer retains.
16
+ */
17
+ export declare const OBSERVATION_BUFFER_CAP = 128;
18
+ /**
19
+ * Pushes one observation and notifies listeners. The internal production
20
+ * early-return is belt-and-suspenders (every call site is ALSO guarded,
21
+ * which is what the bundler erases); it makes the guard directly
22
+ * unit-testable and keeps a future unguarded call site failing safe.
23
+ */
24
+ export declare function emitObservation(observation: ParamourObservation): void;
25
+ /**
26
+ * The slot, created on first touch by whichever side (hooks or panel) runs
27
+ * first.
28
+ */
29
+ export declare function getParamourSeam(): ParamourDevtoolsSeam;
30
+ /**
31
+ * Pages `query` record → wire pairs; `string[]` values expand to repeated
32
+ * keys in array order, `undefined` values are wire absence and are skipped.
33
+ */
34
+ export declare function recordWireSnapshot(source: ParamsSource): ParamourSearchWire;
35
+ /**
36
+ * Decode-time freeze of the (live, mutable) `URLSearchParams` into wire
37
+ * pairs: the observation outlives the render in the ring buffer, so it must
38
+ * capture what the DECODE saw, not a live view.
39
+ */
40
+ export declare function searchWireSnapshot(source: URLSearchParams): ParamourSearchWire;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The hooks' side of the devtools seam: the emit helpers behind the
3
+ * contract in `devtools-seam.ts`. Internal — never exported from the
4
+ * package; the panel re-implements the attach side against the contract.
5
+ * The emitted JS here imports NOTHING (every import above is type-only) —
6
+ * load-bearing for the production erasure described in the contract.
7
+ */
8
+ /**
9
+ * 128: replay only needs the pre-panel-mount window. One observation per
10
+ * decode CHANGE per hook means even a long pre-open session is dozens
11
+ * of entries, not thousands; the panel keys on route, so depth beyond
12
+ * "every route seen recently" adds nothing — the cap mostly bounds how many
13
+ * live route/result references the buffer retains.
14
+ */
15
+ export const OBSERVATION_BUFFER_CAP = 128;
16
+ const SEAM_KEY = Symbol.for("paramour.devtools.seam");
17
+ const globalSlots = globalThis;
18
+ /**
19
+ * Pushes one observation and notifies listeners. The internal production
20
+ * early-return is belt-and-suspenders (every call site is ALSO guarded,
21
+ * which is what the bundler erases); it makes the guard directly
22
+ * unit-testable and keeps a future unguarded call site failing safe.
23
+ */
24
+ export function emitObservation(observation) {
25
+ if (process.env.NODE_ENV === "production")
26
+ return;
27
+ const seam = getParamourSeam();
28
+ seam.buffer.push(observation);
29
+ if (seam.buffer.length > OBSERVATION_BUFFER_CAP)
30
+ seam.buffer.shift();
31
+ for (const listener of seam.listeners) {
32
+ try {
33
+ listener(observation);
34
+ }
35
+ catch {
36
+ // A panel bug must never break app render — emit runs render-phase.
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * The slot, created on first touch by whichever side (hooks or panel) runs
42
+ * first.
43
+ */
44
+ export function getParamourSeam() {
45
+ const existing = globalSlots[SEAM_KEY];
46
+ if (existing !== undefined)
47
+ return existing;
48
+ const created = {
49
+ buffer: [],
50
+ listeners: new Set(),
51
+ version: 1,
52
+ };
53
+ globalSlots[SEAM_KEY] = created;
54
+ return created;
55
+ }
56
+ /**
57
+ * Pages `query` record → wire pairs; `string[]` values expand to repeated
58
+ * keys in array order, `undefined` values are wire absence and are skipped.
59
+ */
60
+ export function recordWireSnapshot(source) {
61
+ const pairs = [];
62
+ for (const [key, value] of Object.entries(source)) {
63
+ if (value === undefined)
64
+ continue;
65
+ if (Array.isArray(value)) {
66
+ for (const element of value)
67
+ pairs.push([key, element]);
68
+ }
69
+ else {
70
+ pairs.push([key, value]);
71
+ }
72
+ }
73
+ return pairs;
74
+ }
75
+ /**
76
+ * Decode-time freeze of the (live, mutable) `URLSearchParams` into wire
77
+ * pairs: the observation outlives the render in the ring buffer, so it must
78
+ * capture what the DECODE saw, not a live view.
79
+ */
80
+ export function searchWireSnapshot(source) {
81
+ const pairs = [];
82
+ for (const [key, value] of source)
83
+ pairs.push([key, value]);
84
+ return pairs;
85
+ }
@@ -27,9 +27,14 @@ import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
27
27
  * - Production: every emit call site sits behind
28
28
  * `process.env.NODE_ENV !== "production"`, which Next's compilers
29
29
  * constant-fold; with the package's `sideEffects: false` the then-dead
30
- * import of this module is dropped entirely. The emitted JS here imports
31
- * NOTHING (the one `paramour` import is type-only) — load-bearing for
32
- * that erasure.
30
+ * import of the emitter module (`devtools-emit.ts`) is dropped entirely.
31
+ * - Stability: this module is the published, semver-covered contract. It
32
+ * declares TYPES ONLY — the emit/attach helpers live in the internal
33
+ * `devtools-emit.ts`, so a value import through the types-only
34
+ * `./devtools-seam` entry can't type-check. Closed-looking unions here
35
+ * (`ParamourHookId`, the observation `kind`s) are OPEN by policy: new
36
+ * hooks and observation kinds ship in minor releases, so consumers must
37
+ * tolerate members they don't recognize.
33
38
  */
34
39
  /** The `Symbol.for("paramour.devtools.seam")` slot shape — the seam contract. */
35
40
  export interface ParamourDevtoolsSeam {
@@ -43,7 +48,10 @@ export interface ParamourDevtoolsSeam {
43
48
  */
44
49
  readonly version: 1;
45
50
  }
46
- /** Discriminant naming which hook reported. */
51
+ /**
52
+ * Discriminant naming which hook reported. Open by policy: a new hook adds a
53
+ * member in a minor release, so switch over it with a default branch.
54
+ */
47
55
  export type ParamourHookId = "app.useRouteParams" | "app.useRouteParamsOrThrow" | "app.useSearch" | "app.useSearchOrThrow" | "pages.useRouteParams" | "pages.useSearch";
48
56
  /**
49
57
  * Navigation capability captured from the EMITTING hook's router: the panel
@@ -66,32 +74,8 @@ export type ParamourNavigate = (search: string) => void;
66
74
  * captured `navigate`/`pathname` never go stale while the hook is mounted.
67
75
  */
68
76
  export type ParamourObservation = ParamourParamsObservation | ParamourSearchObservation;
69
- /**
70
- * Pre-`select` decode result: the hook's full `SafeResult` — the error arm
71
- * carries the LIVE `ParamsDecodeError`/`SearchDecodeError` with its
72
- * `issues` — never the user's `select` projection. `pending` is the
73
- * Pages-only third state. Generic-erased on purpose: the panel treats
74
- * `data` structurally.
75
- */
76
- export type ParamourObservationResult = SafeResult<unknown> | {
77
- readonly status: "pending";
78
- };
79
- /** Params decode: wire is a decode-time shallow copy of the source record. */
80
- export interface ParamourParamsObservation extends ParamourObservationBase {
81
- readonly kind: "params";
82
- readonly wire: Readonly<ParamsSource>;
83
- }
84
- /**
85
- * Search decode: wire is decode-time `[key, value]` pairs in wire order —
86
- * order is load-bearing for repeated keys (P5/S5), and pairs round-trip
87
- * losslessly into the panel's raw-wire editing.
88
- */
89
- export interface ParamourSearchObservation extends ParamourObservationBase {
90
- readonly kind: "search";
91
- readonly wire: ParamourSearchWire;
92
- }
93
- export type ParamourSearchWire = readonly (readonly [string, string])[];
94
- interface ParamourObservationBase {
77
+ /** Fields every observation carries, whatever its `kind`. */
78
+ export interface ParamourObservationBase {
95
79
  readonly hook: ParamourHookId;
96
80
  readonly navigate: ParamourNavigate;
97
81
  /**
@@ -114,34 +98,27 @@ interface ParamourObservationBase {
114
98
  readonly routerKind: RouterKind;
115
99
  }
116
100
  /**
117
- * 128: replay only needs the pre-panel-mount window. One observation per
118
- * decode CHANGE per hook means even a long pre-open session is dozens
119
- * of entries, not thousands; the panel keys on route, so depth beyond
120
- * "every route seen recently" adds nothing — the cap mostly bounds how many
121
- * live route/result references the buffer retains.
122
- */
123
- export declare const OBSERVATION_BUFFER_CAP = 128;
124
- /**
125
- * Pushes one observation and notifies listeners. The internal production
126
- * early-return is belt-and-suspenders (every call site is ALSO guarded,
127
- * which is what the bundler erases); it makes the guard directly
128
- * unit-testable and keeps a future unguarded call site failing safe.
129
- */
130
- export declare function emitObservation(observation: ParamourObservation): void;
131
- /**
132
- * The slot, created on first touch by whichever side (hooks or panel) runs
133
- * first.
134
- */
135
- export declare function getParamourSeam(): ParamourDevtoolsSeam;
136
- /**
137
- * Pages `query` record → wire pairs; `string[]` values expand to repeated
138
- * keys in array order, `undefined` values are wire absence and are skipped.
101
+ * Pre-`select` decode result: the hook's full `SafeResult` — the error arm
102
+ * carries the LIVE `ParamsDecodeError`/`SearchDecodeError` with its
103
+ * `issues` — never the user's `select` projection. `pending` is the
104
+ * Pages-only third state. Generic-erased on purpose: the panel treats
105
+ * `data` structurally.
139
106
  */
140
- export declare function recordWireSnapshot(source: ParamsSource): ParamourSearchWire;
107
+ export type ParamourObservationResult = SafeResult<unknown> | {
108
+ readonly status: "pending";
109
+ };
110
+ /** Params decode: wire is a decode-time shallow copy of the source record. */
111
+ export interface ParamourParamsObservation extends ParamourObservationBase {
112
+ readonly kind: "params";
113
+ readonly wire: Readonly<ParamsSource>;
114
+ }
141
115
  /**
142
- * Decode-time freeze of the (live, mutable) `URLSearchParams` into wire
143
- * pairs: the observation outlives the render in the ring buffer, so it must
144
- * capture what the DECODE saw, not a live view.
116
+ * Search decode: wire is decode-time `[key, value]` pairs in wire order —
117
+ * order is load-bearing for repeated keys (P5/S5), and pairs round-trip
118
+ * losslessly into the panel's raw-wire editing.
145
119
  */
146
- export declare function searchWireSnapshot(source: URLSearchParams): ParamourSearchWire;
147
- export {};
120
+ export interface ParamourSearchObservation extends ParamourObservationBase {
121
+ readonly kind: "search";
122
+ readonly wire: ParamourSearchWire;
123
+ }
124
+ export type ParamourSearchWire = readonly (readonly [string, string])[];
@@ -1,78 +1 @@
1
- /**
2
- * 128: replay only needs the pre-panel-mount window. One observation per
3
- * decode CHANGE per hook means even a long pre-open session is dozens
4
- * of entries, not thousands; the panel keys on route, so depth beyond
5
- * "every route seen recently" adds nothing — the cap mostly bounds how many
6
- * live route/result references the buffer retains.
7
- */
8
- export const OBSERVATION_BUFFER_CAP = 128;
9
- const SEAM_KEY = Symbol.for("paramour.devtools.seam");
10
- const globalSlots = globalThis;
11
- /**
12
- * Pushes one observation and notifies listeners. The internal production
13
- * early-return is belt-and-suspenders (every call site is ALSO guarded,
14
- * which is what the bundler erases); it makes the guard directly
15
- * unit-testable and keeps a future unguarded call site failing safe.
16
- */
17
- export function emitObservation(observation) {
18
- if (process.env.NODE_ENV === "production")
19
- return;
20
- const seam = getParamourSeam();
21
- seam.buffer.push(observation);
22
- if (seam.buffer.length > OBSERVATION_BUFFER_CAP)
23
- seam.buffer.shift();
24
- for (const listener of seam.listeners) {
25
- try {
26
- listener(observation);
27
- }
28
- catch {
29
- // A panel bug must never break app render — emit runs render-phase.
30
- }
31
- }
32
- }
33
- /**
34
- * The slot, created on first touch by whichever side (hooks or panel) runs
35
- * first.
36
- */
37
- export function getParamourSeam() {
38
- const existing = globalSlots[SEAM_KEY];
39
- if (existing !== undefined)
40
- return existing;
41
- const created = {
42
- buffer: [],
43
- listeners: new Set(),
44
- version: 1,
45
- };
46
- globalSlots[SEAM_KEY] = created;
47
- return created;
48
- }
49
- /**
50
- * Pages `query` record → wire pairs; `string[]` values expand to repeated
51
- * keys in array order, `undefined` values are wire absence and are skipped.
52
- */
53
- export function recordWireSnapshot(source) {
54
- const pairs = [];
55
- for (const [key, value] of Object.entries(source)) {
56
- if (value === undefined)
57
- continue;
58
- if (Array.isArray(value)) {
59
- for (const element of value)
60
- pairs.push([key, element]);
61
- }
62
- else {
63
- pairs.push([key, value]);
64
- }
65
- }
66
- return pairs;
67
- }
68
- /**
69
- * Decode-time freeze of the (live, mutable) `URLSearchParams` into wire
70
- * pairs: the observation outlives the render in the ring buffer, so it must
71
- * capture what the DECODE saw, not a live view.
72
- */
73
- export function searchWireSnapshot(source) {
74
- const pairs = [];
75
- for (const [key, value] of source)
76
- pairs.push([key, value]);
77
- return pairs;
78
- }
1
+ export {};
@@ -206,8 +206,6 @@ function readManifest(projectRoot, name) {
206
206
  }
207
207
  }
208
208
  }
209
- /** `1.2.3` / `1.2.3-beta.1` — the shape a published `workspace:*` pin takes. */
210
- const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/;
211
209
  function versionCheck(projectRoot) {
212
210
  const coreManifest = readManifest(projectRoot, "paramour");
213
211
  const nextManifest = readManifest(projectRoot, "@paramour-js/next");
@@ -228,27 +226,22 @@ function versionCheck(projectRoot) {
228
226
  status: "fail",
229
227
  };
230
228
  }
231
- // The packages version INDEPENDENTLY (changesets); comparing the two
232
- // installed versions against each other warns on every correct install.
233
- // The real invariant is that the installed core is the one the installed
234
- // @paramour-js/next declares — `workspace:*` publishes as an exact pin, so
235
- // when the declaration is exact this is string equality. A non-exact
236
- // declaration (a range, or `workspace:*` inside this monorepo itself) is
237
- // the package manager's to enforce; no claim to check.
238
- const declared = nextManifest?.dependencies?.paramour;
239
- if (declared !== undefined &&
240
- EXACT_VERSION.test(declared) &&
241
- core !== declared) {
229
+ // The paramour packages release in LOCKSTEP (one changesets fixed group),
230
+ // and @paramour-js/next peers on the app's own `paramour` — so a coherent
231
+ // install has the two at the same version. The peer range itself is the
232
+ // package manager's to enforce; lockstep equality is the stronger claim,
233
+ // and the one that catches a half-upgraded app.
234
+ if (core !== next) {
242
235
  return {
243
236
  detail: [
244
- "your package manager should have matched these — check for overrides/resolutions or a stale lockfile, then reinstall",
237
+ "paramour packages release together — upgrade them to the same version (e.g. `pnpm up paramour @paramour-js/next`)",
245
238
  ],
246
- label: `versions: installed paramour ${core} != ${declared}, the version @paramour-js/next ${next} depends on`,
239
+ label: `versions: paramour ${core} != @paramour-js/next ${next}`,
247
240
  status: "warn",
248
241
  };
249
242
  }
250
243
  return {
251
- label: `versions: paramour ${core} satisfies @paramour-js/next ${next}'s declared dependency`,
244
+ label: `versions: paramour and @paramour-js/next are both ${core}`,
252
245
  status: "pass",
253
246
  };
254
247
  }
package/dist/observe.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useRef } from "react";
2
- import { emitObservation } from "./devtools-seam.js";
2
+ import { emitObservation } from "./devtools-emit.js";
3
3
  /**
4
4
  * App-flavor navigate capability: `next/navigation`'s `replace` returns void
5
5
  * and resolves the basePath-/locale-relative join itself.
package/dist/pages.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AnyPagesRoute, type InferRouteParams, type SafeResult, type SearchOutputOf } from "paramour";
1
+ import { type AnyPagesRoute, type InferRouteParams, type InferRouteSearch, type ParamsDecodeError, type RouteDecodeError, type SafeResult, type SearchDecodeError } from "paramour";
2
2
  import { type SelectOptions } from "./select.js";
3
3
  export type { SelectOptions } from "./select.js";
4
4
  /**
@@ -40,18 +40,18 @@ export type { SelectOptions } from "./select.js";
40
40
  * page. Literally `SafeResult<T> | { status: "pending" }`, so both routers'
41
41
  * results destructure identically.
42
42
  */
43
- export type RouterResult<T> = SafeResult<T> | {
43
+ export type RouterResult<T, E extends RouteDecodeError = RouteDecodeError> = SafeResult<T, E> | {
44
44
  status: "pending";
45
45
  };
46
46
  /**
47
47
  * Decoded route params as a {@link RouterResult}, optionally projected
48
48
  * through `options.select`.
49
49
  */
50
- export declare function useRouteParams<R extends AnyPagesRoute>(route: R): RouterResult<InferRouteParams<R>>;
51
- export declare function useRouteParams<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): RouterResult<U>;
50
+ export declare function useRouteParams<R extends AnyPagesRoute>(route: R): RouterResult<InferRouteParams<R>, ParamsDecodeError>;
51
+ export declare function useRouteParams<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): RouterResult<U, ParamsDecodeError>;
52
52
  /**
53
53
  * Decoded search params as a {@link RouterResult}, optionally projected
54
54
  * through `options.select`.
55
55
  */
56
- export declare function useSearch<R extends AnyPagesRoute>(route: R): RouterResult<SearchOutputOf<R["~search"]>>;
57
- export declare function useSearch<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): RouterResult<U>;
56
+ export declare function useSearch<R extends AnyPagesRoute>(route: R): RouterResult<InferRouteSearch<R>, SearchDecodeError>;
57
+ export declare function useSearch<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<InferRouteSearch<R>, U>): RouterResult<U, SearchDecodeError>;
package/dist/pages.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { useRouter } from "next/router.js";
8
8
  import { ParamourError, safeDecodeParams, safeDecodeSearch, } from "paramour";
9
9
  import { useContext } from "react";
10
- import { recordWireSnapshot } from "./devtools-seam.js";
10
+ import { recordWireSnapshot } from "./devtools-emit.js";
11
11
  import { PagesNavigationContext, } from "./navigation-adapter.js";
12
12
  import { makePagesNavigate, useDevtoolsEmitter, } from "./observe.js";
13
13
  import { paramsFingerprint, PENDING_FINGERPRINT, queryFingerprint, useSelectedResult, useStableResult, } from "./select.js";
package/dist/select.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AnyRoute, ParamsSource, SafeResult } from "paramour";
1
+ import type { AnyRoute, ParamsSource, RouteDecodeError, SafeResult } from "paramour";
2
2
  /**
3
3
  * Shared internals of the read hooks' selector surface: the raw-slice
4
4
  * stabilization layer and the selector layer. Deliberately NO `"use client"`
@@ -72,10 +72,10 @@ export declare function searchParamsFingerprint(route: AnyRoute, source: URLSear
72
72
  * Error and pending arms pass through untouched; they are already
73
73
  * reference-stabilized by {@link useStableResult}'s raw-slice layer.
74
74
  */
75
- export declare function useSelectedResult<T, U>(result: SafeResult<T>, options: SelectOptions<T, U> | undefined): SafeResult<U>;
76
- export declare function useSelectedResult<T, U>(result: SafeResult<T> | {
75
+ export declare function useSelectedResult<T, U, E extends RouteDecodeError>(result: SafeResult<T, E>, options: SelectOptions<T, U> | undefined): SafeResult<U, E>;
76
+ export declare function useSelectedResult<T, U, E extends RouteDecodeError>(result: SafeResult<T, E> | {
77
77
  status: "pending";
78
- }, options: SelectOptions<T, U> | undefined): SafeResult<U> | {
78
+ }, options: SelectOptions<T, U> | undefined): SafeResult<U, E> | {
79
79
  status: "pending";
80
80
  };
81
81
  /**
@@ -1,11 +1,9 @@
1
- /** Options for {@link withTypedRoutes}. */
1
+ /**
2
+ * Options for {@link withTypedRoutes}. Where routes and the artifact live is
3
+ * NOT configured here: the wrapper reads `paramour.config.*` exactly as the
4
+ * CLI does, so the two writers can never produce different artifacts.
5
+ */
2
6
  export interface WithTypedRoutesOptions {
3
- /**
4
- * Artifact location, for monorepos where the Next app root isn't where the
5
- * file should live — the escape hatch. Relative paths resolve against the
6
- * project root. Default: `paramour-env.d.ts` at the project root.
7
- */
8
- outFile?: string;
9
7
  /**
10
8
  * Upgrade build-phase drift from a loud warning to a build failure — for
11
9
  * teams that want the committed artifact to be the law. Default `false`,
@@ -1,9 +1,10 @@
1
- import { resolve } from "node:path";
1
+ import { NoRouteDirsError, resolveInputs } from "./cli-inputs.js";
2
2
  import { RouteCollisionError } from "./collisions.js";
3
+ import { loadConfigFile } from "./config.js";
3
4
  import { diffGenerated, formatRouteDiff, generate, } from "./generate.js";
4
5
  import { acquireWatcherLock, watcherLockPath, } from "./lock.js";
5
6
  import { DEFAULT_PAGE_EXTENSIONS } from "./scan-app.js";
6
- import { resolveRouteDirs } from "./scan.js";
7
+ import {} from "./scan.js";
7
8
  import { watchRouteDirs } from "./watch.js";
8
9
  /**
9
10
  * Phase constants from `next/constants`, hardcoded so the package stays
@@ -59,20 +60,37 @@ export function withTypedRoutes(config, options = {}) {
59
60
  if (phase !== PHASE_DEVELOPMENT_SERVER && phase !== PHASE_PRODUCTION_BUILD)
60
61
  return resolved;
61
62
  // The dev server and every build worker evaluate the config with the
62
- // project root as cwd; the CLI flags are the home for anything more
63
- // configurable than this.
63
+ // project root as cwd — the same root the CLI resolves against.
64
64
  const projectRoot = process.cwd();
65
- const artifactPath = resolve(projectRoot, options.outFile ?? "paramour-env.d.ts");
65
+ // A malformed paramour.config throws here, like the populated-ignored-dir
66
+ // discovery error: both are configuration mistakes, not incidental
67
+ // generation failures, so they stay loud (see above).
68
+ const file = (await loadConfigFile(projectRoot))?.config;
69
+ // Next's pageExtensions is authoritative inside Next — it decides what is
70
+ // a page. A config file that disagrees would make the CLI scan a
71
+ // different route set, so say so once.
66
72
  const pageExtensions = resolved.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;
67
- // May throw the populated-ignored-dir config error — deliberately not
68
- // caught (see above).
69
- const dirs = resolveRouteDirs(projectRoot, pageExtensions);
70
- if (dirs.appDir === undefined && dirs.pagesDir === undefined) {
73
+ if (file?.pageExtensions !== undefined &&
74
+ file.pageExtensions.join(",") !== pageExtensions.join(",")) {
75
+ warnOnce(`paramour: paramour.config pageExtensions (${file.pageExtensions.join(", ")}) differ from Next's (${pageExtensions.join(", ")}); generation inside Next uses Next's — align the config file so \`paramour generate\` agrees`);
76
+ }
77
+ let inputs;
78
+ try {
79
+ inputs = await resolveInputs({}, projectRoot, file, pageExtensions);
80
+ }
81
+ catch (error) {
82
+ if (!(error instanceof NoRouteDirsError))
83
+ throw error;
71
84
  // Codegen is never load-bearing — a config wrapper must not take down
72
85
  // `next dev`/`next build` over a missing route dir.
73
86
  warnOnce(`paramour: no route directory (app/, pages/, src/app/, or src/pages/) under ${projectRoot}; route generation skipped`);
74
87
  return resolved;
75
88
  }
89
+ const { artifactPath } = inputs;
90
+ const dirs = {
91
+ appDir: inputs.appDir,
92
+ pagesDir: inputs.pagesDir,
93
+ };
76
94
  if (phase === PHASE_PRODUCTION_BUILD) {
77
95
  generateForBuild(dirs, pageExtensions, artifactPath, options.strict ?? false);
78
96
  return resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paramour-js/next",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -33,11 +33,11 @@
33
33
  "dependencies": {
34
34
  "jiti": "^2.7.0",
35
35
  "magicast": "^0.3.5",
36
- "tinyglobby": "^0.2.15",
37
- "paramour": "0.8.0"
36
+ "tinyglobby": "^0.2.15"
38
37
  },
39
38
  "peerDependencies": {
40
39
  "next": ">=15",
40
+ "paramour": ">=0.9.0 <1.0.0 || ^1.0.0-rc.0",
41
41
  "react": ">=18.2.0"
42
42
  },
43
43
  "devDependencies": {
@@ -50,7 +50,8 @@
50
50
  "react": "^19.2.0",
51
51
  "react-dom": "^19.2.0",
52
52
  "typescript": "^6.0.3",
53
- "zod": "^4.4.3"
53
+ "zod": "^4.4.3",
54
+ "paramour": "0.9.0"
54
55
  },
55
56
  "description": "Next.js integration for paramour: withTypedRoutes, App and Pages Router hooks, PageProps glue, and the codegen CLI.",
56
57
  "author": "Jason Paff <jasonpaff@gmail.com>",
@@ -18,7 +18,7 @@ Wire grammars are strict and anchored — no `Number()` coercion, no whitespace,
18
18
  | `p.boolean()` | `boolean` | Exactly `"true"` / `"false"`. | — |
19
19
  | `p.enum(members)` | union of members | Exact member match. `p.enum(["asc", "desc"])` decodes to `"asc" \| "desc"`. | Non-empty readonly string tuple |
20
20
  | `p.isoDate()` | `Date` | `YYYY-MM-DD`, real calendar dates only (rejects `2026-02-30`); serializes UTC date part. | — |
21
- | `p.timestamp()` | `Date` | ISO 8601 UTC only (`...T..:..:..[.mmm]Z`, offsets rejected); serializes `Date#toISOString()`. | — |
21
+ | `p.timestamp()` | `Date` | ISO 8601 (`...T..:..:..[.mmm]` + `Z` or `±HH:MM`; offsets decode to the same instant); always serializes UTC `Date#toISOString()`. | — |
22
22
  | `p.json(schema)` | schema output | `JSON.parse` then schema; serialize re-validates then `JSON.stringify`. | Standard Schema (required) |
23
23
  | `p.index(schema?)` | `number` | 1-based on the wire, 0-based in memory: `?page=1` ↔ `0`. Wire `< 1` is a parse failure; negative in-memory index is a `SerializeError`. | Optional Standard Schema `<number, number>` (validates the 0-based value) |
24
24
  | `p.csv(element?)` | `E[]` | ONE wire value, comma-joined (`?tags=a,b`). Empty wire string is `[]`; `a,,b` / trailing comma are parse failures; serializing an element that is empty or contains a comma is a `SerializeError`. Arity "single" — full modifier set applies. | Optional element codec (default `p.string()`); element must be an unmodified scalar, no nested csv |
@@ -77,7 +77,7 @@ export default async function ProductPage(props: RouteProps) {
77
77
  - "May be absent, no fallback" (`typeof sp.x === "string" ? sp.x : undefined`) → `.optional()`. Decoded as `T | undefined`.
78
78
  - Silent-coercion tolerance (old code shrugged off garbage, e.g. `Number(...)` producing `NaN` handled downstream) → add `.catch(fallback)` so a malformed PRESENT value falls back instead of failing the decode. `.catch()` never covers absence — combine with `.default()`/`.optional()` for that.
79
79
  - Multi-value keys (`sp.tags` handled as `string | string[]`) → `p.array()` for repeated keys (`?tags=a&tags=b`) or `p.csv()` for one comma-joined key (`?tags=a,b`). Match whichever wire form the app already emits.
80
- - Enumerated strings → `p.enum(["a", "b"])`; numbers → `p.number()`; booleans (`sp.flag === "true"`) → `p.boolean()`; dates → `p.isoDate()` (YYYY-MM-DD) or `p.timestamp()` (ISO UTC).
80
+ - Enumerated strings → `p.enum(["a", "b"])`; numbers → `p.number()`; booleans (`sp.flag === "true"`) → `p.boolean()`; dates → `p.isoDate()` (YYYY-MM-DD) or `p.timestamp()` (ISO instant; emits UTC).
81
81
 
82
82
  ### Behavior change to decide explicitly
83
83
 
@@ -22,28 +22,28 @@ Runtime values:
22
22
  | `encodeParams(route, params)` | Encoded path segments as `string[]` |
23
23
  | `encodeStaticParams(route, params)` | Per-param wire-string record for `generateStaticParams` / `getStaticPaths` |
24
24
  | `decodeParams(route, source, opts?)` | Sync params decode; throws `ParamsDecodeError`; `opts: { percentDecode?: boolean }` (default true — App Router) |
25
- | `decodeSearch(config, source, routePath?)` | Sync search decode; throws `SearchDecodeError`; unknown keys ignored |
25
+ | `decodeSearch(routeOrConfig, source)` | Sync search decode; throws `SearchDecodeError`; unknown keys ignored; a route anchors errors to its path |
26
26
  | `safeDecodeParams` / `safeDecodeSearch` | `SafeResult`-returning twins of the two decoders |
27
- | `encodeSearch(config, input)` | Decoded values → ordered wire pairs `[string, string][]` (default elision applied) |
27
+ | `encodeSearch(routeOrConfig, input)` | Decoded values → ordered wire pairs `[string, string][]` (default elision applied) |
28
28
  | `buildSearchString(pairs)` | Pairs → `?…` string (`%20`, never `+`) |
29
- | `searchToString(config, input)` | `encodeSearch` + `buildSearchString` |
30
- | `serializeValue(codec, label, value)` | One value through a codec's serializer, string contract enforced |
29
+ | `searchToString(routeOrConfig, input)` | `encodeSearch` + `buildSearchString` |
30
+ | `serializeValue(codec, label, value)` / `parseValue(codec, raw)` | One value through a codec's serializer (string contract enforced) / parser (no `.catch()` recovery) |
31
31
  | `rawSearch(schema)` / `isRawSearch(config)` | Whole-object search escape hatch and its discriminant |
32
32
  | `standardSearchSchema(route)` | Export a route's search config as a Standard Schema (tRPC input, TanStack `validateSearch`) |
33
33
  | `describeCodec(codec)` / `describeRoute(route)` / `formatCodecDescription(desc, style)` | Reflection over codec/route metadata (powers `paramour list`) |
34
34
  | `ParamourError, ParseError, SerializeError, ParamsDecodeError, SearchDecodeError, SearchSourceError` | Error classes (brand-hardened `instanceof`) |
35
35
 
36
- Key types: `Codec`, `AnyCodec`, `OutputOf`, `ParamCodec`, `Presence`, `PresenceOf`, `Arity`; `AppRoute`, `PagesRoute`, `Route`, `AnyRoute`, `AnyAppRoute`, `AnyPagesRoute`, `RouterKind`, `PagesContext`; `RouteProps`, `ParamsProps`, `SearchProps` (+ `*Input` sync-accepting forms) — annotate page/layout props with these; `InferRouteParams`, `SearchOutputOf`, `InferSearchInput`, `InferSearchOutput`, `InferStaticParams`, `InferHrefInput`, `HrefArgs`, `Href`, `StaticHrefOptions`; `SafeResult`, `RouteDecodeError`, `Issue`, `IssueReason`; `ParamsConfig`, `SearchConfig`, `ParamsSource`, `SearchSource`, `RawSearch`, `StandardSearchSchema`; `ParamourRegister` + `Registered*RoutePaths` (codegen augmentation targets); `CodecDescription`, `RouteDescription`, `ParamDescription`, `SearchDescription`, `CodecDefaultDescription`, `CodecFormatStyle`; `DecodeParamsOptions`.
36
+ Key types: `Codec`, `AnyCodec` (optionally narrowed to one output type: any codec state producing it), `CodecKind`, `InferCodecOutput`, `ParamCodec`, `Presence`, `PresenceOf`, `Arity`; `AppRoute`, `PagesRoute`, `Route`, `AnyRoute`, `AnyAppRoute`, `AnyPagesRoute`, `RouterKind`, `PagesContext`, `RouteConfig` + `SearchSlot` (for wrapping `define*Route`); `RouteProps`, `ParamsProps`, `SearchProps` — annotate page/layout props with these — and their sync-accepting parse-input forms `RoutePropsLike`, `ParamsPropsLike`, `SearchPropsLike`; `InferRouteParams`, `InferRouteSearch` (a route's decoded params / search), `InferSearchOutput`, `InferSearchInput` (of a `search:` slot), `InferStaticParams`, `InferHrefInput`, `HrefArgs`, `Href`, `StaticHrefOptions`; `SafeResult` (its optional second parameter narrows the error arm to the params or search decode error on one-sided surfaces), `RouteDecodeError`, `Issue`, `IssueReason`; `ParamsConfig`, `SearchConfig`, `ParamsSource`, `SearchSource`, `RawSearch`, `StandardSearchSchema`; `ParamourRegister` + `Registered*RoutePaths` (codegen augmentation targets); `CodecDescription`, `RouteDescription`, `ParamDescription`, `SearchDescription`, `CodecDefaultDescription`, `CodecFormatStyle`; `DecodeParamsOptions`.
37
37
 
38
38
  ## `@paramour-js/next` exports
39
39
 
40
- | Entry point | Exports |
41
- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | `@paramour-js/next` | `withTypedRoutes(config, options?)` (`options: { outFile?, strict? }`), `RouteCollisionError`, types `WithTypedRoutesOptions`, `ParamourConfig` |
43
- | `@paramour-js/next/app` | `useRouteParams`, `useRouteParamsOrThrow`, `useSearch`, `useSearchOrThrow` (all `(route, options?)` with `options: { select, equality?: "shallow" }`), type `SelectOptions` |
44
- | `@paramour-js/next/pages` | `useRouteParams`, `useSearch` (return `RouterResult` = `SafeResult` + `{ status: "pending" }`), types `RouterResult`, `SelectOptions` |
45
- | `@paramour-js/next/testing` | `ParamourTestingProvider`, `withParamourTesting(options?)`, type `ParamourTestingOptions` (`isReady, mounted, onReplace, params, pathname, search`) |
46
- | `@paramour-js/next/devtools-seam` | Types-only seam contract consumed by `@paramour-js/devtools-panel`; not needed in app code |
40
+ | Entry point | Exports |
41
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `@paramour-js/next` | `withTypedRoutes(config, options?)` (`options: { strict? }`; everything else comes from `paramour.config`), `RouteCollisionError`, types `WithTypedRoutesOptions`, `ParamourConfig` |
43
+ | `@paramour-js/next/app` | `useRouteParams`, `useRouteParamsOrThrow`, `useSearch`, `useSearchOrThrow` (all `(route, options?)` with `options: { select, equality?: "shallow" }`), type `SelectOptions` |
44
+ | `@paramour-js/next/pages` | `useRouteParams`, `useSearch` (return `RouterResult` = `SafeResult` + `{ status: "pending" }`), types `RouterResult`, `SelectOptions` |
45
+ | `@paramour-js/next/testing` | `ParamourTestingProvider`, `withParamourTesting(options?)`, type `ParamourTestingOptions` (`isReady, mounted, onReplace, params, pathname, search`) |
46
+ | `@paramour-js/next/devtools-seam` | Types-only seam contract consumed by `@paramour-js/devtools-panel`; not needed in app code |
47
47
 
48
48
  ## CLI (`paramour <command>`, bin shipped by `@paramour-js/next`)
49
49
 
@@ -68,7 +68,7 @@ Precedence: CLI flags → config file → discovery. Unknown keys are rejected (
68
68
  | ---------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ |
69
69
  | `appDir` | discovered `app/`/`src/app` | App directory, relative to project root |
70
70
  | `pagesDir` | discovered `pages/`… | Pages directory |
71
- | `outFile` | `paramour-env.d.ts` | Artifact path (monorepo escape hatch); also settable on `withTypedRoutes` |
71
+ | `outFile` | `paramour-env.d.ts` | Artifact path (monorepo escape hatch); honored by the CLI and `withTypedRoutes` alike |
72
72
  | `pageExtensions` | `["tsx","ts","jsx","js"]` | No leading dots |
73
73
  | `routeFiles` | automatic content scan | Globs of modules exporting route definitions — used by `list`/`doctor` only; generation never reads it |
74
74
 
@@ -89,7 +89,7 @@ Precedence: CLI flags → config file → discovery. Unknown keys are rejected (
89
89
  Facts agents trip on:
90
90
 
91
91
  - Booleans serialize as exactly `true`/`false`; anything else fails to parse.
92
- - Dates: `p.isoDate` is `YYYY-MM-DD`; `p.timestamp` is full ISO UTC (`Z` only, offsets rejected); both reject impossible calendar dates.
92
+ - Dates: `p.isoDate` is `YYYY-MM-DD`; `p.timestamp` is a full ISO instant (`Z` or `±HH:MM` on input, always UTC on output); both reject impossible calendar dates.
93
93
  - Integers reject `1e3`, hex, whitespace, and unsafe-range values.
94
94
  - Arrays: `p.array` repeats the key (`?t=a&t=b`); `p.csv` packs one key (`?t=a,b`). Same in-memory `string[]`, two deliberate wire spellings — do not swap them casually.
95
95
  - Value-form `.default()` elides: building a URL with the default value emits nothing for that key; decoding the bare URL restores the default. Factory defaults never elide.
@@ -61,7 +61,7 @@ const nextConfig: NextConfig = {};
61
61
  export default withTypedRoutes(nextConfig);
62
62
  ```
63
63
 
64
- `withTypedRoutes(config, options?)` regenerates the artifact once per production build (drift warns; `{ strict: true }` fails the build on drift instead) and runs a debounced regeneration watcher during `next dev`. `{ outFile: "..." }` relocates the artifact (monorepo escape hatch). Generation is never load-bearing: a missing route dir or an incidental failure warns and continues with stale types — the two exceptions that throw are an app↔pages route collision and a populated-but-ignored route dir.
64
+ `withTypedRoutes(config, options?)` regenerates the artifact once per production build (drift warns; `{ strict: true }` fails the build on drift instead) and runs a debounced regeneration watcher during `next dev`. It reads `paramour.config` like the CLI does (`outFile` there relocates the artifact for both; Next's own `pageExtensions` wins inside Next, with a warning if the config file disagrees). Generation is never load-bearing: a missing route dir or an incidental failure warns and continues with stale types — the two exceptions that throw are an app↔pages route collision and a populated-but-ignored route dir.
65
65
 
66
66
  Add the script to `package.json`:
67
67