@paramour-js/next 0.8.0 → 0.10.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 +8 -8
- package/dist/app.js +4 -11
- package/dist/cli-inputs.d.ts +7 -3
- package/dist/cli-inputs.js +11 -4
- package/dist/collisions.d.ts +4 -2
- package/dist/collisions.js +16 -1
- package/dist/config.d.ts +3 -3
- package/dist/devtools-emit.d.ts +40 -0
- package/dist/devtools-emit.js +85 -0
- package/dist/devtools-seam.d.ts +35 -58
- package/dist/devtools-seam.js +1 -78
- package/dist/doctor/checks.js +9 -16
- package/dist/observe.js +1 -1
- package/dist/pages.d.ts +6 -6
- package/dist/pages.js +1 -1
- package/dist/select.d.ts +4 -4
- package/dist/with-typed-routes.d.ts +5 -7
- package/dist/with-typed-routes.js +27 -9
- package/package.json +5 -4
- package/skills/paramour/SKILL.md +1 -1
- package/skills/paramour/references/authoring.md +10 -10
- package/skills/paramour/references/migration.md +1 -1
- package/skills/paramour/references/reference.md +14 -14
- package/skills/paramour/references/setup.md +1 -1
package/dist/app.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnyAppRoute, type InferRouteParams, type
|
|
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<
|
|
91
|
-
export declare function useSearch<R extends AnyAppRoute, U>(route: R, options: SelectOptions<
|
|
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):
|
|
98
|
-
export declare function useSearchOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<
|
|
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-
|
|
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
|
-
//
|
|
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
|
|
134
|
+
return decodeSearch(route, searchParams);
|
|
142
135
|
}
|
|
143
136
|
try {
|
|
144
|
-
const data = decodeSearch(route
|
|
137
|
+
const data = decodeSearch(route, searchParams);
|
|
145
138
|
if (spec !== undefined) {
|
|
146
139
|
emitter.observe(spec, { data, status: "success" });
|
|
147
140
|
}
|
package/dist/cli-inputs.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/cli-inputs.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/dist/collisions.d.ts
CHANGED
|
@@ -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
|
|
15
|
-
name:
|
|
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
|
package/dist/collisions.js
CHANGED
|
@@ -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
|
|
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
|
|
3
|
-
* field is optional; the CLI's precedence is flags →
|
|
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
|
+
}
|
package/dist/devtools-seam.d.ts
CHANGED
|
@@ -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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
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
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
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
|
|
147
|
-
|
|
120
|
+
export interface ParamourSearchObservation extends ParamourObservationBase {
|
|
121
|
+
readonly kind: "search";
|
|
122
|
+
readonly wire: ParamourSearchWire;
|
|
123
|
+
}
|
|
124
|
+
export type ParamourSearchWire = readonly (readonly [string, string])[];
|
package/dist/devtools-seam.js
CHANGED
|
@@ -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 {};
|
package/dist/doctor/checks.js
CHANGED
|
@@ -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
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
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
|
-
"
|
|
237
|
+
"paramour packages release together — upgrade them to the same version (e.g. `pnpm up paramour @paramour-js/next`)",
|
|
245
238
|
],
|
|
246
|
-
label: `versions:
|
|
239
|
+
label: `versions: paramour ${core} != @paramour-js/next ${next}`,
|
|
247
240
|
status: "warn",
|
|
248
241
|
};
|
|
249
242
|
}
|
|
250
243
|
return {
|
|
251
|
-
label: `versions: paramour
|
|
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-
|
|
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
|
|
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<
|
|
57
|
-
export declare function useSearch<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<
|
|
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-
|
|
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
|
-
/**
|
|
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 {
|
|
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 {
|
|
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
|
|
63
|
-
// configurable than this.
|
|
63
|
+
// project root as cwd — the same root the CLI resolves against.
|
|
64
64
|
const projectRoot = process.cwd();
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
3
|
+
"version": "0.10.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.10.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>",
|
package/skills/paramour/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ Paramour is a type-safe routing companion for Next.js: each route is defined onc
|
|
|
12
12
|
1. Import only from package barrels: `paramour`, `@paramour-js/next`, `@paramour-js/next/app`, `@paramour-js/next/pages`, `@paramour-js/next/testing`. Never import `dist/` or deep source paths.
|
|
13
13
|
2. Codec modifier legality is type-state: illegal chains do not compile (the method's type becomes `never`) and throw at runtime for JS callers. The rules:
|
|
14
14
|
- `.optional()` and `.default()` apply only to a bare, unmodified single-value codec — at most ONE of the two, at most once. `.optional().default()`, `.default().optional()`, and any repeat are illegal.
|
|
15
|
-
- `.catch()` applies at most once and combines with either presence modifier in either order. It recovers parse failures of PRESENT wire values only — never absence.
|
|
15
|
+
- `.catch()` applies at most once and combines with either presence modifier in either order. It recovers parse failures of PRESENT wire values only — never absence. On an `.optional()` codec the fallback may be `undefined`, so a bad value reads as absent (`.optional().catch(undefined)`).
|
|
16
16
|
- `p.array(...)` codecs take no `.optional()`/`.default()` (an absent key and `[]` are the same wire state). `.catch()` is allowed.
|
|
17
17
|
- Codecs in a `params:` config take no presence modifiers at all (`.optional()`/`.default()` are illegal there); `.catch()` is allowed.
|
|
18
18
|
- `p.csv(element)`/`p.array(element)` elements must be bare unmodified scalars: no modifiers, no csv inside csv, no array-arity element.
|
|
@@ -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
|
|
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 |
|
|
@@ -27,18 +27,18 @@ Wire grammars are strict and anchored — no `Number()` coercion, no whitespace,
|
|
|
27
27
|
|
|
28
28
|
## Modifier chains
|
|
29
29
|
|
|
30
|
-
| Modifier | Effect on decode
|
|
31
|
-
| ------------------------------- |
|
|
32
|
-
| (none — required) | Absent key is a decode issue
|
|
33
|
-
| `.optional()` | Absent → `undefined`; field type `T \| undefined`
|
|
34
|
-
| `.default(value)` | Absent → default; field type stays `T`
|
|
35
|
-
| `.default(() => value)` | Absent → factory result; field type stays `T`
|
|
36
|
-
| `.catch(v)` / `.catch(() => v)` | A PRESENT value that fails parsing → fallback. Never covers absence. | No change | No change |
|
|
30
|
+
| Modifier | Effect on decode | Effect on href input | Effect on URL |
|
|
31
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| (none — required) | Absent key is a decode issue | Key required | Always emitted when building |
|
|
33
|
+
| `.optional()` | Absent → `undefined`; field type `T \| undefined` | Key omittable | Omitted key emits nothing |
|
|
34
|
+
| `.default(value)` | Absent → default; field type stays `T` | Key omittable | Value equal to the default ELIDES (compared by serialized wire form) — one canonical URL per state |
|
|
35
|
+
| `.default(() => value)` | Absent → factory result; field type stays `T` | Key omittable | NEVER elides (a time-varying factory would swallow explicit values) |
|
|
36
|
+
| `.catch(v)` / `.catch(() => v)` | A PRESENT value that fails parsing → fallback. Never covers absence. After `.optional()`, the fallback may be `undefined` (bad value → absent). | No change | No change |
|
|
37
37
|
|
|
38
38
|
Legality (compile-time type-state — illegal calls type as `never`; runtime throws for JS):
|
|
39
39
|
|
|
40
|
-
- Legal: `p.integer()`, `.optional()`, `.default(1)`, `.catch(0)`, `.optional().catch(0)`, `.catch(0).optional()`, `.default(1).catch(0)`, `.catch(0).default(1)`, `p.csv().default([])`, `p.array().catch([])`.
|
|
41
|
-
- Illegal: `.optional().default(...)`, `.default(...).optional()`, `.optional().optional()`, `.default(...).default(...)`, `.catch(...).catch(...)`, `p.array().optional()`, `p.array().default(...)`, any modifier on a csv/array ELEMENT (`p.csv(p.string().optional())`), `p.csv(p.csv())`, `p.array(p.array())`, and `.default(value)` where the value's type includes a function member (use the factory form).
|
|
40
|
+
- Legal: `p.integer()`, `.optional()`, `.default(1)`, `.catch(0)`, `.optional().catch(0)`, `.optional().catch(undefined)`, `.catch(0).optional()`, `.default(1).catch(0)`, `.catch(0).default(1)`, `p.csv().default([])`, `p.array().catch([])`.
|
|
41
|
+
- Illegal: `.optional().default(...)`, `.default(...).optional()`, `.optional().optional()`, `.default(...).default(...)`, `.catch(...).catch(...)`, `.catch(undefined)` on a required/defaulted codec (apply `.optional()` first), `p.array().optional()`, `p.array().default(...)`, any modifier on a csv/array ELEMENT (`p.csv(p.string().optional())`), `p.csv(p.csv())`, `p.array(p.array())`, and `.default(value)` where the value's type includes a function member (use the factory form).
|
|
42
42
|
- `params:` codecs additionally forbid `.optional()`/`.default()` (path optionality comes from `[[...slug]]`); `.catch()` is fine.
|
|
43
43
|
|
|
44
44
|
Value vs factory `.default()`: value defaults are serialized eagerly at definition time (an invalid default fails immediately) and participate in URL elision; factory defaults are invoked per decode (fresh reference per call — use for mutable objects) and never elide. Array value defaults are handed out as fresh shallow copies per decode.
|
|
@@ -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(
|
|
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(
|
|
27
|
+
| `encodeSearch(routeOrConfig, input)` | Decoded values → ordered wire pairs `[string, string][]` (default elision applied) |
|
|
28
28
|
| `buildSearchString(pairs)` | Pairs → `?…` string (`%20`, never `+`) |
|
|
29
|
-
| `searchToString(
|
|
30
|
-
| `serializeValue(codec, label, value)`
|
|
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`, `
|
|
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: {
|
|
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);
|
|
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
|
|
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`. `
|
|
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
|
|