@paramour-js/next 0.4.0 → 0.4.1
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 +48 -49
- package/dist/app.js +11 -12
- package/dist/cli-args.d.ts +4 -4
- package/dist/cli-args.js +4 -4
- package/dist/cli-inputs.d.ts +6 -7
- package/dist/cli-inputs.js +6 -7
- package/dist/cli.js +1 -1
- package/dist/collisions.d.ts +8 -8
- package/dist/collisions.js +9 -9
- package/dist/commands/generate.d.ts +7 -7
- package/dist/commands/generate.js +16 -16
- package/dist/commands/init.js +1 -1
- package/dist/config.d.ts +10 -10
- package/dist/config.js +4 -4
- package/dist/devtools-seam.d.ts +19 -19
- package/dist/devtools-seam.js +3 -3
- package/dist/doctor/checks.js +1 -1
- package/dist/emit.d.ts +12 -12
- package/dist/emit.js +13 -13
- package/dist/generate.d.ts +14 -14
- package/dist/generate.js +11 -11
- package/dist/list/discover-route-defs.d.ts +4 -4
- package/dist/list/discover-route-defs.js +4 -4
- package/dist/lock.d.ts +8 -9
- package/dist/lock.js +11 -12
- package/dist/navigation-adapter.d.ts +17 -18
- package/dist/navigation-adapter.js +4 -4
- package/dist/observe.d.ts +14 -14
- package/dist/observe.js +4 -4
- package/dist/pages.d.ts +30 -31
- package/dist/pages.js +10 -10
- package/dist/run-cli.d.ts +1 -1
- package/dist/run-cli.js +1 -1
- package/dist/scan-app.d.ts +10 -10
- package/dist/scan-app.js +30 -30
- package/dist/scan-pages.d.ts +6 -6
- package/dist/scan-pages.js +28 -26
- package/dist/scan.d.ts +13 -10
- package/dist/scan.js +7 -7
- package/dist/select.d.ts +40 -40
- package/dist/select.js +30 -30
- package/dist/testing.d.ts +28 -32
- package/dist/testing.js +15 -15
- package/dist/watch.d.ts +12 -12
- package/dist/watch.js +13 -13
- package/dist/with-typed-routes.d.ts +11 -10
- package/dist/with-typed-routes.js +42 -39
- package/package.json +2 -2
package/dist/devtools-seam.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
2
2
|
/**
|
|
3
|
-
* The devtools observation seam
|
|
3
|
+
* The devtools observation seam: a dependency-free global
|
|
4
4
|
* slot the hooks push decode observations into and the devtools panel
|
|
5
5
|
* (`@paramour-js/devtools`) reads out of. This module's JSDoc is the
|
|
6
6
|
* CONTRACT OF RECORD for the slot — the panel never imports runtime code
|
|
@@ -10,7 +10,7 @@ import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
|
10
10
|
*
|
|
11
11
|
* - Slot key: `Symbol.for("paramour.devtools.seam")` — the realm-global
|
|
12
12
|
* symbol registry, the same cross-copy identity idiom as core's error
|
|
13
|
-
* brands
|
|
13
|
+
* brands: a second physical copy of this module (dual-package
|
|
14
14
|
* hazard, bundler duplication) mints the SAME symbol and lands on the
|
|
15
15
|
* same slot. Either side — hooks or panel — may create the slot; both
|
|
16
16
|
* sides are create-if-absent.
|
|
@@ -24,14 +24,14 @@ import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
|
24
24
|
* `add` — same JS thread, so nothing can be emitted between the read and
|
|
25
25
|
* the add. Emitters push to `buffer` (FIFO-capped at
|
|
26
26
|
* {@link OBSERVATION_BUFFER_CAP}) and then invoke every listener.
|
|
27
|
-
* - Production
|
|
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
30
|
* import of this module is dropped entirely. The emitted JS here imports
|
|
31
31
|
* NOTHING (the one `paramour` import is type-only) — load-bearing for
|
|
32
32
|
* that erasure.
|
|
33
33
|
*/
|
|
34
|
-
/** The `Symbol.for("paramour.devtools.seam")` slot shape — the
|
|
34
|
+
/** The `Symbol.for("paramour.devtools.seam")` slot shape — the seam contract. */
|
|
35
35
|
export interface ParamourDevtoolsSeam {
|
|
36
36
|
/** Capped FIFO; oldest dropped past the cap. Replay = read it. */
|
|
37
37
|
readonly buffer: ParamourObservation[];
|
|
@@ -43,12 +43,12 @@ export interface ParamourDevtoolsSeam {
|
|
|
43
43
|
*/
|
|
44
44
|
readonly version: 1;
|
|
45
45
|
}
|
|
46
|
-
/** Discriminant naming which hook reported
|
|
46
|
+
/** Discriminant naming which hook reported. */
|
|
47
47
|
export type ParamourHookId = "app.useRouteParams" | "app.useRouteParamsOrThrow" | "app.useSearch" | "app.useSearchOrThrow" | "pages.useRouteParams" | "pages.useSearch";
|
|
48
48
|
/**
|
|
49
|
-
* Navigation capability captured from the EMITTING hook's router
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Navigation capability captured from the EMITTING hook's router: the panel
|
|
50
|
+
* commits URL edits through this, so it never guesses which router is live
|
|
51
|
+
* and never imports Next. The panel passes ONLY the
|
|
52
52
|
* serialized search string (`""` or `"?…"`); the hook resolves it against
|
|
53
53
|
* its OWN current pathname — `usePathname()` (App) / `asPath`'s path part
|
|
54
54
|
* (Pages), both basePath-/locale-relative, which is what `replace()`
|
|
@@ -60,18 +60,18 @@ export type ParamourHookId = "app.useRouteParams" | "app.useRouteParamsOrThrow"
|
|
|
60
60
|
*/
|
|
61
61
|
export type ParamourNavigate = (search: string) => void;
|
|
62
62
|
/**
|
|
63
|
-
* One hook decode, reported on decode CHANGE
|
|
63
|
+
* One hook decode, reported on decode CHANGE — and
|
|
64
64
|
* re-reported when the hook's resolution base moves under an unchanged
|
|
65
65
|
* decode (a layout surviving `/product/1?q=a` → `/product/2?q=a`), so the
|
|
66
66
|
* captured `navigate`/`pathname` never go stale while the hook is mounted.
|
|
67
67
|
*/
|
|
68
68
|
export type ParamourObservation = ParamourParamsObservation | ParamourSearchObservation;
|
|
69
69
|
/**
|
|
70
|
-
* Pre-`select` decode result
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
75
|
*/
|
|
76
76
|
export type ParamourObservationResult = SafeResult<unknown> | {
|
|
77
77
|
readonly status: "pending";
|
|
@@ -84,7 +84,7 @@ export interface ParamourParamsObservation extends ParamourObservationBase {
|
|
|
84
84
|
/**
|
|
85
85
|
* Search decode: wire is decode-time `[key, value]` pairs in wire order —
|
|
86
86
|
* order is load-bearing for repeated keys (P5/S5), and pairs round-trip
|
|
87
|
-
* losslessly into the panel's raw-wire editing
|
|
87
|
+
* losslessly into the panel's raw-wire editing.
|
|
88
88
|
*/
|
|
89
89
|
export interface ParamourSearchObservation extends ParamourObservationBase {
|
|
90
90
|
readonly kind: "search";
|
|
@@ -106,7 +106,7 @@ interface ParamourObservationBase {
|
|
|
106
106
|
readonly pathname: string;
|
|
107
107
|
readonly result: ParamourObservationResult;
|
|
108
108
|
/**
|
|
109
|
-
* The LIVE route object (
|
|
109
|
+
* The LIVE route object (same JS context, no serialization) — the
|
|
110
110
|
* panel calls `describeRoute`, the route's own codecs, and
|
|
111
111
|
* `buildSearchString` on it directly.
|
|
112
112
|
*/
|
|
@@ -115,7 +115,7 @@ interface ParamourObservationBase {
|
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* 128: replay only needs the pre-panel-mount window. One observation per
|
|
118
|
-
* decode CHANGE per hook
|
|
118
|
+
* decode CHANGE per hook means even a long pre-open session is dozens
|
|
119
119
|
* of entries, not thousands; the panel keys on route, so depth beyond
|
|
120
120
|
* "every route seen recently" adds nothing — the cap mostly bounds how many
|
|
121
121
|
* live route/result references the buffer retains.
|
|
@@ -123,8 +123,8 @@ interface ParamourObservationBase {
|
|
|
123
123
|
export declare const OBSERVATION_BUFFER_CAP = 128;
|
|
124
124
|
/**
|
|
125
125
|
* Pushes one observation and notifies listeners. The internal production
|
|
126
|
-
* early-return is belt-and-suspenders
|
|
127
|
-
*
|
|
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
128
|
* unit-testable and keeps a future unguarded call site failing safe.
|
|
129
129
|
*/
|
|
130
130
|
export declare function emitObservation(observation: ParamourObservation): void;
|
package/dist/devtools-seam.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 128: replay only needs the pre-panel-mount window. One observation per
|
|
3
|
-
* decode CHANGE per hook
|
|
3
|
+
* decode CHANGE per hook means even a long pre-open session is dozens
|
|
4
4
|
* of entries, not thousands; the panel keys on route, so depth beyond
|
|
5
5
|
* "every route seen recently" adds nothing — the cap mostly bounds how many
|
|
6
6
|
* live route/result references the buffer retains.
|
|
@@ -10,8 +10,8 @@ const SEAM_KEY = Symbol.for("paramour.devtools.seam");
|
|
|
10
10
|
const globalSlots = globalThis;
|
|
11
11
|
/**
|
|
12
12
|
* Pushes one observation and notifies listeners. The internal production
|
|
13
|
-
* early-return is belt-and-suspenders
|
|
14
|
-
*
|
|
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
15
|
* unit-testable and keeps a future unguarded call site failing safe.
|
|
16
16
|
*/
|
|
17
17
|
export function emitObservation(observation) {
|
package/dist/doctor/checks.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function runDoctorChecks(projectRoot) {
|
|
|
34
34
|
status: "fail",
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
// 2. Route directories resolve (
|
|
37
|
+
// 2. Route directories resolve (joint discovery, config dirs honored).
|
|
38
38
|
let inputs;
|
|
39
39
|
try {
|
|
40
40
|
inputs = await resolveInputs({}, projectRoot, config);
|
package/dist/emit.d.ts
CHANGED
|
@@ -2,34 +2,34 @@
|
|
|
2
2
|
export interface WriteIfChangedResult {
|
|
3
3
|
/**
|
|
4
4
|
* Prior file content, or `null` when the file did not exist — the input
|
|
5
|
-
* for
|
|
5
|
+
* for the drift warning (which paths appeared/disappeared).
|
|
6
6
|
*/
|
|
7
7
|
previousContent: null | string;
|
|
8
8
|
/**
|
|
9
|
-
* `false` on a byte-identical no-op — the signal `--check`
|
|
10
|
-
*
|
|
9
|
+
* `false` on a byte-identical no-op — the signal `--check` and the watch
|
|
10
|
+
* loop hang off.
|
|
11
11
|
*/
|
|
12
12
|
written: boolean;
|
|
13
13
|
}
|
|
14
|
-
/** Input of {@link emitArtifact} — one union per router
|
|
14
|
+
/** Input of {@link emitArtifact} — one union per router. */
|
|
15
15
|
export interface EmitRoutes {
|
|
16
16
|
appRoutes: readonly string[];
|
|
17
17
|
pagesRoutes: readonly string[];
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Artifact text for the per-router route unions
|
|
21
|
-
*
|
|
20
|
+
* Artifact text for the per-router route unions: deterministic — sorted,
|
|
21
|
+
* deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
|
|
22
22
|
* happens here as well as in the scanner so byte-identity never depends on
|
|
23
23
|
* who the caller was. Always the leading-pipe multiline union form, even for
|
|
24
24
|
* one path — one shape, no count-dependent formatting. An empty union omits
|
|
25
|
-
* its member entirely (
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* its member entirely (absent, not `never`, applied per router): a
|
|
26
|
+
* pages-only project keeps world-A's permissive `string` fallback for
|
|
27
|
+
* `defineAppRoute`, and vice versa.
|
|
28
28
|
*/
|
|
29
29
|
export declare function emitArtifact(routes: EmitRoutes): string;
|
|
30
30
|
/**
|
|
31
|
-
* Compare-before-write
|
|
32
|
-
*
|
|
33
|
-
* concurrent-generator races
|
|
31
|
+
* Compare-before-write: a no-op regeneration must not touch the file — that
|
|
32
|
+
* property is what prevents TS-server churn, watch-loop feedback, and
|
|
33
|
+
* concurrent-generator races.
|
|
34
34
|
*/
|
|
35
35
|
export declare function writeIfChanged(filePath: string, content: string): WriteIfChangedResult;
|
package/dist/emit.js
CHANGED
|
@@ -2,14 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
const HEADER = "// Generated by @paramour-js/next. Do not edit — regenerate with `paramour generate`.";
|
|
4
4
|
/**
|
|
5
|
-
* Artifact text for the per-router route unions
|
|
6
|
-
*
|
|
5
|
+
* Artifact text for the per-router route unions: deterministic — sorted,
|
|
6
|
+
* deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
|
|
7
7
|
* happens here as well as in the scanner so byte-identity never depends on
|
|
8
8
|
* who the caller was. Always the leading-pipe multiline union form, even for
|
|
9
9
|
* one path — one shape, no count-dependent formatting. An empty union omits
|
|
10
|
-
* its member entirely (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* its member entirely (absent, not `never`, applied per router): a
|
|
11
|
+
* pages-only project keeps world-A's permissive `string` fallback for
|
|
12
|
+
* `defineAppRoute`, and vice versa.
|
|
13
13
|
*/
|
|
14
14
|
export function emitArtifact(routes) {
|
|
15
15
|
const members = ["appRoutes", "pagesRoutes"].flatMap((member) => {
|
|
@@ -21,22 +21,22 @@ export function emitArtifact(routes) {
|
|
|
21
21
|
// contain a quote or backslash (Linux/macOS), which unescaped would emit
|
|
22
22
|
// invalid TS or silently declare the wrong route. JSON string escaping is
|
|
23
23
|
// a subset of TS string-literal escaping (output stays valid TS) and is
|
|
24
|
-
// deterministic, so
|
|
24
|
+
// deterministic, so the byte-identity guarantee holds.
|
|
25
25
|
return [
|
|
26
26
|
` ${member}:`,
|
|
27
27
|
`${sorted.map((path) => ` | ${JSON.stringify(path)}`).join("\n")};`,
|
|
28
28
|
];
|
|
29
29
|
});
|
|
30
30
|
if (members.length === 0) {
|
|
31
|
-
//
|
|
31
|
+
// No routes yet → NO members (explicitly not `never`, which would
|
|
32
32
|
// make every route-constructor call an error). The empty merge keeps
|
|
33
|
-
// the documented world-A `string` fallback
|
|
33
|
+
// the documented world-A `string` fallback.
|
|
34
34
|
return [
|
|
35
35
|
HEADER,
|
|
36
36
|
'import "paramour";',
|
|
37
37
|
"",
|
|
38
38
|
'declare module "paramour" {',
|
|
39
|
-
" //
|
|
39
|
+
" // Empty merge — no routes discovered yet; the pre-generation",
|
|
40
40
|
" // `string` fallback stays in effect.",
|
|
41
41
|
" // eslint-disable-next-line @typescript-eslint/no-empty-object-type",
|
|
42
42
|
" interface ParamourRegister {}",
|
|
@@ -57,9 +57,9 @@ export function emitArtifact(routes) {
|
|
|
57
57
|
].join("\n");
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
|
-
* Compare-before-write
|
|
61
|
-
*
|
|
62
|
-
* concurrent-generator races
|
|
60
|
+
* Compare-before-write: a no-op regeneration must not touch the file — that
|
|
61
|
+
* property is what prevents TS-server churn, watch-loop feedback, and
|
|
62
|
+
* concurrent-generator races.
|
|
63
63
|
*/
|
|
64
64
|
export function writeIfChanged(filePath, content) {
|
|
65
65
|
const previousContent = existsSync(filePath)
|
|
@@ -67,7 +67,7 @@ export function writeIfChanged(filePath, content) {
|
|
|
67
67
|
: null;
|
|
68
68
|
if (previousContent === content)
|
|
69
69
|
return { previousContent, written: false };
|
|
70
|
-
//
|
|
70
|
+
// The outFile escape hatch may point into a not-yet-existing directory.
|
|
71
71
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
72
72
|
writeFileSync(filePath, content);
|
|
73
73
|
return { previousContent, written: true };
|
package/dist/generate.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared generation engine
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The shared generation engine: `withTypedRoutes` and the CLI drive these
|
|
3
|
+
* same functions, so wrapper and `paramour generate` cannot drift on what a
|
|
4
|
+
* pass produces. Everything here is @internal — not barrel API.
|
|
5
5
|
*/
|
|
6
6
|
/** Result of {@link checkArtifact}. */
|
|
7
7
|
export interface CheckResult {
|
|
8
|
-
/** App-router drift — split per router so the report names it
|
|
8
|
+
/** App-router drift — split per router so the report names it. */
|
|
9
9
|
app: RouterDrift;
|
|
10
10
|
/** `true` when the artifact file does not exist at all. */
|
|
11
11
|
missingFile: boolean;
|
|
@@ -14,7 +14,7 @@ export interface CheckResult {
|
|
|
14
14
|
/** `true` on a byte-identical artifact — the only non-drift state. */
|
|
15
15
|
upToDate: boolean;
|
|
16
16
|
}
|
|
17
|
-
/** Inputs to one generation/check pass; either dir may be absent
|
|
17
|
+
/** Inputs to one generation/check pass; either dir may be absent. */
|
|
18
18
|
export interface GenerateInputs {
|
|
19
19
|
appDir?: string | undefined;
|
|
20
20
|
artifactPath: string;
|
|
@@ -29,10 +29,10 @@ export interface GenerateResult {
|
|
|
29
29
|
pagesRoutes: string[];
|
|
30
30
|
/** Prior artifact content, `null` when the file did not exist. */
|
|
31
31
|
previousContent: null | string;
|
|
32
|
-
/** `false` on a byte-identical no-op (
|
|
32
|
+
/** `false` on a byte-identical no-op (write-if-changed). */
|
|
33
33
|
written: boolean;
|
|
34
34
|
}
|
|
35
|
-
/** One router's appeared/disappeared route paths
|
|
35
|
+
/** One router's appeared/disappeared route paths. */
|
|
36
36
|
export interface RouterDrift {
|
|
37
37
|
/** Routes on disk (scan) that the artifact lacks. */
|
|
38
38
|
appeared: string[];
|
|
@@ -40,25 +40,25 @@ export interface RouterDrift {
|
|
|
40
40
|
disappeared: string[];
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* `--check
|
|
44
|
-
*
|
|
45
|
-
* CI-degrades-to-world-A case the committed file exists to prevent
|
|
43
|
+
* `--check`: scan to memory and byte-compare against disk — never writes. A
|
|
44
|
+
* missing artifact is drift, not an error: that is exactly the
|
|
45
|
+
* CI-degrades-to-world-A case the committed file exists to prevent.
|
|
46
46
|
*/
|
|
47
47
|
export declare function checkArtifact(inputs: GenerateInputs): CheckResult;
|
|
48
48
|
/**
|
|
49
49
|
* Per-router drift of a completed {@link generate} pass against the artifact
|
|
50
|
-
* it replaced — the wrapper's build-phase drift report
|
|
50
|
+
* it replaced — the wrapper's build-phase drift report.
|
|
51
51
|
*/
|
|
52
52
|
export declare function diffGenerated(result: GenerateResult): {
|
|
53
53
|
app: RouterDrift;
|
|
54
54
|
pages: RouterDrift;
|
|
55
55
|
};
|
|
56
56
|
/**
|
|
57
|
-
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report
|
|
58
|
-
*
|
|
57
|
+
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report — each
|
|
58
|
+
* line names the router its path moved in.
|
|
59
59
|
*/
|
|
60
60
|
export declare function formatRouteDiff(app: RouterDrift, pages: RouterDrift): string[];
|
|
61
|
-
/** One generation pass: scan → emit → write-if-changed
|
|
61
|
+
/** One generation pass: scan → emit → write-if-changed. */
|
|
62
62
|
export declare function generate(inputs: GenerateInputs): GenerateResult;
|
|
63
63
|
/**
|
|
64
64
|
* Per-router route paths in a previously emitted artifact; both empty for a
|
package/dist/generate.js
CHANGED
|
@@ -3,17 +3,17 @@ import { emitArtifact, writeIfChanged } from "./emit.js";
|
|
|
3
3
|
import { scanRoutes } from "./scan.js";
|
|
4
4
|
/**
|
|
5
5
|
* Reads the per-router unions back out of a previously emitted artifact for
|
|
6
|
-
* drift diffs
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* drift diffs. Only ever applied to text this package generated (its
|
|
7
|
+
* deterministic form), so line-anchored matches on the member headers and
|
|
8
|
+
* union members are exact, not heuristic. `\s*$` on both tolerates a
|
|
9
9
|
* CRLF-resaved artifact.
|
|
10
10
|
*/
|
|
11
11
|
const MEMBER_HEADER = /^\s*(appRoutes|pagesRoutes):\s*$/;
|
|
12
12
|
const UNION_MEMBER = /^\s*\| "(.*)";?\s*$/;
|
|
13
13
|
/**
|
|
14
|
-
* `--check
|
|
15
|
-
*
|
|
16
|
-
* CI-degrades-to-world-A case the committed file exists to prevent
|
|
14
|
+
* `--check`: scan to memory and byte-compare against disk — never writes. A
|
|
15
|
+
* missing artifact is drift, not an error: that is exactly the
|
|
16
|
+
* CI-degrades-to-world-A case the committed file exists to prevent.
|
|
17
17
|
*/
|
|
18
18
|
export function checkArtifact(inputs) {
|
|
19
19
|
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
@@ -39,7 +39,7 @@ export function checkArtifact(inputs) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Per-router drift of a completed {@link generate} pass against the artifact
|
|
42
|
-
* it replaced — the wrapper's build-phase drift report
|
|
42
|
+
* it replaced — the wrapper's build-phase drift report.
|
|
43
43
|
*/
|
|
44
44
|
export function diffGenerated(result) {
|
|
45
45
|
const previous = parseArtifactRoutes(result.previousContent);
|
|
@@ -49,8 +49,8 @@ export function diffGenerated(result) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report
|
|
53
|
-
*
|
|
52
|
+
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report — each
|
|
53
|
+
* line names the router its path moved in.
|
|
54
54
|
*/
|
|
55
55
|
export function formatRouteDiff(app, pages) {
|
|
56
56
|
return [
|
|
@@ -60,7 +60,7 @@ export function formatRouteDiff(app, pages) {
|
|
|
60
60
|
...pages.disappeared.map((path) => ` - ${path} (pages)`),
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
|
-
/** One generation pass: scan → emit → write-if-changed
|
|
63
|
+
/** One generation pass: scan → emit → write-if-changed. */
|
|
64
64
|
export function generate(inputs) {
|
|
65
65
|
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
66
66
|
return {
|
|
@@ -92,7 +92,7 @@ export function parseArtifactRoutes(previousContent) {
|
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
// Any other line ends the member block — union members are contiguous
|
|
95
|
-
// in the
|
|
95
|
+
// in the deterministic emitted form.
|
|
96
96
|
current = undefined;
|
|
97
97
|
}
|
|
98
98
|
return { appRoutes, pagesRoutes };
|
|
@@ -37,10 +37,10 @@ export interface RouteDefinition {
|
|
|
37
37
|
* its text mentions a define constructor (grep-then-load). `routeFiles`
|
|
38
38
|
* config globs replace the default patterns when the heuristic misfires.
|
|
39
39
|
*
|
|
40
|
-
* jiti evaluates matched files (the
|
|
41
|
-
* both handled by the per-module degrade: tsconfig `paths`
|
|
42
|
-
* resolved (a future improvement could feed them into jiti's
|
|
43
|
-
* option), and `server-only`-style imports throw outside Next.
|
|
40
|
+
* jiti evaluates matched files (the loader carry-over from config loading).
|
|
41
|
+
* Known limits, both handled by the per-module degrade: tsconfig `paths`
|
|
42
|
+
* aliases are not resolved (a future improvement could feed them into jiti's
|
|
43
|
+
* `alias` option), and `server-only`-style imports throw outside Next.
|
|
44
44
|
*/
|
|
45
45
|
export declare function discoverRouteDefinitions(projectRoot: string, options?: {
|
|
46
46
|
routeFiles?: readonly string[] | undefined;
|
|
@@ -23,10 +23,10 @@ const MAX_PREFILTER_BYTES = 512 * 1024;
|
|
|
23
23
|
* its text mentions a define constructor (grep-then-load). `routeFiles`
|
|
24
24
|
* config globs replace the default patterns when the heuristic misfires.
|
|
25
25
|
*
|
|
26
|
-
* jiti evaluates matched files (the
|
|
27
|
-
* both handled by the per-module degrade: tsconfig `paths`
|
|
28
|
-
* resolved (a future improvement could feed them into jiti's
|
|
29
|
-
* option), and `server-only`-style imports throw outside Next.
|
|
26
|
+
* jiti evaluates matched files (the loader carry-over from config loading).
|
|
27
|
+
* Known limits, both handled by the per-module degrade: tsconfig `paths`
|
|
28
|
+
* aliases are not resolved (a future improvement could feed them into jiti's
|
|
29
|
+
* `alias` option), and `server-only`-style imports throw outside Next.
|
|
30
30
|
*/
|
|
31
31
|
export async function discoverRouteDefinitions(projectRoot, options = {}) {
|
|
32
32
|
// Dynamic imports, same stance as config.ts: only commands that actually
|
package/dist/lock.d.ts
CHANGED
|
@@ -12,18 +12,17 @@ export interface AcquireLockResult {
|
|
|
12
12
|
release?: () => void;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Cross-process single-writer guard
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* points, not here.
|
|
15
|
+
* Cross-process single-writer guard: a best-effort pidfile lock. On startup:
|
|
16
|
+
* read lock → liveness-probe the owner → decline if alive, (over)write and
|
|
17
|
+
* acquire if dead or absent. Deliberately best-effort, not correct — the
|
|
18
|
+
* deterministic write-if-changed output means two live watchers produce
|
|
19
|
+
* identical bytes; imperfect locking costs a log line, not corruption. Hence
|
|
20
|
+
* no flock semantics, atomic-rename dances, or PID-reuse paranoia. The
|
|
21
|
+
* in-process singleton guard lives at the composition points, not here.
|
|
23
22
|
*/
|
|
24
23
|
export declare function acquireWatcherLock(lockPath: string): AcquireLockResult;
|
|
25
24
|
/**
|
|
26
|
-
* The one canonical pidfile location
|
|
25
|
+
* The one canonical pidfile location: CLI-vs-wrapper dedupe only works
|
|
27
26
|
* because both paths compute the lock from the same project root.
|
|
28
27
|
*/
|
|
29
28
|
export declare function watcherLockPath(projectRoot: string): string;
|
package/dist/lock.js
CHANGED
|
@@ -3,14 +3,13 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
/** Strict anchored PID parse — anything else is a stale/corrupt lock. */
|
|
4
4
|
const PID_RE = /^\d+$/;
|
|
5
5
|
/**
|
|
6
|
-
* Cross-process single-writer guard
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* points, not here.
|
|
6
|
+
* Cross-process single-writer guard: a best-effort pidfile lock. On startup:
|
|
7
|
+
* read lock → liveness-probe the owner → decline if alive, (over)write and
|
|
8
|
+
* acquire if dead or absent. Deliberately best-effort, not correct — the
|
|
9
|
+
* deterministic write-if-changed output means two live watchers produce
|
|
10
|
+
* identical bytes; imperfect locking costs a log line, not corruption. Hence
|
|
11
|
+
* no flock semantics, atomic-rename dances, or PID-reuse paranoia. The
|
|
12
|
+
* in-process singleton guard lives at the composition points, not here.
|
|
14
13
|
*/
|
|
15
14
|
export function acquireWatcherLock(lockPath) {
|
|
16
15
|
const ownerPid = readOwnerPid(lockPath);
|
|
@@ -35,8 +34,8 @@ export function acquireWatcherLock(lockPath) {
|
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
catch {
|
|
38
|
-
// Best-effort
|
|
39
|
-
//
|
|
37
|
+
// Best-effort: a leftover lock self-heals via the liveness probe on
|
|
38
|
+
// the next startup.
|
|
40
39
|
}
|
|
41
40
|
};
|
|
42
41
|
const reraise = (signal) => {
|
|
@@ -57,13 +56,13 @@ export function acquireWatcherLock(lockPath) {
|
|
|
57
56
|
return { acquired: true, release };
|
|
58
57
|
}
|
|
59
58
|
/**
|
|
60
|
-
* The one canonical pidfile location
|
|
59
|
+
* The one canonical pidfile location: CLI-vs-wrapper dedupe only works
|
|
61
60
|
* because both paths compute the lock from the same project root.
|
|
62
61
|
*/
|
|
63
62
|
export function watcherLockPath(projectRoot) {
|
|
64
63
|
return join(projectRoot, "node_modules", ".cache", "paramour", "watcher.lock");
|
|
65
64
|
}
|
|
66
|
-
/** `true` when `pid` is a live process
|
|
65
|
+
/** `true` when `pid` is a live process — the liveness probe. */
|
|
67
66
|
function isAlive(pid) {
|
|
68
67
|
try {
|
|
69
68
|
process.kill(pid, 0);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { ParamsSource } from "paramour";
|
|
2
2
|
/**
|
|
3
|
-
* Adapter seam for the client hooks' framework reads
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* shared leaf.
|
|
3
|
+
* Adapter seam for the client hooks' framework reads: each flavor's hooks
|
|
4
|
+
* resolve Next through a React context so the /testing entry can override
|
|
5
|
+
* the reads without runner-specific module mocking. Deliberately NO
|
|
6
|
+
* `"use client"` directive — app.ts (which carries one) and pages.ts (which
|
|
7
|
+
* must not) both import from here, like observe.ts/select.ts; the directive
|
|
8
|
+
* belongs on the entry modules, not a shared leaf.
|
|
10
9
|
*
|
|
11
|
-
* This module is Next-free on purpose
|
|
10
|
+
* This module is Next-free on purpose: the contexts default to `null`
|
|
12
11
|
* and each flavor ENTRY supplies its own real-Next fallback
|
|
13
12
|
* (`useContext(ctx) ?? realAdapter`), so neither this module nor the
|
|
14
13
|
* /testing entry ever drags a `next/*` specifier into its graph. The
|
|
@@ -20,11 +19,11 @@ import type { ParamsSource } from "paramour";
|
|
|
20
19
|
/**
|
|
21
20
|
* App-flavor adapter: EXACTLY the ambient view of `next/navigation` declared
|
|
22
21
|
* in `src/types/next-navigation.d.ts` — that ambient is the contract of
|
|
23
|
-
* record, and this interface must stay in lockstep with it (
|
|
22
|
+
* record, and this interface must stay in lockstep with it (the
|
|
24
23
|
* `examples/next-compat` pins guard the real-Next side). `useParams()`'s
|
|
25
24
|
* `null` arm is the outside-App-Router-tree state (Next #48058 family) the
|
|
26
25
|
* hooks deliberately tolerate; `useRouter().replace`/`usePathname` are the
|
|
27
|
-
* devtools `navigate` capability's write path and resolution base
|
|
26
|
+
* devtools `navigate` capability's write path and resolution base.
|
|
28
27
|
*/
|
|
29
28
|
export interface AppNavigationAdapter {
|
|
30
29
|
useParams(): null | ParamsSource;
|
|
@@ -37,10 +36,10 @@ export interface AppNavigationAdapter {
|
|
|
37
36
|
/**
|
|
38
37
|
* Pages-flavor adapter: EXACTLY the ambient view of `next/router.js`
|
|
39
38
|
* declared in `src/types/next-router.d.ts` — that ambient is the contract of
|
|
40
|
-
* record, and this interface must stay in lockstep with it
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* record, and this interface must stay in lockstep with it. The ambient's
|
|
40
|
+
* throw-on-unmounted behavior under `app/` is part of the contract: adapter
|
|
41
|
+
* implementations reproduce it by THROWING from `useRouter()`, which
|
|
42
|
+
* pages.ts translates.
|
|
44
43
|
*/
|
|
45
44
|
export interface PagesNavigationAdapter {
|
|
46
45
|
useRouter(): {
|
|
@@ -51,10 +50,10 @@ export interface PagesNavigationAdapter {
|
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
/**
|
|
54
|
-
* The `null` default is load-bearing
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* The `null` default is load-bearing: with no provider mounted, app.ts falls
|
|
54
|
+
* back to its real `next/navigation` adapter — zero markup and zero behavior
|
|
55
|
+
* change in production.
|
|
57
56
|
*/
|
|
58
57
|
export declare const AppNavigationContext: import("react").Context<AppNavigationAdapter | null>;
|
|
59
|
-
/** The pages twin; `null` default load-bearing for the same
|
|
58
|
+
/** The pages twin; its `null` default is load-bearing for the same reasons. */
|
|
60
59
|
export declare const PagesNavigationContext: import("react").Context<PagesNavigationAdapter | null>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
/**
|
|
3
|
-
* The `null` default is load-bearing
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The `null` default is load-bearing: with no provider mounted, app.ts falls
|
|
4
|
+
* back to its real `next/navigation` adapter — zero markup and zero behavior
|
|
5
|
+
* change in production.
|
|
6
6
|
*/
|
|
7
7
|
export const AppNavigationContext = createContext(null);
|
|
8
|
-
/** The pages twin; `null` default load-bearing for the same
|
|
8
|
+
/** The pages twin; its `null` default is load-bearing for the same reasons. */
|
|
9
9
|
export const PagesNavigationContext = createContext(null);
|
package/dist/observe.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { AnyRoute, ParamsSource, RouterKind } from "paramour";
|
|
2
2
|
import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, ParamourSearchWire } from "./devtools-seam.js";
|
|
3
3
|
/**
|
|
4
|
-
* Shared devtools seam wiring for the six read hooks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Shared devtools seam wiring for the six read hooks: the navigate builders
|
|
5
|
+
* (one per router flavor) and the per-hook emitter that owns WHEN an
|
|
6
|
+
* observation goes out. Deliberately NO `"use client"` directive — app.ts
|
|
7
|
+
* (which carries one) and pages.ts (which must not) both import from here,
|
|
8
|
+
* like select.ts.
|
|
9
9
|
*
|
|
10
10
|
* Emission policy: the hooks call {@link DevtoolsEmitter.observe} from
|
|
11
|
-
* inside the `useStableResult` compute — the
|
|
12
|
-
*
|
|
11
|
+
* inside the `useStableResult` compute — the fingerprint cache miss IS the
|
|
12
|
+
* decode-change dedup, and only render-phase can report the
|
|
13
13
|
* OrThrow hooks' error observation before the rethrow. `observe` alone
|
|
14
14
|
* would leave one staleness hole: a component that survives a navigation
|
|
15
15
|
* whose decode is unchanged (`/product/1?q=a` → `/product/2?q=a` in a
|
|
@@ -18,10 +18,10 @@ import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, Param
|
|
|
18
18
|
* navigate back to the old resource. {@link DevtoolsEmitter.refresh},
|
|
19
19
|
* called render-phase after the stable result returns, closes it: when the
|
|
20
20
|
* resolution base (or a HMR-reminted route) moved under a cached decode, it
|
|
21
|
-
* re-emits the CACHED result with the fresh spec — decode stability
|
|
22
|
-
*
|
|
21
|
+
* re-emits the CACHED result with the fresh spec — decode stability is
|
|
22
|
+
* untouched, only the seam payload is renewed.
|
|
23
23
|
*
|
|
24
|
-
* Production erasure
|
|
24
|
+
* Production erasure: every call site keeps a literal
|
|
25
25
|
* `process.env.NODE_ENV` guard the bundler constant-folds, and both emitter
|
|
26
26
|
* methods early-return behind the same literal guard, so the
|
|
27
27
|
* `emitObservation` import above is dead in a production bundle and drops
|
|
@@ -51,15 +51,15 @@ export interface ObservationSpec {
|
|
|
51
51
|
readonly wire: () => ParamourSearchWire | Readonly<ParamsSource>;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* App-flavor navigate capability
|
|
55
|
-
*
|
|
54
|
+
* App-flavor navigate capability: `next/navigation`'s `replace` returns void
|
|
55
|
+
* and resolves the basePath-/locale-relative join itself.
|
|
56
56
|
*/
|
|
57
57
|
export declare function makeAppNavigate(router: {
|
|
58
58
|
replace: (href: string) => void;
|
|
59
59
|
}, pathname: string): ParamourNavigate;
|
|
60
60
|
/**
|
|
61
|
-
* Pages-flavor navigate capability
|
|
62
|
-
*
|
|
61
|
+
* Pages-flavor navigate capability: `next/router`'s `replace` returns a
|
|
62
|
+
* promise that REJECTS on routine navigation aborts (rapid re-commits
|
|
63
63
|
* from the panel), marked with next's `cancelled` discriminant — those must
|
|
64
64
|
* not surface as unhandled rejections. Anything else is a real failure
|
|
65
65
|
* (render error, route-info error) silently discarding the user's edit, so
|