@paramour-js/next 0.3.1 → 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 +48 -21
- 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 +59 -0
- package/dist/navigation-adapter.js +9 -0
- package/dist/observe.d.ts +14 -14
- package/dist/observe.js +4 -4
- package/dist/pages.d.ts +30 -31
- package/dist/pages.js +25 -9
- 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 +68 -0
- package/dist/testing.js +113 -0
- 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 +6 -2
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
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useParams, usePathname, useRouter, useSearchParams, } from "next/navigation";
|
|
3
3
|
import { decodeParams, decodeSearch, ParamsDecodeError, safeDecodeParams, safeDecodeSearch, SearchDecodeError, } from "paramour";
|
|
4
|
+
import { useContext } from "react";
|
|
4
5
|
import { searchWireSnapshot } from "./devtools-seam.js";
|
|
6
|
+
import { AppNavigationContext, } from "./navigation-adapter.js";
|
|
5
7
|
import { makeAppNavigate, useDevtoolsEmitter, } from "./observe.js";
|
|
6
8
|
import { paramsFingerprint, searchParamsFingerprint, useSelectedResult, useSelectedValue, useStableResult, } from "./select.js";
|
|
7
9
|
export function useRouteParams(route, options) {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const nav = useAppNavigation();
|
|
11
|
+
const params = nav.useParams() ?? {};
|
|
12
|
+
const router = nav.useRouter();
|
|
13
|
+
const pathname = nav.usePathname();
|
|
11
14
|
const emitter = useDevtoolsEmitter();
|
|
12
15
|
const spec = process.env.NODE_ENV === "production"
|
|
13
16
|
? undefined
|
|
@@ -33,9 +36,10 @@ export function useRouteParams(route, options) {
|
|
|
33
36
|
return useSelectedResult(result, options);
|
|
34
37
|
}
|
|
35
38
|
export function useRouteParamsOrThrow(route, options) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
+
const nav = useAppNavigation();
|
|
40
|
+
const params = nav.useParams() ?? {};
|
|
41
|
+
const router = nav.useRouter();
|
|
42
|
+
const pathname = nav.usePathname();
|
|
39
43
|
const emitter = useDevtoolsEmitter();
|
|
40
44
|
const spec = process.env.NODE_ENV === "production"
|
|
41
45
|
? undefined
|
|
@@ -50,7 +54,7 @@ export function useRouteParamsOrThrow(route, options) {
|
|
|
50
54
|
};
|
|
51
55
|
const value = useStableResult(route, paramsFingerprint(route, params), () => {
|
|
52
56
|
// The duplicated decode call across the prod/dev branches is the price
|
|
53
|
-
// of literal-zero prod cost — the bundler keeps exactly one branch
|
|
57
|
+
// of literal-zero prod cost — the bundler keeps exactly one branch.
|
|
54
58
|
if (process.env.NODE_ENV === "production") {
|
|
55
59
|
return decodeParams(route, params);
|
|
56
60
|
}
|
|
@@ -62,7 +66,7 @@ export function useRouteParamsOrThrow(route, options) {
|
|
|
62
66
|
return data;
|
|
63
67
|
}
|
|
64
68
|
catch (error) {
|
|
65
|
-
// Report BEFORE the throw reaches the error boundary
|
|
69
|
+
// Report BEFORE the throw reaches the error boundary. Only the
|
|
66
70
|
// decode-error class is observed — foreign errors are not URL facts
|
|
67
71
|
// the panel explains, matching safeDecode*'s taxonomy.
|
|
68
72
|
if (error instanceof ParamsDecodeError && spec !== undefined) {
|
|
@@ -77,9 +81,10 @@ export function useRouteParamsOrThrow(route, options) {
|
|
|
77
81
|
return useSelectedValue(value, options);
|
|
78
82
|
}
|
|
79
83
|
export function useSearch(route, options) {
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
84
|
+
const nav = useAppNavigation();
|
|
85
|
+
const searchParams = nav.useSearchParams();
|
|
86
|
+
const router = nav.useRouter();
|
|
87
|
+
const pathname = nav.usePathname();
|
|
83
88
|
const emitter = useDevtoolsEmitter();
|
|
84
89
|
const spec = process.env.NODE_ENV === "production"
|
|
85
90
|
? undefined
|
|
@@ -105,9 +110,10 @@ export function useSearch(route, options) {
|
|
|
105
110
|
return useSelectedResult(result, options);
|
|
106
111
|
}
|
|
107
112
|
export function useSearchOrThrow(route, options) {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
113
|
+
const nav = useAppNavigation();
|
|
114
|
+
const searchParams = nav.useSearchParams();
|
|
115
|
+
const router = nav.useRouter();
|
|
116
|
+
const pathname = nav.usePathname();
|
|
111
117
|
const emitter = useDevtoolsEmitter();
|
|
112
118
|
const spec = process.env.NODE_ENV === "production"
|
|
113
119
|
? undefined
|
|
@@ -121,7 +127,7 @@ export function useSearchOrThrow(route, options) {
|
|
|
121
127
|
wire: () => searchWireSnapshot(searchParams),
|
|
122
128
|
};
|
|
123
129
|
const value = useStableResult(route, searchParamsFingerprint(route, searchParams),
|
|
124
|
-
// decodeSearch is keyed on SearchOutputOf (
|
|
130
|
+
// decodeSearch is keyed on SearchOutputOf (SS6) — the correct
|
|
125
131
|
// public type — but AnyAppRoute erases its SC to `any`, so for a still-
|
|
126
132
|
// generic R the call's SearchOutputOf<R["~search"]> reduces to `unknown`
|
|
127
133
|
// on the value side while staying deferred on the annotation side. The
|
|
@@ -129,22 +135,21 @@ export function useSearchOrThrow(route, options) {
|
|
|
129
135
|
// rawSearch route now infers its schema output here, not a garbage
|
|
130
136
|
// {~kind, ~schema} shape. The cast appears in both branches below — the
|
|
131
137
|
// prod/dev split (and its duplicated decode call) is the price of
|
|
132
|
-
// literal-zero prod cost; the bundler keeps exactly one branch
|
|
138
|
+
// literal-zero prod cost; the bundler keeps exactly one branch.
|
|
133
139
|
() => {
|
|
134
140
|
if (process.env.NODE_ENV === "production") {
|
|
135
|
-
return decodeSearch(route["~search"], searchParams);
|
|
141
|
+
return decodeSearch(route["~search"], searchParams, route.path);
|
|
136
142
|
}
|
|
137
143
|
try {
|
|
138
|
-
const data = decodeSearch(route["~search"], searchParams);
|
|
144
|
+
const data = decodeSearch(route["~search"], searchParams, route.path);
|
|
139
145
|
if (spec !== undefined) {
|
|
140
146
|
emitter.observe(spec, { data, status: "success" });
|
|
141
147
|
}
|
|
142
148
|
return data;
|
|
143
149
|
}
|
|
144
150
|
catch (error) {
|
|
145
|
-
// Report BEFORE the throw reaches the error boundary
|
|
146
|
-
//
|
|
147
|
-
// taxonomy.
|
|
151
|
+
// Report BEFORE the throw reaches the error boundary; only the
|
|
152
|
+
// decode-error class is observed, matching safeDecode*'s taxonomy.
|
|
148
153
|
if (error instanceof SearchDecodeError && spec !== undefined) {
|
|
149
154
|
emitter.observe(spec, { error, status: "error" });
|
|
150
155
|
}
|
|
@@ -156,3 +161,25 @@ export function useSearchOrThrow(route, options) {
|
|
|
156
161
|
}
|
|
157
162
|
return useSelectedValue(value, options);
|
|
158
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Real-Next fallback for the adapter seam: the /testing provider overrides
|
|
166
|
+
* these reads through {@link AppNavigationContext}; with
|
|
167
|
+
* no provider mounted the context's `null` default resolves here, so
|
|
168
|
+
* production behavior (and this module's `next/navigation`-only bundle
|
|
169
|
+
* graph, per dist.test.ts) is unchanged.
|
|
170
|
+
*/
|
|
171
|
+
const realAppAdapter = {
|
|
172
|
+
useParams,
|
|
173
|
+
usePathname,
|
|
174
|
+
useRouter,
|
|
175
|
+
useSearchParams,
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Every hook resolves the adapter ONCE at its top and calls the adapter's
|
|
179
|
+
* reads unconditionally, exactly where the direct Next calls previously sat
|
|
180
|
+
* — hook call order is identical across renders and across provider
|
|
181
|
+
* presence.
|
|
182
|
+
*/
|
|
183
|
+
function useAppNavigation() {
|
|
184
|
+
return useContext(AppNavigationContext) ?? realAppAdapter;
|
|
185
|
+
}
|
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,11 +1,11 @@
|
|
|
1
1
|
import { type CliIo } from "../cli-io.js";
|
|
2
2
|
/**
|
|
3
|
-
* @internal `paramour generate` and its `check` alias
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* usage/config/operational errors — route collisions included (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* @internal `paramour generate` and its `check` alias, in-process testable:
|
|
4
|
+
* returns the exit code instead of exiting. Codes are grep-style so CI can
|
|
5
|
+
* tell drift from breakage: 0 success, 1 check-drift ONLY, 2
|
|
6
|
+
* usage/config/operational errors — route collisions included (Next itself
|
|
7
|
+
* fails that build, so there is no artifact to emit). Unlike the wrapper,
|
|
8
|
+
* which is never load-bearing, the CLI fails loudly — running it is explicit
|
|
9
|
+
* user intent.
|
|
10
10
|
*/
|
|
11
11
|
export declare function runGenerate(argv: readonly string[], io: CliIo, mode: "check" | "generate"): Promise<number>;
|
|
@@ -38,13 +38,13 @@ const CHECK_USAGE = [
|
|
|
38
38
|
...SHARED_OPTION_LINES,
|
|
39
39
|
].join("\n");
|
|
40
40
|
/**
|
|
41
|
-
* @internal `paramour generate` and its `check` alias
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* usage/config/operational errors — route collisions included (
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
41
|
+
* @internal `paramour generate` and its `check` alias, in-process testable:
|
|
42
|
+
* returns the exit code instead of exiting. Codes are grep-style so CI can
|
|
43
|
+
* tell drift from breakage: 0 success, 1 check-drift ONLY, 2
|
|
44
|
+
* usage/config/operational errors — route collisions included (Next itself
|
|
45
|
+
* fails that build, so there is no artifact to emit). Unlike the wrapper,
|
|
46
|
+
* which is never load-bearing, the CLI fails loudly — running it is explicit
|
|
47
|
+
* user intent.
|
|
48
48
|
*/
|
|
49
49
|
export async function runGenerate(argv, io, mode) {
|
|
50
50
|
const { stderr, stdout } = resolveIo(io);
|
|
@@ -105,7 +105,7 @@ function describeRoutes(result) {
|
|
|
105
105
|
];
|
|
106
106
|
return parts.length === 0 ? "0 routes" : parts.join(", ");
|
|
107
107
|
}
|
|
108
|
-
/** `--check
|
|
108
|
+
/** `--check`: exit 1 on any drift, including a missing artifact. */
|
|
109
109
|
function runCheck(inputs, stdout, stderr) {
|
|
110
110
|
let result;
|
|
111
111
|
try {
|
|
@@ -132,7 +132,7 @@ function runCheck(inputs, stdout, stderr) {
|
|
|
132
132
|
stderr("Run `paramour generate` and commit the result.");
|
|
133
133
|
return 1;
|
|
134
134
|
}
|
|
135
|
-
/** One-shot `paramour generate
|
|
135
|
+
/** One-shot `paramour generate`. */
|
|
136
136
|
function runOnce(inputs, stdout, stderr) {
|
|
137
137
|
let result;
|
|
138
138
|
try {
|
|
@@ -149,8 +149,8 @@ function runOnce(inputs, stdout, stderr) {
|
|
|
149
149
|
return 0;
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
|
-
* `--watch
|
|
153
|
-
*
|
|
152
|
+
* `--watch`: the watcher behind the cross-process lock, over both route
|
|
153
|
+
* dirs. A declined lock exits 0 — another live watcher (usually `next dev`)
|
|
154
154
|
* is the designed dedupe case, and the initial generation already ran.
|
|
155
155
|
* Without an abort signal the returned promise stays pending; the process
|
|
156
156
|
* lives via the FSWatcher refs and dies with the standard signal exits
|
|
@@ -172,7 +172,7 @@ function runWatch(inputs, projectRoot, io, stdout, stderr) {
|
|
|
172
172
|
}
|
|
173
173
|
catch (error) {
|
|
174
174
|
// 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
|
|
175
|
+
// operational error, not a crash: exit 2 like every other one.
|
|
176
176
|
stderr(`paramour: ${message(error)}`);
|
|
177
177
|
return 2;
|
|
178
178
|
}
|
|
@@ -196,13 +196,13 @@ function runWatch(inputs, projectRoot, io, stdout, stderr) {
|
|
|
196
196
|
}
|
|
197
197
|
catch (error) {
|
|
198
198
|
if (error instanceof RouteCollisionError) {
|
|
199
|
-
//
|
|
200
|
-
// mid-move — log loudly every time, keep the last good
|
|
201
|
-
// on disk, keep running
|
|
199
|
+
// The collision watch exception: a collision mid-watch is usually
|
|
200
|
+
// a file mid-move — log loudly every time, keep the last good
|
|
201
|
+
// artifact on disk, keep running.
|
|
202
202
|
stderr(`paramour: ${message(error)}; keeping the last good artifact and watching for the fix`);
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
-
throw error; // routed to onError by the watcher
|
|
205
|
+
throw error; // routed to onError by the watcher — non-fatal
|
|
206
206
|
}
|
|
207
207
|
},
|
|
208
208
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function runInit(argv, io) {
|
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
// A .mjs/.json left behind would be shadowed by the scaffold under the
|
|
70
|
-
// ts-first discovery order
|
|
70
|
+
// ts-first discovery order — --force must truly replace it.
|
|
71
71
|
if (existing !== undefined && existing !== "paramour.config.ts" && !dry) {
|
|
72
72
|
unlinkSync(join(projectRoot, existing));
|
|
73
73
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shape of `paramour.config.{ts,mjs,json}`
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Shape of `paramour.config.{ts,mjs,json}` — the CLI's config file. Every
|
|
3
|
+
* field is optional; the CLI's precedence is flags → this file → inference.
|
|
4
|
+
* `.ts`/`.mjs` files default-export this object.
|
|
5
5
|
*/
|
|
6
6
|
export interface ParamourConfig {
|
|
7
|
-
/** App dir, relative to the project root; default: joint discovery
|
|
7
|
+
/** App dir, relative to the project root; default: joint discovery. */
|
|
8
8
|
appDir?: string;
|
|
9
|
-
/** Artifact path, relative to the project root
|
|
9
|
+
/** Artifact path, relative to the project root — the escape hatch. */
|
|
10
10
|
outFile?: string;
|
|
11
11
|
/** Page extensions, no leading dot; default: Next's four. */
|
|
12
12
|
pageExtensions?: string[];
|
|
13
|
-
/** Pages dir, relative to the project root; default: joint discovery
|
|
13
|
+
/** Pages dir, relative to the project root; default: joint discovery. */
|
|
14
14
|
pagesDir?: string;
|
|
15
15
|
/**
|
|
16
16
|
* Globs (relative to the project root) of modules exporting route
|
|
@@ -20,14 +20,14 @@ export interface ParamourConfig {
|
|
|
20
20
|
*/
|
|
21
21
|
routeFiles?: string[];
|
|
22
22
|
}
|
|
23
|
-
/** @internal Discovery order at the project root
|
|
23
|
+
/** @internal Discovery order at the project root — first match wins. */
|
|
24
24
|
export declare const CONFIG_FILE_NAMES: readonly ["paramour.config.ts", "paramour.config.mjs", "paramour.config.json"];
|
|
25
25
|
/**
|
|
26
26
|
* @internal Load and validate the project's config file, or `undefined`
|
|
27
27
|
* when none exists. No upward traversal — the documented contract is three
|
|
28
|
-
* filenames at the project root
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* filenames at the project root. jiti is imported dynamically so only CLI
|
|
29
|
+
* runs that actually have a `.ts`/`.mjs` config pay for it;
|
|
30
|
+
* `withTypedRoutes` users never execute it.
|
|
31
31
|
*/
|
|
32
32
|
export declare function loadConfigFile(projectRoot: string): Promise<undefined | {
|
|
33
33
|
config: ParamourConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
/** @internal Discovery order at the project root
|
|
3
|
+
/** @internal Discovery order at the project root — first match wins. */
|
|
4
4
|
export const CONFIG_FILE_NAMES = [
|
|
5
5
|
"paramour.config.ts",
|
|
6
6
|
"paramour.config.mjs",
|
|
@@ -9,9 +9,9 @@ export const CONFIG_FILE_NAMES = [
|
|
|
9
9
|
/**
|
|
10
10
|
* @internal Load and validate the project's config file, or `undefined`
|
|
11
11
|
* when none exists. No upward traversal — the documented contract is three
|
|
12
|
-
* filenames at the project root
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* filenames at the project root. jiti is imported dynamically so only CLI
|
|
13
|
+
* runs that actually have a `.ts`/`.mjs` config pay for it;
|
|
14
|
+
* `withTypedRoutes` users never execute it.
|
|
15
15
|
*/
|
|
16
16
|
export async function loadConfigFile(projectRoot) {
|
|
17
17
|
for (const name of CONFIG_FILE_NAMES) {
|