@paramour-js/next 0.4.0 → 0.5.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/README.md +1 -1
- 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/doctor.d.ts +12 -0
- package/dist/commands/doctor.js +12 -7
- package/dist/commands/generate.d.ts +55 -7
- package/dist/commands/generate.js +29 -22
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +111 -19
- package/dist/commands/list.d.ts +21 -0
- package/dist/commands/list.js +12 -7
- package/dist/commands/skills.d.ts +45 -0
- package/dist/commands/skills.js +222 -0
- package/dist/config.d.ts +17 -10
- package/dist/config.js +17 -4
- package/dist/devtools-seam.d.ts +19 -19
- package/dist/devtools-seam.js +3 -3
- package/dist/doctor/checks.js +7 -3
- 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/init/agents-md.d.ts +32 -0
- package/dist/init/agents-md.js +78 -0
- package/dist/init/scaffold.js +54 -0
- 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 +3 -1
- package/dist/run-cli.js +7 -3
- 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/skills/doctor.d.ts +11 -0
- package/dist/skills/doctor.js +71 -0
- package/dist/skills/manifest.d.ts +41 -0
- package/dist/skills/manifest.js +96 -0
- package/dist/skills/packaged.d.ts +21 -0
- package/dist/skills/packaged.js +32 -0
- package/dist/skills/sync.d.ts +84 -0
- package/dist/skills/sync.js +136 -0
- package/dist/skills/targets.d.ts +29 -0
- package/dist/skills/targets.js +73 -0
- 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 +4 -3
- package/skills/paramour/SKILL.md +43 -0
- package/skills/paramour/references/authoring.md +113 -0
- package/skills/paramour/references/migration.md +141 -0
- package/skills/paramour/references/reference.md +96 -0
- package/skills/paramour/references/setup.md +139 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The Next.js integration for [paramour](https://paramour.dev):
|
|
4
4
|
`withTypedRoutes` (build-time registry generation and drift checking),
|
|
5
5
|
typed client hooks for both routers, and the `paramour` CLI
|
|
6
|
-
(`generate` / `check` / `init` / `list` / `doctor`).
|
|
6
|
+
(`generate` / `check` / `init` / `list` / `doctor` / `skills`).
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
9
|
pnpm add paramour @paramour-js/next
|
package/dist/app.d.ts
CHANGED
|
@@ -2,67 +2,66 @@ import { type AnyAppRoute, type InferRouteParams, type SafeResult, type SearchOu
|
|
|
2
2
|
import { type SelectOptions } from "./select.js";
|
|
3
3
|
export type { SelectOptions } from "./select.js";
|
|
4
4
|
/**
|
|
5
|
-
* Client hooks
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Client hooks. Each layers over Next's `useSearchParams()` / `useParams()`
|
|
6
|
+
* — App-Router params are synchronous on the client, so there is no loading
|
|
7
|
+
* state, no `useEffect`/`useState`, and the result is SSR-consistent. Two
|
|
8
|
+
* layers per hook:
|
|
9
9
|
*
|
|
10
|
-
* - Raw-slice stabilization
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - Selection
|
|
18
|
-
*
|
|
10
|
+
* - Raw-slice stabilization: the decode is keyed on the DECLARED slice of
|
|
11
|
+
* the raw source, not on Next's object reference — a URL change that only
|
|
12
|
+
* touches keys the route doesn't own (`?utm_source=` churn) returns the
|
|
13
|
+
* previous result by identity, without re-decoding. Next still re-renders
|
|
14
|
+
* every subscriber on any URL change (it owns the subscription; selectors
|
|
15
|
+
* stabilize slices, they cannot skip renders); this layer makes that
|
|
16
|
+
* render cheap and downstream-invisible.
|
|
17
|
+
* - Selection: every hook takes an optional `{ select }` that projects the
|
|
18
|
+
* decoded value, with result-equality checking (`Object.is`,
|
|
19
19
|
* `equality: "shallow"` opt-in) so an unchanged selection keeps its
|
|
20
20
|
* previous reference when OTHER params change.
|
|
21
21
|
*
|
|
22
|
-
* Both layers are render-phase ref caches
|
|
23
|
-
*
|
|
22
|
+
* Both layers are render-phase ref caches — the one sanctioned departure
|
|
23
|
+
* from the pure-`useMemo` discipline these hooks previously held.
|
|
24
24
|
*
|
|
25
25
|
* Two surfaces per half, mirroring core's server `parse` vs `safeParse`:
|
|
26
26
|
* - `useSearch` / `useRouteParams` return the `SafeResult` union
|
|
27
|
-
* (discriminated on `status
|
|
28
|
-
*
|
|
27
|
+
* (discriminated on `status`) — a user editing the URL never crashes the
|
|
28
|
+
* component. The selector runs on the success arm only.
|
|
29
29
|
* - `useSearchOrThrow` / `useRouteParamsOrThrow` throw the decode error in
|
|
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
|
|
33
|
+
* decoders — `@paramour/next` is a sanctioned consumer of those internals.
|
|
34
34
|
*
|
|
35
|
-
* Every hook is gated to `AnyAppRoute
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* Every hook is gated to `AnyAppRoute`: a pages-branded route at one of
|
|
36
|
+
* these call sites is a compile error, not a runtime surprise — these hooks
|
|
37
|
+
* read Next's App-Router navigation hooks, whose pages twin has different
|
|
38
|
+
* state cardinality (`@paramour-js/next/pages`).
|
|
39
39
|
*
|
|
40
|
-
* Devtools instrumentation
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* captures them. The `navigate` capability receives the panel's SEARCH
|
|
40
|
+
* Devtools instrumentation: each hook reports through the shared emitter in
|
|
41
|
+
* observe.ts — `observe` from inside the `useStableResult` compute callback,
|
|
42
|
+
* which runs exactly on a `(route, fingerprint)` cache miss, so the
|
|
43
|
+
* fingerprint layer IS the decode-change dedup (StrictMode's dev double
|
|
44
|
+
* render reuses the ref cache and cannot double-emit), and `refresh` after
|
|
45
|
+
* the stable result returns, re-emitting the CACHED result when the pathname
|
|
46
|
+
* moved under an unchanged decode so the seam's `navigate`/`pathname` never
|
|
47
|
+
* go stale. Observations carry the full pre-`select` result, and the
|
|
48
|
+
* `OrThrow` hooks report the error observation BEFORE rethrowing — only
|
|
49
|
+
* render-phase can, since an effect never runs for a throwing render. Every
|
|
50
|
+
* emit sits behind `process.env.NODE_ENV !== "production"`, which bundlers
|
|
51
|
+
* constant-fold and erase along with the seam module; the spec each hook
|
|
52
|
+
* hands the emitter is built behind the same literal guard, so prod
|
|
53
|
+
* allocates nothing. `useRouter` and `usePathname` are called
|
|
54
|
+
* unconditionally in every hook (rules of hooks — a build-constant-guarded
|
|
55
|
+
* call would make hook order differ between dev and prod bundles); their
|
|
56
|
+
* cost is a referentially-stable context read each, and only the dev-only
|
|
57
|
+
* spec captures them. The `navigate` capability receives the panel's SEARCH
|
|
59
58
|
* STRING only and resolves it against `usePathname()` — basePath-/locale-
|
|
60
|
-
* relative, exactly what `router.replace` expects back (
|
|
61
|
-
*
|
|
59
|
+
* relative, exactly what `router.replace` expects back (the live hash is
|
|
60
|
+
* preserved at call time).
|
|
62
61
|
*/
|
|
63
62
|
/**
|
|
64
|
-
* Decoded route params as a `SafeResult` (discriminated on `status
|
|
65
|
-
* optionally projected through `options.select
|
|
63
|
+
* Decoded route params as a `SafeResult` (discriminated on `status`),
|
|
64
|
+
* optionally projected through `options.select`.
|
|
66
65
|
*
|
|
67
66
|
* `useParams()` returns `null` outside an App-Router tree — including the
|
|
68
67
|
* initial render of every pages-router page in a hybrid app — so a `null`
|
|
@@ -76,7 +75,7 @@ export declare function useRouteParams<R extends AnyAppRoute, U>(route: R, optio
|
|
|
76
75
|
/**
|
|
77
76
|
* Decoded route params, or a thrown {@link ParamsDecodeError} (→ nearest
|
|
78
77
|
* client error boundary) on a malformed URL. Optionally projected through
|
|
79
|
-
* `options.select
|
|
78
|
+
* `options.select`.
|
|
80
79
|
*
|
|
81
80
|
* A `null` `useParams()` (outside an App-Router tree, e.g. a hybrid app's
|
|
82
81
|
* pages-router initial render) degrades to `{}` so required params throw the
|
|
@@ -85,15 +84,15 @@ export declare function useRouteParams<R extends AnyAppRoute, U>(route: R, optio
|
|
|
85
84
|
export declare function useRouteParamsOrThrow<R extends AnyAppRoute>(route: R): InferRouteParams<R>;
|
|
86
85
|
export declare function useRouteParamsOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): U;
|
|
87
86
|
/**
|
|
88
|
-
* Decoded search params as a `SafeResult` (discriminated on `status
|
|
89
|
-
* optionally projected through `options.select
|
|
87
|
+
* Decoded search params as a `SafeResult` (discriminated on `status`),
|
|
88
|
+
* optionally projected through `options.select`.
|
|
90
89
|
*/
|
|
91
90
|
export declare function useSearch<R extends AnyAppRoute>(route: R): SafeResult<SearchOutputOf<R["~search"]>>;
|
|
92
91
|
export declare function useSearch<R extends AnyAppRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): SafeResult<U>;
|
|
93
92
|
/**
|
|
94
93
|
* Decoded search params, or a thrown {@link SearchDecodeError} (→ nearest
|
|
95
94
|
* client error boundary) on a malformed URL. Optionally projected through
|
|
96
|
-
* `options.select
|
|
95
|
+
* `options.select`.
|
|
97
96
|
*/
|
|
98
97
|
export declare function useSearchOrThrow<R extends AnyAppRoute>(route: R): SearchOutputOf<R["~search"]>;
|
|
99
98
|
export declare function useSearchOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): U;
|
package/dist/app.js
CHANGED
|
@@ -54,7 +54,7 @@ export function useRouteParamsOrThrow(route, options) {
|
|
|
54
54
|
};
|
|
55
55
|
const value = useStableResult(route, paramsFingerprint(route, params), () => {
|
|
56
56
|
// The duplicated decode call across the prod/dev branches is the price
|
|
57
|
-
// of literal-zero prod cost — the bundler keeps exactly one branch
|
|
57
|
+
// of literal-zero prod cost — the bundler keeps exactly one branch.
|
|
58
58
|
if (process.env.NODE_ENV === "production") {
|
|
59
59
|
return decodeParams(route, params);
|
|
60
60
|
}
|
|
@@ -66,7 +66,7 @@ export function useRouteParamsOrThrow(route, options) {
|
|
|
66
66
|
return data;
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
|
-
// Report BEFORE the throw reaches the error boundary
|
|
69
|
+
// Report BEFORE the throw reaches the error boundary. Only the
|
|
70
70
|
// decode-error class is observed — foreign errors are not URL facts
|
|
71
71
|
// the panel explains, matching safeDecode*'s taxonomy.
|
|
72
72
|
if (error instanceof ParamsDecodeError && spec !== undefined) {
|
|
@@ -127,7 +127,7 @@ export function useSearchOrThrow(route, options) {
|
|
|
127
127
|
wire: () => searchWireSnapshot(searchParams),
|
|
128
128
|
};
|
|
129
129
|
const value = useStableResult(route, searchParamsFingerprint(route, searchParams),
|
|
130
|
-
// decodeSearch is keyed on SearchOutputOf (
|
|
130
|
+
// decodeSearch is keyed on SearchOutputOf (SS6) — the correct
|
|
131
131
|
// public type — but AnyAppRoute erases its SC to `any`, so for a still-
|
|
132
132
|
// generic R the call's SearchOutputOf<R["~search"]> reduces to `unknown`
|
|
133
133
|
// on the value side while staying deferred on the annotation side. The
|
|
@@ -135,22 +135,21 @@ export function useSearchOrThrow(route, options) {
|
|
|
135
135
|
// rawSearch route now infers its schema output here, not a garbage
|
|
136
136
|
// {~kind, ~schema} shape. The cast appears in both branches below — the
|
|
137
137
|
// prod/dev split (and its duplicated decode call) is the price of
|
|
138
|
-
// literal-zero prod cost; the bundler keeps exactly one branch
|
|
138
|
+
// literal-zero prod cost; the bundler keeps exactly one branch.
|
|
139
139
|
() => {
|
|
140
140
|
if (process.env.NODE_ENV === "production") {
|
|
141
|
-
return decodeSearch(route["~search"], searchParams);
|
|
141
|
+
return decodeSearch(route["~search"], searchParams, route.path);
|
|
142
142
|
}
|
|
143
143
|
try {
|
|
144
|
-
const data = decodeSearch(route["~search"], searchParams);
|
|
144
|
+
const data = decodeSearch(route["~search"], searchParams, route.path);
|
|
145
145
|
if (spec !== undefined) {
|
|
146
146
|
emitter.observe(spec, { data, status: "success" });
|
|
147
147
|
}
|
|
148
148
|
return data;
|
|
149
149
|
}
|
|
150
150
|
catch (error) {
|
|
151
|
-
// Report BEFORE the throw reaches the error boundary
|
|
152
|
-
//
|
|
153
|
-
// taxonomy.
|
|
151
|
+
// Report BEFORE the throw reaches the error boundary; only the
|
|
152
|
+
// decode-error class is observed, matching safeDecode*'s taxonomy.
|
|
154
153
|
if (error instanceof SearchDecodeError && spec !== undefined) {
|
|
155
154
|
emitter.observe(spec, { error, status: "error" });
|
|
156
155
|
}
|
|
@@ -163,8 +162,8 @@ export function useSearchOrThrow(route, options) {
|
|
|
163
162
|
return useSelectedValue(value, options);
|
|
164
163
|
}
|
|
165
164
|
/**
|
|
166
|
-
* Real-Next fallback for the adapter seam
|
|
167
|
-
*
|
|
165
|
+
* Real-Next fallback for the adapter seam: the /testing provider overrides
|
|
166
|
+
* these reads through {@link AppNavigationContext}; with
|
|
168
167
|
* no provider mounted the context's `null` default resolves here, so
|
|
169
168
|
* production behavior (and this module's `next/navigation`-only bundle
|
|
170
169
|
* graph, per dist.test.ts) is unchanged.
|
|
@@ -179,7 +178,7 @@ const realAppAdapter = {
|
|
|
179
178
|
* Every hook resolves the adapter ONCE at its top and calls the adapter's
|
|
180
179
|
* reads unconditionally, exactly where the direct Next calls previously sat
|
|
181
180
|
* — hook call order is identical across renders and across provider
|
|
182
|
-
* presence
|
|
181
|
+
* presence.
|
|
183
182
|
*/
|
|
184
183
|
function useAppNavigation() {
|
|
185
184
|
return useContext(AppNavigationContext) ?? realAppAdapter;
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ type ParsedValues<T extends ParseArgsOptionsConfig> = ReturnType<typeof parseArg
|
|
|
15
15
|
options: T;
|
|
16
16
|
}>>["values"];
|
|
17
17
|
/**
|
|
18
|
-
* The shared command prologue
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* The shared command prologue: parse flags, print usage on a parse error
|
|
19
|
+
* (exit 2) or `--help` (exit 0), and reject positionals — no command takes
|
|
20
|
+
* one. Callers branch on `"exit" in result`; anything past the prologue
|
|
21
|
+
* (mode merging, flag exclusivity) stays per-command.
|
|
22
22
|
*/
|
|
23
23
|
export declare function parseCommandFlags<const T extends HelpOption & ParseArgsOptionsConfig>(argv: readonly string[], options: T, usage: string, { stderr, stdout }: ResolvedIo): {
|
|
24
24
|
exit: 0 | 2;
|
package/dist/cli-args.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { message } from "./cli-io.js";
|
|
3
3
|
/**
|
|
4
|
-
* The shared command prologue
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The shared command prologue: parse flags, print usage on a parse error
|
|
5
|
+
* (exit 2) or `--help` (exit 0), and reject positionals — no command takes
|
|
6
|
+
* one. Callers branch on `"exit" in result`; anything past the prologue
|
|
7
|
+
* (mode merging, flag exclusivity) stays per-command.
|
|
8
8
|
*/
|
|
9
9
|
export function parseCommandFlags(argv, options, usage, { stderr, stdout }) {
|
|
10
10
|
let parsed;
|
package/dist/cli-inputs.d.ts
CHANGED
|
@@ -18,13 +18,12 @@ export interface InputFlags {
|
|
|
18
18
|
export declare class NoRouteDirsError extends Error {
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Precedence lives in exactly this function
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* projects are both fine.
|
|
21
|
+
* Precedence lives in exactly this function: flags → config file → joint
|
|
22
|
+
* discovery. Paths resolve against the project root (= cwd, where `next`
|
|
23
|
+
* itself would run). Discovery only runs for dirs not explicitly given —
|
|
24
|
+
* passing both bypasses it (and its populated-ignored-dir config error)
|
|
25
|
+
* entirely, which is the documented escape hatch. Only when NEITHER dir
|
|
26
|
+
* exists is that an error: app-only and pages-only projects are both fine.
|
|
28
27
|
*
|
|
29
28
|
* Commands that already loaded the config file (for fields beyond these,
|
|
30
29
|
* e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once.
|
package/dist/cli-inputs.js
CHANGED
|
@@ -12,13 +12,12 @@ import { resolveRouteDirs } from "./scan.js";
|
|
|
12
12
|
export class NoRouteDirsError extends Error {
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Precedence lives in exactly this function
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* projects are both fine.
|
|
15
|
+
* Precedence lives in exactly this function: flags → config file → joint
|
|
16
|
+
* discovery. Paths resolve against the project root (= cwd, where `next`
|
|
17
|
+
* itself would run). Discovery only runs for dirs not explicitly given —
|
|
18
|
+
* passing both bypasses it (and its populated-ignored-dir config error)
|
|
19
|
+
* entirely, which is the documented escape hatch. Only when NEITHER dir
|
|
20
|
+
* exists is that an error: app-only and pages-only projects are both fine.
|
|
22
21
|
*
|
|
23
22
|
* Commands that already loaded the config file (for fields beyond these,
|
|
24
23
|
* e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once.
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runCli } from "./run-cli.js";
|
|
3
|
-
// The bin entry
|
|
3
|
+
// The bin entry: all logic lives in run-cli.ts so tests never execute
|
|
4
4
|
// this statement. exitCode, not exit() — pending stdio writes must flush.
|
|
5
5
|
process.exitCode = await runCli(process.argv.slice(2));
|
package/dist/collisions.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
/** A scanned route path labeled with the router that produced it
|
|
1
|
+
/** A scanned route path labeled with the router that produced it. */
|
|
2
2
|
export interface ScannedRoute {
|
|
3
3
|
path: string;
|
|
4
4
|
router: "app" | "pages";
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Route-collision failure mode
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Route-collision failure mode: states Next itself refuses to build have no
|
|
8
|
+
* valid artifact, so the scanners throw instead of emitting one. Composition
|
|
9
|
+
* points map this error to their own exits — CLI exit 2, `withTypedRoutes`
|
|
10
|
+
* throw during config evaluation, and a non-fatal loud log under watch (a
|
|
11
|
+
* collision mid-`--watch` is usually a file mid-move, so the last good
|
|
12
|
+
* artifact stays on disk).
|
|
13
13
|
*/
|
|
14
14
|
export declare class RouteCollisionError extends Error {
|
|
15
15
|
name: string;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Structural collisions — same detection pass, non-equal strings. Two
|
|
19
19
|
* states Next also refuses to build that plain string equality misses:
|
|
20
20
|
*
|
|
21
21
|
* - **Different slug names at one level**: `/x/[id]` + `/x/[slug]` — Next:
|
package/dist/collisions.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Route-collision failure mode
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Route-collision failure mode: states Next itself refuses to build have no
|
|
3
|
+
* valid artifact, so the scanners throw instead of emitting one. Composition
|
|
4
|
+
* points map this error to their own exits — CLI exit 2, `withTypedRoutes`
|
|
5
|
+
* throw during config evaluation, and a non-fatal loud log under watch (a
|
|
6
|
+
* collision mid-`--watch` is usually a file mid-move, so the last good
|
|
7
|
+
* artifact stays on disk).
|
|
8
8
|
*/
|
|
9
9
|
export class RouteCollisionError extends Error {
|
|
10
10
|
name = "RouteCollisionError";
|
|
@@ -22,7 +22,7 @@ const OPTIONAL_CATCH_ALL = /^\[\[\.\.\..+\]\]$/;
|
|
|
22
22
|
*/
|
|
23
23
|
const DYNAMIC_SEGMENT = /^(?:\[\[\.\.\.(?<optional>.+)\]\]|\[\.\.\.(?<catchAll>.+)\]|\[(?<plain>[^[\]]+)\])$/;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Structural collisions — same detection pass, non-equal strings. Two
|
|
26
26
|
* states Next also refuses to build that plain string equality misses:
|
|
27
27
|
*
|
|
28
28
|
* - **Different slug names at one level**: `/x/[id]` + `/x/[slug]` — Next:
|
|
@@ -56,7 +56,7 @@ export function assertNoStructuralCollisions(routes) {
|
|
|
56
56
|
const key = `${segments.slice(0, index).join("/")}#${String(index)}#${kind}`;
|
|
57
57
|
const existing = dynamicAt.get(key);
|
|
58
58
|
if (existing !== undefined && existing.segment !== segment) {
|
|
59
|
-
throw new RouteCollisionError(`route collision: "${existing.path}" (${existing.router}) and "${route.path}" (${route.router}) declare conflicting dynamic segments (${existing.segment} vs ${segment}) at the same position — Next refuses different slug names for the same dynamic path
|
|
59
|
+
throw new RouteCollisionError(`route collision: "${existing.path}" (${existing.router}) and "${route.path}" (${route.router}) declare conflicting dynamic segments (${existing.segment} vs ${segment}) at the same position — Next refuses different slug names for the same dynamic path`);
|
|
60
60
|
}
|
|
61
61
|
if (existing === undefined) {
|
|
62
62
|
dynamicAt.set(key, { ...route, segment });
|
|
@@ -67,7 +67,7 @@ export function assertNoStructuralCollisions(routes) {
|
|
|
67
67
|
const base = segments.length === 1 ? "/" : `/${segments.slice(0, -1).join("/")}`;
|
|
68
68
|
const baseRoute = byPath.get(base);
|
|
69
69
|
if (baseRoute !== undefined) {
|
|
70
|
-
throw new RouteCollisionError(`route collision: "${route.path}" (${route.router}) also matches "${base}" (${baseRoute.router}) — an optional catch-all has the same specificity as its base path, which Next refuses to build
|
|
70
|
+
throw new RouteCollisionError(`route collision: "${route.path}" (${route.router}) also matches "${base}" (${baseRoute.router}) — an optional catch-all has the same specificity as its base path, which Next refuses to build`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { type CliIo } from "../cli-io.js";
|
|
2
|
+
/** @internal `paramour doctor` flags — skill-drift test's source of truth. */
|
|
3
|
+
export declare const DOCTOR_OPTIONS: {
|
|
4
|
+
readonly help: {
|
|
5
|
+
readonly default: false;
|
|
6
|
+
readonly short: "h";
|
|
7
|
+
readonly type: "boolean";
|
|
8
|
+
};
|
|
9
|
+
readonly json: {
|
|
10
|
+
readonly default: false;
|
|
11
|
+
readonly type: "boolean";
|
|
12
|
+
};
|
|
13
|
+
};
|
|
2
14
|
/**
|
|
3
15
|
* @internal `paramour doctor` — a verification, so its exit codes follow
|
|
4
16
|
* `check`'s class: 0 pass/warn, 1 any fail, 2 only when doctor itself
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,9 +6,9 @@ const USAGE = [
|
|
|
6
6
|
"Usage: paramour doctor [options]",
|
|
7
7
|
"",
|
|
8
8
|
"Diagnose the project's paramour setup: config validity, artifact",
|
|
9
|
-
"freshness, next.config wrapping, version alignment,
|
|
10
|
-
"and route-definition discovery (which
|
|
11
|
-
"`paramour list`).",
|
|
9
|
+
"freshness, next.config wrapping, version alignment, installed agent",
|
|
10
|
+
"skills, tsconfig coverage, and route-definition discovery (which",
|
|
11
|
+
"evaluates matched modules, like `paramour list`).",
|
|
12
12
|
"",
|
|
13
13
|
"Exit codes: 0 all checks pass (warnings allowed), 1 any check fails.",
|
|
14
14
|
"",
|
|
@@ -16,6 +16,11 @@ const USAGE = [
|
|
|
16
16
|
" --help, -h show this help",
|
|
17
17
|
" --json machine-readable output",
|
|
18
18
|
].join("\n");
|
|
19
|
+
/** @internal `paramour doctor` flags — skill-drift test's source of truth. */
|
|
20
|
+
export const DOCTOR_OPTIONS = {
|
|
21
|
+
help: { default: false, short: "h", type: "boolean" },
|
|
22
|
+
json: { default: false, type: "boolean" },
|
|
23
|
+
};
|
|
19
24
|
/**
|
|
20
25
|
* @internal `paramour doctor` — a verification, so its exit codes follow
|
|
21
26
|
* `check`'s class: 0 pass/warn, 1 any fail, 2 only when doctor itself
|
|
@@ -23,10 +28,10 @@ const USAGE = [
|
|
|
23
28
|
*/
|
|
24
29
|
export async function runDoctor(argv, io) {
|
|
25
30
|
const { stderr, stdout } = resolveIo(io);
|
|
26
|
-
const parsed = parseCommandFlags(argv, {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
31
|
+
const parsed = parseCommandFlags(argv, DOCTOR_OPTIONS, USAGE, {
|
|
32
|
+
stderr,
|
|
33
|
+
stdout,
|
|
34
|
+
});
|
|
30
35
|
if ("exit" in parsed)
|
|
31
36
|
return parsed.exit;
|
|
32
37
|
const flags = parsed.values;
|
|
@@ -1,11 +1,59 @@
|
|
|
1
1
|
import { type CliIo } from "../cli-io.js";
|
|
2
|
+
/** @internal `paramour check` flags — skill-drift test's source of truth. */
|
|
3
|
+
export declare const CHECK_OPTIONS: {
|
|
4
|
+
readonly "app-dir": {
|
|
5
|
+
readonly type: "string";
|
|
6
|
+
};
|
|
7
|
+
readonly help: {
|
|
8
|
+
readonly default: false;
|
|
9
|
+
readonly short: "h";
|
|
10
|
+
readonly type: "boolean";
|
|
11
|
+
};
|
|
12
|
+
readonly "out-file": {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
};
|
|
15
|
+
readonly "page-extensions": {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
};
|
|
18
|
+
readonly "pages-dir": {
|
|
19
|
+
readonly type: "string";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** @internal `paramour generate` flags — skill-drift test's source of truth. */
|
|
23
|
+
export declare const GENERATE_OPTIONS: {
|
|
24
|
+
readonly check: {
|
|
25
|
+
readonly default: false;
|
|
26
|
+
readonly type: "boolean";
|
|
27
|
+
};
|
|
28
|
+
readonly watch: {
|
|
29
|
+
readonly default: false;
|
|
30
|
+
readonly type: "boolean";
|
|
31
|
+
};
|
|
32
|
+
readonly "app-dir": {
|
|
33
|
+
readonly type: "string";
|
|
34
|
+
};
|
|
35
|
+
readonly help: {
|
|
36
|
+
readonly default: false;
|
|
37
|
+
readonly short: "h";
|
|
38
|
+
readonly type: "boolean";
|
|
39
|
+
};
|
|
40
|
+
readonly "out-file": {
|
|
41
|
+
readonly type: "string";
|
|
42
|
+
};
|
|
43
|
+
readonly "page-extensions": {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
};
|
|
46
|
+
readonly "pages-dir": {
|
|
47
|
+
readonly type: "string";
|
|
48
|
+
};
|
|
49
|
+
};
|
|
2
50
|
/**
|
|
3
|
-
* @internal `paramour generate` and its `check` alias
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* usage/config/operational errors — route collisions included (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
51
|
+
* @internal `paramour generate` and its `check` alias, in-process testable:
|
|
52
|
+
* returns the exit code instead of exiting. Codes are grep-style so CI can
|
|
53
|
+
* tell drift from breakage: 0 success, 1 check-drift ONLY, 2
|
|
54
|
+
* usage/config/operational errors — route collisions included (Next itself
|
|
55
|
+
* fails that build, so there is no artifact to emit). Unlike the wrapper,
|
|
56
|
+
* which is never load-bearing, the CLI fails loudly — running it is explicit
|
|
57
|
+
* user intent.
|
|
10
58
|
*/
|
|
11
59
|
export declare function runGenerate(argv: readonly string[], io: CliIo, mode: "check" | "generate"): Promise<number>;
|
|
@@ -12,6 +12,14 @@ const SHARED_OPTIONS = {
|
|
|
12
12
|
"page-extensions": { type: "string" },
|
|
13
13
|
"pages-dir": { type: "string" },
|
|
14
14
|
};
|
|
15
|
+
/** @internal `paramour check` flags — skill-drift test's source of truth. */
|
|
16
|
+
export const CHECK_OPTIONS = SHARED_OPTIONS;
|
|
17
|
+
/** @internal `paramour generate` flags — skill-drift test's source of truth. */
|
|
18
|
+
export const GENERATE_OPTIONS = {
|
|
19
|
+
...SHARED_OPTIONS,
|
|
20
|
+
check: { default: false, type: "boolean" },
|
|
21
|
+
watch: { default: false, type: "boolean" },
|
|
22
|
+
};
|
|
15
23
|
const SHARED_OPTION_LINES = [
|
|
16
24
|
" --app-dir <dir> app directory (default: discovered app/ or src/app/)",
|
|
17
25
|
" --help, -h show this help",
|
|
@@ -38,24 +46,23 @@ const CHECK_USAGE = [
|
|
|
38
46
|
...SHARED_OPTION_LINES,
|
|
39
47
|
].join("\n");
|
|
40
48
|
/**
|
|
41
|
-
* @internal `paramour generate` and its `check` alias
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* usage/config/operational errors — route collisions included (
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
49
|
+
* @internal `paramour generate` and its `check` alias, in-process testable:
|
|
50
|
+
* returns the exit code instead of exiting. Codes are grep-style so CI can
|
|
51
|
+
* tell drift from breakage: 0 success, 1 check-drift ONLY, 2
|
|
52
|
+
* usage/config/operational errors — route collisions included (Next itself
|
|
53
|
+
* fails that build, so there is no artifact to emit). Unlike the wrapper,
|
|
54
|
+
* which is never load-bearing, the CLI fails loudly — running it is explicit
|
|
55
|
+
* user intent.
|
|
48
56
|
*/
|
|
49
57
|
export async function runGenerate(argv, io, mode) {
|
|
50
58
|
const { stderr, stdout } = resolveIo(io);
|
|
51
59
|
const usage = mode === "check" ? CHECK_USAGE : GENERATE_USAGE;
|
|
52
60
|
let flags;
|
|
53
61
|
if (mode === "generate") {
|
|
54
|
-
const parsed = parseCommandFlags(argv, {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}, usage, { stderr, stdout });
|
|
62
|
+
const parsed = parseCommandFlags(argv, GENERATE_OPTIONS, usage, {
|
|
63
|
+
stderr,
|
|
64
|
+
stdout,
|
|
65
|
+
});
|
|
59
66
|
if ("exit" in parsed)
|
|
60
67
|
return parsed.exit;
|
|
61
68
|
flags = parsed.values;
|
|
@@ -63,7 +70,7 @@ export async function runGenerate(argv, io, mode) {
|
|
|
63
70
|
else {
|
|
64
71
|
// `check` omits --check/--watch entirely: `paramour check --watch`
|
|
65
72
|
// fails as an unknown option, which is the right message for it.
|
|
66
|
-
const parsed = parseCommandFlags(argv,
|
|
73
|
+
const parsed = parseCommandFlags(argv, CHECK_OPTIONS, usage, {
|
|
67
74
|
stderr,
|
|
68
75
|
stdout,
|
|
69
76
|
});
|
|
@@ -105,7 +112,7 @@ function describeRoutes(result) {
|
|
|
105
112
|
];
|
|
106
113
|
return parts.length === 0 ? "0 routes" : parts.join(", ");
|
|
107
114
|
}
|
|
108
|
-
/** `--check
|
|
115
|
+
/** `--check`: exit 1 on any drift, including a missing artifact. */
|
|
109
116
|
function runCheck(inputs, stdout, stderr) {
|
|
110
117
|
let result;
|
|
111
118
|
try {
|
|
@@ -132,7 +139,7 @@ function runCheck(inputs, stdout, stderr) {
|
|
|
132
139
|
stderr("Run `paramour generate` and commit the result.");
|
|
133
140
|
return 1;
|
|
134
141
|
}
|
|
135
|
-
/** One-shot `paramour generate
|
|
142
|
+
/** One-shot `paramour generate`. */
|
|
136
143
|
function runOnce(inputs, stdout, stderr) {
|
|
137
144
|
let result;
|
|
138
145
|
try {
|
|
@@ -149,8 +156,8 @@ function runOnce(inputs, stdout, stderr) {
|
|
|
149
156
|
return 0;
|
|
150
157
|
}
|
|
151
158
|
/**
|
|
152
|
-
* `--watch
|
|
153
|
-
*
|
|
159
|
+
* `--watch`: the watcher behind the cross-process lock, over both route
|
|
160
|
+
* dirs. A declined lock exits 0 — another live watcher (usually `next dev`)
|
|
154
161
|
* is the designed dedupe case, and the initial generation already ran.
|
|
155
162
|
* Without an abort signal the returned promise stays pending; the process
|
|
156
163
|
* lives via the FSWatcher refs and dies with the standard signal exits
|
|
@@ -172,7 +179,7 @@ function runWatch(inputs, projectRoot, io, stdout, stderr) {
|
|
|
172
179
|
}
|
|
173
180
|
catch (error) {
|
|
174
181
|
// A corrupt lock location (e.g. a directory at the pidfile path) is an
|
|
175
|
-
// operational error, not a crash: exit 2 like every other one
|
|
182
|
+
// operational error, not a crash: exit 2 like every other one.
|
|
176
183
|
stderr(`paramour: ${message(error)}`);
|
|
177
184
|
return 2;
|
|
178
185
|
}
|
|
@@ -196,13 +203,13 @@ function runWatch(inputs, projectRoot, io, stdout, stderr) {
|
|
|
196
203
|
}
|
|
197
204
|
catch (error) {
|
|
198
205
|
if (error instanceof RouteCollisionError) {
|
|
199
|
-
//
|
|
200
|
-
// mid-move — log loudly every time, keep the last good
|
|
201
|
-
// on disk, keep running
|
|
206
|
+
// The collision watch exception: a collision mid-watch is usually
|
|
207
|
+
// a file mid-move — log loudly every time, keep the last good
|
|
208
|
+
// artifact on disk, keep running.
|
|
202
209
|
stderr(`paramour: ${message(error)}; keeping the last good artifact and watching for the fix`);
|
|
203
210
|
return;
|
|
204
211
|
}
|
|
205
|
-
throw error; // routed to onError by the watcher
|
|
212
|
+
throw error; // routed to onError by the watcher — non-fatal
|
|
206
213
|
}
|
|
207
214
|
},
|
|
208
215
|
});
|