@paramour-js/next 0.11.0 → 0.11.2
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 +1 -2
- 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/navigation-adapter.d.ts +1 -1
- package/package.json +2 -2
- package/skills/paramour/SKILL.md +1 -1
- package/skills/paramour/references/authoring.md +1 -1
- package/skills/paramour/references/migration.md +1 -1
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
|
@@ -241,8 +241,7 @@ async function trailingSlashCheck(nextConfigPath, definitions) {
|
|
|
241
241
|
const setting = `trailingSlash: ${String(configured)}`;
|
|
242
242
|
// The routes come from the app's own paramour, which may predate the
|
|
243
243
|
// option and lack the member: missing reads as the R6 default.
|
|
244
|
-
const mismatched = definitions.filter((definition) => (definition.route["~trailingSlash"] ?? false) !==
|
|
245
|
-
configured);
|
|
244
|
+
const mismatched = definitions.filter((definition) => (definition.route["~trailingSlash"] ?? false) !== configured);
|
|
246
245
|
if (mismatched.length === 0) {
|
|
247
246
|
return {
|
|
248
247
|
label: `trailing slash: route definitions match ${name} (${setting})`,
|
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,
|
|
@@ -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.11.
|
|
3
|
+
"version": "0.11.2",
|
|
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.11.
|
|
54
|
+
"paramour": "0.11.2"
|
|
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>",
|
package/skills/paramour/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ Paramour is a type-safe routing companion for Next.js: each route is defined onc
|
|
|
12
12
|
1. Import only from package barrels: `paramour`, `@paramour-js/next`, `@paramour-js/next/app`, `@paramour-js/next/pages`, `@paramour-js/next/testing`. Never import `dist/` or deep source paths.
|
|
13
13
|
2. Codec modifier legality is type-state: illegal chains do not compile (the method's type becomes `never`) and throw at runtime for JS callers. The rules:
|
|
14
14
|
- `.optional()` and `.default()` apply only to a bare, unmodified single-value codec — at most ONE of the two, at most once. `.optional().default()`, `.default().optional()`, and any repeat are illegal.
|
|
15
|
-
- `.catch()` applies at most once and combines with either presence modifier in either order. It recovers parse failures of PRESENT wire values only — never absence. On an `.optional()` codec the fallback may be `undefined`, so a bad value reads as absent (`.optional().catch(undefined)`).
|
|
15
|
+
- `.catch()` applies at most once and combines with either presence modifier in either order, except that an `undefined` fallback needs `.optional()` first. It recovers parse failures of PRESENT wire values only — never absence. On an `.optional()` codec the fallback may be `undefined`, so a bad value reads as absent (`.optional().catch(undefined)`).
|
|
16
16
|
- `p.array(...)` codecs take no `.optional()`/`.default()` (an absent key and `[]` are the same wire state). `.catch()` is allowed.
|
|
17
17
|
- Codecs in a `params:` config take no presence modifiers at all (`.optional()`/`.default()` are illegal there); `.catch()` is allowed.
|
|
18
18
|
- `p.csv(element)`/`p.array(element)` elements must be bare unmodified scalars: no modifiers, no csv inside csv, no array-arity element.
|
|
@@ -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. 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`.
|
|
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 `/`. When every route needs it, re-export a typed wrapper (`export const defineAppRoute: typeof defineRoute = (path, config) => defineRoute(path, { trailingSlash: true, ...config });`) instead of repeating it. The string form `href("/about")` has no route object, so it never adds the slash: in a `trailingSlash` app, link through the route object. In tests, set `process.env.__NEXT_TRAILING_SLASH = "true"`, or `next/link` strips the slash from rendered hrefs. Lower-level pieces: `buildPath(route, params)`, `searchToString(config, input)`, `encodeStaticParams(route, params)` for `generateStaticParams`/`getStaticPaths`.
|
|
96
96
|
|
|
97
97
|
## Client hooks
|
|
98
98
|
|
|
@@ -76,7 +76,7 @@ export default async function ProductPage(props: RouteProps) {
|
|
|
76
76
|
- `sp.x ?? "default"` / `Number(sp.x ?? "1")` → codec with `.default(value)`. The decoded field is non-optional and the default value elides from built URLs.
|
|
77
77
|
- "May be absent, no fallback" (`typeof sp.x === "string" ? sp.x : undefined`) → `.optional()`. Decoded as `T | undefined`.
|
|
78
78
|
- Silent-coercion tolerance (old code shrugged off garbage, e.g. `Number(...)` producing `NaN` handled downstream) → add `.catch(fallback)` so a malformed PRESENT value falls back instead of failing the decode. `.catch()` never covers absence — combine with `.default()`/`.optional()` for that.
|
|
79
|
-
- Multi-value keys (`sp.tags` handled as `string | string[]`) → `p.array()` for repeated keys (`?tags=a&tags=b`) or `p.csv()` for one comma-joined key (`?tags=a,b`). Match whichever wire form the app already emits.
|
|
79
|
+
- Multi-value keys (`sp.tags` handled as `string | string[]`) → `p.array()` for repeated keys (`?tags=a&tags=b`) or `p.csv()` for one comma-joined key (`?tags=a,b`). Match whichever wire form the app already emits. Lists that nuqs's `parseAsArrayOf` already wrote (elements with `%2C`-escaped commas, bad elements dropped) → `nuqsArrayOf(element)` from `@paramour-js/nuqs`, not `p.csv()`.
|
|
80
80
|
- Enumerated strings → `p.enum(["a", "b"])`; numbers → `p.number()`; booleans (`sp.flag === "true"`) → `p.boolean()`; dates → `p.isoDate()` (YYYY-MM-DD) or `p.timestamp()` (ISO instant; emits UTC).
|
|
81
81
|
|
|
82
82
|
### Behavior change to decide explicitly
|