@paramour-js/next 0.10.0 → 0.11.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.js +7 -1
- package/dist/doctor/checks.js +62 -10
- package/dist/emit.d.ts +12 -3
- package/dist/emit.js +14 -2
- package/dist/generate.d.ts +4 -3
- package/dist/generate.js +6 -5
- package/dist/init/wrap-next-config.d.ts +10 -0
- package/dist/init/wrap-next-config.js +124 -0
- package/dist/navigation-adapter.d.ts +1 -1
- package/package.json +2 -2
- package/skills/paramour/references/authoring.md +1 -1
- package/skills/paramour/references/reference.md +9 -9
package/dist/app.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
2
|
+
// Extensionful for the same reason as pages.ts's `next/router.js`: `next`
|
|
3
|
+
// ships no `exports` map, so wherever this package is loaded through Node's
|
|
4
|
+
// ESM resolver (Vitest externalizes node_modules) the bare `next/navigation`
|
|
5
|
+
// dies with ERR_MODULE_NOT_FOUND. The root stub `next/navigation.js` is the
|
|
6
|
+
// file the bare specifier resolves to, and Next's bundler aliases are keyed
|
|
7
|
+
// on that file, so bundles see the same module either way.
|
|
8
|
+
import { useParams, usePathname, useRouter, useSearchParams, } from "next/navigation.js";
|
|
3
9
|
import { decodeParams, decodeSearch, ParamsDecodeError, safeDecodeParams, safeDecodeSearch, SearchDecodeError, } from "paramour";
|
|
4
10
|
import { useContext } from "react";
|
|
5
11
|
import { searchWireSnapshot } from "./devtools-emit.js";
|
package/dist/doctor/checks.js
CHANGED
|
@@ -5,7 +5,7 @@ import { message } from "../cli-io.js";
|
|
|
5
5
|
import { loadConfigFile } from "../config.js";
|
|
6
6
|
import { checkArtifact, formatRouteDiff, } from "../generate.js";
|
|
7
7
|
import { tsconfigCheck } from "../init/scaffold.js";
|
|
8
|
-
import { detectWrapState, findNextConfig } from "../init/wrap-next-config.js";
|
|
8
|
+
import { detectWrapState, findNextConfig, readTrailingSlash, } from "../init/wrap-next-config.js";
|
|
9
9
|
import { discoverRouteDefinitions, routeKey, } from "../list/discover-route-defs.js";
|
|
10
10
|
import { scanRoutes } from "../scan.js";
|
|
11
11
|
import { skillsDoctorChecks } from "../skills/doctor.js";
|
|
@@ -151,7 +151,16 @@ export async function runDoctorChecks(projectRoot) {
|
|
|
151
151
|
status: coverage.ok ? "pass" : "warn",
|
|
152
152
|
});
|
|
153
153
|
// 8. Route-definition discovery health (list's engine).
|
|
154
|
-
|
|
154
|
+
const discovered = await discoveryCheck(projectRoot, config, routes);
|
|
155
|
+
checks.push(discovered.check);
|
|
156
|
+
// 9. Route definitions build the URLs next.config serves. Reported only
|
|
157
|
+
// when there is an answer: definitions exist and the config's
|
|
158
|
+
// trailingSlash reads statically.
|
|
159
|
+
if (nextConfig !== undefined && discovered.definitions.length > 0) {
|
|
160
|
+
const check = await trailingSlashCheck(nextConfig.path, discovered.definitions);
|
|
161
|
+
if (check !== undefined)
|
|
162
|
+
checks.push(check);
|
|
163
|
+
}
|
|
155
164
|
return checks;
|
|
156
165
|
}
|
|
157
166
|
async function discoveryCheck(projectRoot, config, routes) {
|
|
@@ -177,18 +186,24 @@ async function discoveryCheck(projectRoot, config, routes) {
|
|
|
177
186
|
detail.push(`duplicate definition of ${duplicate.path} (${duplicate.router}) in ${duplicate.file} — ${duplicate.firstFile} wins`);
|
|
178
187
|
}
|
|
179
188
|
return {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
check: {
|
|
190
|
+
detail,
|
|
191
|
+
label: `route definitions: ${String(discovery.definitions.length)} found in ${String(files.size)} module${files.size === 1 ? "" : "s"}`,
|
|
192
|
+
status: discovery.loadFailures.length > 0 || discovery.duplicates.length > 0
|
|
193
|
+
? "warn"
|
|
194
|
+
: "pass",
|
|
195
|
+
},
|
|
196
|
+
definitions: discovery.definitions,
|
|
185
197
|
};
|
|
186
198
|
}
|
|
187
199
|
catch (error) {
|
|
188
200
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
check: {
|
|
202
|
+
detail: [message(error)],
|
|
203
|
+
label: "route definitions: discovery failed",
|
|
204
|
+
status: "warn",
|
|
205
|
+
},
|
|
206
|
+
definitions: [],
|
|
192
207
|
};
|
|
193
208
|
}
|
|
194
209
|
}
|
|
@@ -206,6 +221,43 @@ function readManifest(projectRoot, name) {
|
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Warn-level: a route whose `trailingSlash` disagrees with next.config's
|
|
226
|
+
* builds a URL the app only reaches through a redirect (or, in a static
|
|
227
|
+
* export, a path with no file behind it). `undefined` when the config's value
|
|
228
|
+
* cannot be read statically — no finding beats a guessed one.
|
|
229
|
+
*/
|
|
230
|
+
async function trailingSlashCheck(nextConfigPath, definitions) {
|
|
231
|
+
let configured;
|
|
232
|
+
try {
|
|
233
|
+
configured = await readTrailingSlash(readFileSync(nextConfigPath, "utf8"));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
if (configured === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
const name = basename(nextConfigPath);
|
|
241
|
+
const setting = `trailingSlash: ${String(configured)}`;
|
|
242
|
+
// The routes come from the app's own paramour, which may predate the
|
|
243
|
+
// option and lack the member: missing reads as the R6 default.
|
|
244
|
+
const mismatched = definitions.filter((definition) => (definition.route["~trailingSlash"] ?? false) !==
|
|
245
|
+
configured);
|
|
246
|
+
if (mismatched.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
label: `trailing slash: route definitions match ${name} (${setting})`,
|
|
249
|
+
status: "pass",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const fix = configured
|
|
253
|
+
? "add trailingSlash: true to its definition"
|
|
254
|
+
: "remove trailingSlash: true from its definition";
|
|
255
|
+
return {
|
|
256
|
+
detail: mismatched.map((definition) => `${definition.route.path} (${definition.route["~router"]}) in ${definition.file}: ${fix}`),
|
|
257
|
+
label: `trailing slash: ${String(mismatched.length)} route definition${mismatched.length === 1 ? "" : "s"} disagree${mismatched.length === 1 ? "s" : ""} with ${name} (${setting})`,
|
|
258
|
+
status: "warn",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
209
261
|
function versionCheck(projectRoot) {
|
|
210
262
|
const coreManifest = readManifest(projectRoot, "paramour");
|
|
211
263
|
const nextManifest = readManifest(projectRoot, "@paramour-js/next");
|
package/dist/emit.d.ts
CHANGED
|
@@ -6,11 +6,18 @@ export interface WriteIfChangedResult {
|
|
|
6
6
|
*/
|
|
7
7
|
previousContent: null | string;
|
|
8
8
|
/**
|
|
9
|
-
* `false` on a
|
|
10
|
-
* loop hang off.
|
|
9
|
+
* `false` on a no-op (identical up to line endings) — the signal `--check`
|
|
10
|
+
* and the watch loop hang off.
|
|
11
11
|
*/
|
|
12
12
|
written: boolean;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Content equality that ignores CRLF vs LF. A consumer repo with git
|
|
16
|
+
* `core.autocrlf=true` checks the committed LF artifact out as CRLF; that
|
|
17
|
+
* flip is not drift, and treating it as drift fails every strict build and
|
|
18
|
+
* `paramour check` on a fresh Windows checkout.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sameContent(a: string, b: string): boolean;
|
|
14
21
|
/** Input of {@link emitArtifact} — one union per router. */
|
|
15
22
|
export interface EmitRoutes {
|
|
16
23
|
appRoutes: readonly string[];
|
|
@@ -30,6 +37,8 @@ export declare function emitArtifact(routes: EmitRoutes): string;
|
|
|
30
37
|
/**
|
|
31
38
|
* Compare-before-write: a no-op regeneration must not touch the file — that
|
|
32
39
|
* property is what prevents TS-server churn, watch-loop feedback, and
|
|
33
|
-
* concurrent-generator races.
|
|
40
|
+
* concurrent-generator races. A file differing only in line endings is a
|
|
41
|
+
* no-op too, so a CRLF checkout keeps its endings and `git status` stays
|
|
42
|
+
* clean; a real change is written LF.
|
|
34
43
|
*/
|
|
35
44
|
export declare function writeIfChanged(filePath: string, content: string): WriteIfChangedResult;
|
package/dist/emit.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Content equality that ignores CRLF vs LF. A consumer repo with git
|
|
5
|
+
* `core.autocrlf=true` checks the committed LF artifact out as CRLF; that
|
|
6
|
+
* flip is not drift, and treating it as drift fails every strict build and
|
|
7
|
+
* `paramour check` on a fresh Windows checkout.
|
|
8
|
+
*/
|
|
9
|
+
export function sameContent(a, b) {
|
|
10
|
+
return a.replaceAll("\r\n", "\n") === b.replaceAll("\r\n", "\n");
|
|
11
|
+
}
|
|
3
12
|
const HEADER = "// Generated by @paramour-js/next. Do not edit — regenerate with `paramour generate`.";
|
|
4
13
|
/**
|
|
5
14
|
* Artifact text for the per-router route unions: deterministic — sorted,
|
|
@@ -59,14 +68,17 @@ export function emitArtifact(routes) {
|
|
|
59
68
|
/**
|
|
60
69
|
* Compare-before-write: a no-op regeneration must not touch the file — that
|
|
61
70
|
* property is what prevents TS-server churn, watch-loop feedback, and
|
|
62
|
-
* concurrent-generator races.
|
|
71
|
+
* concurrent-generator races. A file differing only in line endings is a
|
|
72
|
+
* no-op too, so a CRLF checkout keeps its endings and `git status` stays
|
|
73
|
+
* clean; a real change is written LF.
|
|
63
74
|
*/
|
|
64
75
|
export function writeIfChanged(filePath, content) {
|
|
65
76
|
const previousContent = existsSync(filePath)
|
|
66
77
|
? readFileSync(filePath, "utf8")
|
|
67
78
|
: null;
|
|
68
|
-
if (previousContent
|
|
79
|
+
if (previousContent !== null && sameContent(previousContent, content)) {
|
|
69
80
|
return { previousContent, written: false };
|
|
81
|
+
}
|
|
70
82
|
// The outFile escape hatch may point into a not-yet-existing directory.
|
|
71
83
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
72
84
|
writeFileSync(filePath, content);
|
package/dist/generate.d.ts
CHANGED
|
@@ -40,9 +40,10 @@ export interface RouterDrift {
|
|
|
40
40
|
disappeared: string[];
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* `--check`: scan to memory and
|
|
44
|
-
*
|
|
45
|
-
* CI-degrades-to-world-A case the committed
|
|
43
|
+
* `--check`: scan to memory and compare against disk (line endings aside, as
|
|
44
|
+
* in {@link writeIfChanged}) — never writes. A missing artifact is drift, not
|
|
45
|
+
* an error: that is exactly the CI-degrades-to-world-A case the committed
|
|
46
|
+
* file exists to prevent.
|
|
46
47
|
*/
|
|
47
48
|
export declare function checkArtifact(inputs: GenerateInputs): CheckResult;
|
|
48
49
|
/**
|
package/dist/generate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { emitArtifact, writeIfChanged } from "./emit.js";
|
|
2
|
+
import { emitArtifact, sameContent, 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
|
|
@@ -11,9 +11,10 @@ import { scanRoutes } from "./scan.js";
|
|
|
11
11
|
const MEMBER_HEADER = /^\s*(appRoutes|pagesRoutes):\s*$/;
|
|
12
12
|
const UNION_MEMBER = /^\s*\| "(.*)";?\s*$/;
|
|
13
13
|
/**
|
|
14
|
-
* `--check`: scan to memory and
|
|
15
|
-
*
|
|
16
|
-
* CI-degrades-to-world-A case the committed
|
|
14
|
+
* `--check`: scan to memory and compare against disk (line endings aside, as
|
|
15
|
+
* in {@link writeIfChanged}) — never writes. A missing artifact is drift, not
|
|
16
|
+
* an error: that is exactly the CI-degrades-to-world-A case the committed
|
|
17
|
+
* file exists to prevent.
|
|
17
18
|
*/
|
|
18
19
|
export function checkArtifact(inputs) {
|
|
19
20
|
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
@@ -21,7 +22,7 @@ export function checkArtifact(inputs) {
|
|
|
21
22
|
const current = existsSync(inputs.artifactPath)
|
|
22
23
|
? readFileSync(inputs.artifactPath, "utf8")
|
|
23
24
|
: null;
|
|
24
|
-
if (current
|
|
25
|
+
if (current !== null && sameContent(current, expected)) {
|
|
25
26
|
return {
|
|
26
27
|
app: { appeared: [], disappeared: [] },
|
|
27
28
|
missingFile: false,
|
|
@@ -28,6 +28,16 @@ export declare function detectWrapState(source: string): Promise<"not-wrapped" |
|
|
|
28
28
|
export declare function findNextConfig(projectRoot: string): FoundNextConfig | undefined;
|
|
29
29
|
/** The `manual` fallback text — also printed when no config file exists. */
|
|
30
30
|
export declare function manualSnippet(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Reads `trailingSlash` from a next.config source for `doctor`, statically:
|
|
33
|
+
* the default export (or CJS `module.exports`) is followed through wrapper
|
|
34
|
+
* calls (first argument), TS `as`/`satisfies`, and top-level `const`
|
|
35
|
+
* bindings to an object literal. A literal without the key is Next's default,
|
|
36
|
+
* `false`. Anything else (a config function, a non-literal value, a spread
|
|
37
|
+
* that could carry the key) is `undefined`: doctor stays quiet rather than
|
|
38
|
+
* guess, since evaluating the config would run user code.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readTrailingSlash(source: string): Promise<boolean | undefined>;
|
|
31
41
|
/**
|
|
32
42
|
* The transform: add the named import (unless present under any alias) and
|
|
33
43
|
* rewrap the default export — identifier, object literal, existing wrapper
|
|
@@ -41,6 +41,49 @@ export function manualSnippet() {
|
|
|
41
41
|
"export default withTypedRoutes(nextConfig);",
|
|
42
42
|
].join("\n");
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Reads `trailingSlash` from a next.config source for `doctor`, statically:
|
|
46
|
+
* the default export (or CJS `module.exports`) is followed through wrapper
|
|
47
|
+
* calls (first argument), TS `as`/`satisfies`, and top-level `const`
|
|
48
|
+
* bindings to an object literal. A literal without the key is Next's default,
|
|
49
|
+
* `false`. Anything else (a config function, a non-literal value, a spread
|
|
50
|
+
* that could carry the key) is `undefined`: doctor stays quiet rather than
|
|
51
|
+
* guess, since evaluating the config would run user code.
|
|
52
|
+
*/
|
|
53
|
+
export async function readTrailingSlash(source) {
|
|
54
|
+
const { parseModule } = await import("magicast");
|
|
55
|
+
let program;
|
|
56
|
+
try {
|
|
57
|
+
program = parseModule(source).$ast;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const body = asNodes(program.body);
|
|
63
|
+
const bindings = new Map();
|
|
64
|
+
let exported;
|
|
65
|
+
for (const statement of body) {
|
|
66
|
+
if (statement.type === "VariableDeclaration") {
|
|
67
|
+
for (const declarator of asNodes(statement.declarations)) {
|
|
68
|
+
const id = asNode(declarator.id);
|
|
69
|
+
if (id?.type === "Identifier" && typeof id.name === "string") {
|
|
70
|
+
bindings.set(id.name, declarator.init);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (statement.type === "ExportDefaultDeclaration") {
|
|
75
|
+
exported = statement.declaration;
|
|
76
|
+
}
|
|
77
|
+
else if (statement.type === "ExpressionStatement") {
|
|
78
|
+
const expression = asNode(statement.expression);
|
|
79
|
+
if (expression?.type === "AssignmentExpression" &&
|
|
80
|
+
isModuleExports(asNode(expression.left))) {
|
|
81
|
+
exported = expression.right;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return trailingSlashOf(exported, bindings, 0);
|
|
86
|
+
}
|
|
44
87
|
/**
|
|
45
88
|
* The transform: add the named import (unless present under any alias) and
|
|
46
89
|
* rewrap the default export — identifier, object literal, existing wrapper
|
|
@@ -82,6 +125,28 @@ export async function wrapNextConfigSource(source) {
|
|
|
82
125
|
return { snippet: manualSnippet(), status: "manual" };
|
|
83
126
|
}
|
|
84
127
|
}
|
|
128
|
+
function asNode(value) {
|
|
129
|
+
return typeof value === "object" &&
|
|
130
|
+
value !== null &&
|
|
131
|
+
typeof value.type === "string"
|
|
132
|
+
? value
|
|
133
|
+
: undefined;
|
|
134
|
+
}
|
|
135
|
+
function asNodes(value) {
|
|
136
|
+
return Array.isArray(value)
|
|
137
|
+
? value.flatMap((item) => asNode(item) ?? [])
|
|
138
|
+
: [];
|
|
139
|
+
}
|
|
140
|
+
function isModuleExports(node) {
|
|
141
|
+
if (node?.type !== "MemberExpression" || node.computed === true)
|
|
142
|
+
return false;
|
|
143
|
+
const object = asNode(node.object);
|
|
144
|
+
const property = asNode(node.property);
|
|
145
|
+
return (object?.type === "Identifier" &&
|
|
146
|
+
object.name === "module" &&
|
|
147
|
+
property?.type === "Identifier" &&
|
|
148
|
+
property.name === "exports");
|
|
149
|
+
}
|
|
85
150
|
function isWrappedCall(value, local) {
|
|
86
151
|
if (typeof value !== "object" || value === null)
|
|
87
152
|
return false;
|
|
@@ -94,6 +159,65 @@ function isWrappedCall(value, local) {
|
|
|
94
159
|
((local !== undefined && node.$callee === local) ||
|
|
95
160
|
node.$callee.endsWith(".withTypedRoutes")));
|
|
96
161
|
}
|
|
162
|
+
function propertyName(node) {
|
|
163
|
+
if (node.computed === true)
|
|
164
|
+
return undefined;
|
|
165
|
+
const key = asNode(node.key);
|
|
166
|
+
if (key?.type === "Identifier" && typeof key.name === "string") {
|
|
167
|
+
return key.name;
|
|
168
|
+
}
|
|
169
|
+
if (key?.type === "StringLiteral" && typeof key.value === "string") {
|
|
170
|
+
return key.value;
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* One resolution step of {@link readTrailingSlash}. The depth bound stops a
|
|
176
|
+
* self-referencing binding (`const a = f(a)`) from looping.
|
|
177
|
+
*/
|
|
178
|
+
function trailingSlashOf(value, bindings, depth) {
|
|
179
|
+
const node = asNode(value);
|
|
180
|
+
if (node === undefined || depth > 16)
|
|
181
|
+
return undefined;
|
|
182
|
+
switch (node.type) {
|
|
183
|
+
case "CallExpression": {
|
|
184
|
+
const [first] = asNodes(node.arguments);
|
|
185
|
+
return trailingSlashOf(first, bindings, depth + 1);
|
|
186
|
+
}
|
|
187
|
+
case "Identifier": {
|
|
188
|
+
return typeof node.name === "string" && bindings.has(node.name)
|
|
189
|
+
? trailingSlashOf(bindings.get(node.name), bindings, depth + 1)
|
|
190
|
+
: undefined;
|
|
191
|
+
}
|
|
192
|
+
case "ObjectExpression": {
|
|
193
|
+
// Last write wins, as at runtime; any spread could carry the key.
|
|
194
|
+
let result = false;
|
|
195
|
+
for (const property of asNodes(node.properties)) {
|
|
196
|
+
if (property.type === "SpreadElement")
|
|
197
|
+
return undefined;
|
|
198
|
+
if (propertyName(property) !== "trailingSlash")
|
|
199
|
+
continue;
|
|
200
|
+
const literal = asNode(property.value);
|
|
201
|
+
result =
|
|
202
|
+
property.type === "ObjectProperty" &&
|
|
203
|
+
literal?.type === "BooleanLiteral" &&
|
|
204
|
+
typeof literal.value === "boolean"
|
|
205
|
+
? literal.value
|
|
206
|
+
: undefined;
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
case "ParenthesizedExpression":
|
|
211
|
+
case "TSAsExpression":
|
|
212
|
+
case "TSNonNullExpression":
|
|
213
|
+
case "TSSatisfiesExpression": {
|
|
214
|
+
return trailingSlashOf(node.expression, bindings, depth + 1);
|
|
215
|
+
}
|
|
216
|
+
default: {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
97
221
|
function withTypedRoutesLocal(items) {
|
|
98
222
|
return items.find((item) => item.from === "@paramour-js/next" && item.imported === "withTypedRoutes")?.local;
|
|
99
223
|
}
|
|
@@ -12,7 +12,7 @@ import type { ParamsSource } from "paramour";
|
|
|
12
12
|
* (`useContext(ctx) ?? realAdapter`), so neither this module nor the
|
|
13
13
|
* /testing entry ever drags a `next/*` specifier into its graph. The
|
|
14
14
|
* dist.test.ts bundle-hygiene invariants (/app reaches only
|
|
15
|
-
* `next/navigation`, /pages only `next/router.js`, /testing neither) depend
|
|
15
|
+
* `next/navigation.js`, /pages only `next/router.js`, /testing neither) depend
|
|
16
16
|
* on exactly this split — a context whose DEFAULT VALUE were the real
|
|
17
17
|
* adapter would break all three.
|
|
18
18
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paramour-js/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react-dom": "^19.2.0",
|
|
52
52
|
"typescript": "^6.0.3",
|
|
53
53
|
"zod": "^4.4.3",
|
|
54
|
-
"paramour": "0.
|
|
54
|
+
"paramour": "0.11.1"
|
|
55
55
|
},
|
|
56
56
|
"description": "Next.js integration for paramour: withTypedRoutes, App and Pages Router hooks, PageProps glue, and the codegen CLI.",
|
|
57
57
|
"author": "Jason Paff <jasonpaff@gmail.com>",
|
|
@@ -92,7 +92,7 @@ href(productRoute, { params: { id: 1 }, hash: "reviews" }); // "#reviews" append
|
|
|
92
92
|
href("/about", { hash: "team" }); // string form: registered STATIC paths only
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
`href` returns `Href` — a string subtype accepted by `next/link`, `router.push`, `redirect` unchanged. Required params/search make the options argument required; defaulted/optional/array keys are omittable. Serialization failures (bad value, empty segment, required catch-all given `[]`) throw `SerializeError` at link-build time. Lower-level pieces: `buildPath(route, params)`, `searchToString(config, input)`, `encodeStaticParams(route, params)` for `generateStaticParams`/`getStaticPaths`.
|
|
95
|
+
`href` returns `Href` — a string subtype accepted by `next/link`, `router.push`, `redirect` unchanged. Required params/search make the options argument required; defaulted/optional/array keys are omittable. Serialization failures (bad value, empty segment, required catch-all given `[]`) throw `SerializeError` at link-build time. A route defined with `trailingSlash: true` (match `next.config`'s `trailingSlash`) builds `/asset/?q=1` instead of `/asset?q=1`; the root stays `/` and the path literal never ends in `/`. Lower-level pieces: `buildPath(route, params)`, `searchToString(config, input)`, `encodeStaticParams(route, params)` for `generateStaticParams`/`getStaticPaths`.
|
|
96
96
|
|
|
97
97
|
## Client hooks
|
|
98
98
|
|
|
@@ -76,15 +76,15 @@ Precedence: CLI flags → config file → discovery. Unknown keys are rejected (
|
|
|
76
76
|
|
|
77
77
|
## Wire-format summary (numbered spec: docs/reference/wire-format on paramour.dev)
|
|
78
78
|
|
|
79
|
-
| Family | Scope
|
|
80
|
-
| ------ |
|
|
81
|
-
| S | Byte layer: `%20` never `+` (S2); absence = omitted key, `""` = `key=` (S3); never bare `?flag` (S4); deterministic declaration order (S5); hash only from `href`'s explicit option, verbatim (S10)
|
|
82
|
-
| P | Parse layer: duplicate values on a scalar codec are an error, catchable (P5); absent array key → `[]` (P6); unknown keys ignored and never validated (P8)
|
|
83
|
-
| SS | `rawSearch`: explicit wrapper only; schema sees every key on decode; encode is a raw pass-through, schema never runs; no elision/round-trip
|
|
84
|
-
| D | Codec grammar: `.catch()` recovers parse failures never absence (D2); presence governs absence and every declared key appears in decode output (D4); params take no presence modifiers (D5); a catch-all codec describes one element (D6); value defaults elide (D8)
|
|
85
|
-
| CV | `p.csv`: one comma-joined wire value; `""` ↔ `[]`; empty elements are parse failures; serialize rejects elements that are empty or contain commas
|
|
86
|
-
| R | Route segments: one segment per `[param]` (R1); catch-all elements encode independently, inner `/` → `%2F` (R2); optional catch-all elides, required `[]` is a `SerializeError` (R3); empty segment value is a `SerializeError` (R4); App Router param props arrive percent-ENCODED and core decodes them — Pages surfaces opt out via `percentDecode: false` (R5); no trailing slashes (R6) |
|
|
87
|
-
| PP | `p.array` typed elements by composition (PP1); `p.index` is 1-based on the wire, 0-based in memory (PP5)
|
|
79
|
+
| Family | Scope |
|
|
80
|
+
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| S | Byte layer: `%20` never `+` (S2); absence = omitted key, `""` = `key=` (S3); never bare `?flag` (S4); deterministic declaration order (S5); hash only from `href`'s explicit option, verbatim (S10) |
|
|
82
|
+
| P | Parse layer: duplicate values on a scalar codec are an error, catchable (P5); absent array key → `[]` (P6); unknown keys ignored and never validated (P8) |
|
|
83
|
+
| SS | `rawSearch`: explicit wrapper only; schema sees every key on decode; encode is a raw pass-through, schema never runs; no elision/round-trip |
|
|
84
|
+
| D | Codec grammar: `.catch()` recovers parse failures never absence (D2); presence governs absence and every declared key appears in decode output (D4); params take no presence modifiers (D5); a catch-all codec describes one element (D6); value defaults elide (D8) |
|
|
85
|
+
| CV | `p.csv`: one comma-joined wire value; `""` ↔ `[]`; empty elements are parse failures; serialize rejects elements that are empty or contain commas |
|
|
86
|
+
| R | Route segments: one segment per `[param]` (R1); catch-all elements encode independently, inner `/` → `%2F` (R2); optional catch-all elides, required `[]` is a `SerializeError` (R3); empty segment value is a `SerializeError` (R4); App Router param props arrive percent-ENCODED and core decodes them — Pages surfaces opt out via `percentDecode: false` (R5); no trailing slashes (R6), unless the route sets `trailingSlash: true`, which ends every non-root built path in `/` before `?`/`#` |
|
|
87
|
+
| PP | `p.array` typed elements by composition (PP1); `p.index` is 1-based on the wire, 0-based in memory (PP5) |
|
|
88
88
|
|
|
89
89
|
Facts agents trip on:
|
|
90
90
|
|