@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Migration: converting raw params/searchParams to paramour
|
|
2
|
+
|
|
3
|
+
## THE RULE
|
|
4
|
+
|
|
5
|
+
Migrate INCREMENTALLY: one route per pass. For each pass — define the route object, convert that route's files, run `paramour check` plus the project type check, commit, then move to the next route. NEVER rewrite all routes at once. Paramour routes and raw `params`/`searchParams` access coexist without conflict, so a partially migrated app is a normal, stable state. If asked to "migrate the app", plan a route-by-route sequence and execute it as separate verified passes.
|
|
6
|
+
|
|
7
|
+
Prerequisite: paramour is installed and `withTypedRoutes` is wired (otherwise do `references/setup.md` first — `npx paramour init` is safe to run on an existing project).
|
|
8
|
+
|
|
9
|
+
## Worked example: one App Router route
|
|
10
|
+
|
|
11
|
+
### Before
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// app/products/[id]/page.tsx
|
|
15
|
+
export default async function ProductPage({
|
|
16
|
+
params,
|
|
17
|
+
searchParams,
|
|
18
|
+
}: {
|
|
19
|
+
params: Promise<{ id: string }>;
|
|
20
|
+
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
|
21
|
+
}) {
|
|
22
|
+
const { id } = await params;
|
|
23
|
+
const sp = await searchParams;
|
|
24
|
+
const page = Number(sp.page ?? "1");
|
|
25
|
+
const sort = sp.sort === "desc" ? "desc" : "asc";
|
|
26
|
+
const q = typeof sp.q === "string" ? sp.q : undefined;
|
|
27
|
+
// ...
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### After
|
|
32
|
+
|
|
33
|
+
1. Define the route object, colocated with the page:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// app/products/[id]/route.def.ts
|
|
37
|
+
import { defineAppRoute, p } from "paramour";
|
|
38
|
+
|
|
39
|
+
export const productRoute = defineAppRoute("/products/[id]", {
|
|
40
|
+
params: { id: p.integer() },
|
|
41
|
+
search: {
|
|
42
|
+
page: p.integer().default(1),
|
|
43
|
+
q: p.string().optional(),
|
|
44
|
+
sort: p.enum(["asc", "desc"]).default("asc"),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. Convert the page (and `generateMetadata`, which takes the same props):
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// app/products/[id]/page.tsx
|
|
53
|
+
import type { RouteProps } from "paramour";
|
|
54
|
+
import { productRoute } from "./route.def";
|
|
55
|
+
|
|
56
|
+
export default async function ProductPage(props: RouteProps) {
|
|
57
|
+
const { params, search } = await productRoute.parse(props);
|
|
58
|
+
// params.id: number — search.page: number — search.sort: "asc" | "desc"
|
|
59
|
+
// search.q: string | undefined
|
|
60
|
+
// ...
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
3. Verify: `paramour check` (the path already existed, so usually no artifact change), then `tsc --noEmit`. Commit.
|
|
65
|
+
|
|
66
|
+
## Mapping decisions
|
|
67
|
+
|
|
68
|
+
### Dynamic segments → `params:` codecs
|
|
69
|
+
|
|
70
|
+
- `[id]` numeric in practice → `p.integer()`; otherwise keep `p.string()` (behavior-preserving — when unsure, `p.string()` first, tighten later).
|
|
71
|
+
- `[...slug]` / `[[...slug]]` → the codec describes ONE segment element (usually `p.string()`); the array comes from the path shape. Decoded type is `Out[]`; an absent optional catch-all decodes to `[]`.
|
|
72
|
+
- Presence modifiers are illegal in `params:` — optionality lives in the path grammar (`[[...slug]]`), not the codec.
|
|
73
|
+
|
|
74
|
+
### searchParams reads → `search:` codecs
|
|
75
|
+
|
|
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
|
+
- "May be absent, no fallback" (`typeof sp.x === "string" ? sp.x : undefined`) → `.optional()`. Decoded as `T | undefined`.
|
|
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.
|
|
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 UTC).
|
|
81
|
+
|
|
82
|
+
### Behavior change to decide explicitly
|
|
83
|
+
|
|
84
|
+
Raw code silently tolerated malformed URLs; `route.parse` THROWS `ParamsDecodeError`/`SearchDecodeError` on them. Pick one per route:
|
|
85
|
+
|
|
86
|
+
- `parse` + a colocated `error.tsx` boundary (malformed URL renders the boundary), or
|
|
87
|
+
- `safeParse` and branch on the result: `if (result.status === "error") notFound();` (or render a fallback), or
|
|
88
|
+
- keep `parse` but add `.catch()` on the keys that used to be silently coerced.
|
|
89
|
+
|
|
90
|
+
### Client components in the route
|
|
91
|
+
|
|
92
|
+
Replace `useParams()` / `useSearchParams()` reads with the typed hooks:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
"use client";
|
|
96
|
+
import { useRouteParams, useSearch } from "@paramour-js/next/app";
|
|
97
|
+
import { productRoute } from "./route.def";
|
|
98
|
+
|
|
99
|
+
export function Panel() {
|
|
100
|
+
const params = useRouteParams(productRoute); // SafeResult
|
|
101
|
+
const search = useSearch(productRoute);
|
|
102
|
+
if (params.status === "error" || search.status === "error") return null;
|
|
103
|
+
return <span>{search.data.q ?? ""}</span>;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`useRouteParamsOrThrow`/`useSearchOrThrow` throw to the nearest error boundary instead of returning the error arm. Pages Router components use `@paramour-js/next/pages` (`useRouteParams`/`useSearch`, which add a `pending` status arm; server side, `route.parseContext(ctx)` in `getServerSideProps`).
|
|
108
|
+
|
|
109
|
+
### Links into the migrated route
|
|
110
|
+
|
|
111
|
+
Replace hand-built strings with `href` wherever the route is linked:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// before
|
|
115
|
+
<Link href={`/products/${id}?page=2`}>
|
|
116
|
+
// after
|
|
117
|
+
<Link href={href(productRoute, { params: { id }, search: { page: 2 } })}>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`href(...)` returns a string subtype — `next/link`, `router.push`, and `redirect` accept it unchanged. Convert the links you find; ones you miss keep working (they are just untyped strings) and can be converted in later passes.
|
|
121
|
+
|
|
122
|
+
### Static generation
|
|
123
|
+
|
|
124
|
+
`generateStaticParams` → build entries with `encodeStaticParams(route, { id: 42 })` from `paramour`, which returns the per-param wire-string record Next expects.
|
|
125
|
+
|
|
126
|
+
## What NOT to touch in a pass
|
|
127
|
+
|
|
128
|
+
- Other routes' raw `params`/`searchParams` access — leave it alone until that route's own pass.
|
|
129
|
+
- Shared layouts/components serving unmigrated routes.
|
|
130
|
+
- Unrelated search keys the page never read — do not "complete" the search config speculatively; declare only what the route actually uses (undeclared keys are ignored by decode and never emitted by encode).
|
|
131
|
+
- Wire formats: do not change a param's URL spelling (e.g. csv ↔ repeated keys) while migrating; preserve existing URLs, change formats in a separate deliberate commit.
|
|
132
|
+
|
|
133
|
+
## Per-route checklist
|
|
134
|
+
|
|
135
|
+
1. Read every file of the route (page, layout if it reads params, generateMetadata, client components, links into it) and inventory each `params`/`searchParams` key and its fallback semantics.
|
|
136
|
+
2. Create `route.def.ts` with `defineAppRoute` (or `definePagesRoute`): `params:` codec per dynamic segment, `search:` codec per read key with `.default()`/`.optional()`/`.catch()` matching the old semantics.
|
|
137
|
+
3. Convert server access: props typed `RouteProps`; `route.parse(props)` / `parseParams` / `parseSearch` (or the `safeParse*` forms). Decide the malformed-URL story (error boundary vs safeParse vs `.catch()`).
|
|
138
|
+
4. Convert the route's client components to `@paramour-js/next/app` (or `/pages`) hooks.
|
|
139
|
+
5. Convert links into the route to `href(route, ...)`.
|
|
140
|
+
6. Run `paramour check` (regenerate + commit the artifact if routes were added/renamed), then the type check and tests.
|
|
141
|
+
7. Commit. Next route.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Reference: exports, CLI, config, wire format
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
The tables and the "Key types" list in this file are drift-checked against
|
|
5
|
+
the package source by packages/next/test/skill-drift.test.ts. Keep each
|
|
6
|
+
cited export a single backtick token (`name` or `name(...)`) in the listed
|
|
7
|
+
column, flags spelled `--flag` in the Flags column, and wire-rule IDs in
|
|
8
|
+
their published S/P/D/CV/R/PP spelling.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
## `paramour` barrel exports
|
|
12
|
+
|
|
13
|
+
Runtime values:
|
|
14
|
+
|
|
15
|
+
| Export | Purpose |
|
|
16
|
+
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `p` | Codec builders: `string, integer, number, boolean, enum, isoDate, timestamp, json, csv, array, index, custom` |
|
|
18
|
+
| `defineAppRoute(path, config)` | Define an App Router route object (`config: { params?, search? }`) |
|
|
19
|
+
| `definePagesRoute(path, config)` | Define a Pages Router route object |
|
|
20
|
+
| `href(routeOrStaticPath, options?)` | Build a typed link (`{ params?, search?, hash? }`); returns `Href` (string subtype) |
|
|
21
|
+
| `buildPath(route, params)` | Path portion only (`/a/b`), no query |
|
|
22
|
+
| `encodeParams(route, params)` | Encoded path segments as `string[]` |
|
|
23
|
+
| `encodeStaticParams(route, params)` | Per-param wire-string record for `generateStaticParams` / `getStaticPaths` |
|
|
24
|
+
| `decodeParams(route, source, opts?)` | Sync params decode; throws `ParamsDecodeError`; `opts: { percentDecode?: boolean }` (default true — App Router) |
|
|
25
|
+
| `decodeSearch(config, source, routePath?)` | Sync search decode; throws `SearchDecodeError`; unknown keys ignored |
|
|
26
|
+
| `safeDecodeParams` / `safeDecodeSearch` | `SafeResult`-returning twins of the two decoders |
|
|
27
|
+
| `encodeSearch(config, input)` | Decoded values → ordered wire pairs `[string, string][]` (default elision applied) |
|
|
28
|
+
| `buildSearchString(pairs)` | Pairs → `?…` string (`%20`, never `+`) |
|
|
29
|
+
| `searchToString(config, input)` | `encodeSearch` + `buildSearchString` |
|
|
30
|
+
| `serializeValue(codec, label, value)` | One value through a codec's serializer, string contract enforced |
|
|
31
|
+
| `rawSearch(schema)` / `isRawSearch(config)` | Whole-object search escape hatch and its discriminant |
|
|
32
|
+
| `standardSearchSchema(route)` | Export a route's search config as a Standard Schema (tRPC input, TanStack `validateSearch`) |
|
|
33
|
+
| `describeCodec(codec)` / `describeRoute(route)` / `formatCodecDescription(desc, style)` | Reflection over codec/route metadata (powers `paramour list`) |
|
|
34
|
+
| `ParamourError, ParseError, SerializeError, ParamsDecodeError, SearchDecodeError, SearchSourceError` | Error classes (brand-hardened `instanceof`) |
|
|
35
|
+
|
|
36
|
+
Key types: `Codec`, `AnyCodec`, `OutputOf`, `ParamCodec`, `Presence`, `PresenceOf`, `Arity`; `AppRoute`, `PagesRoute`, `Route`, `AnyRoute`, `AnyAppRoute`, `AnyPagesRoute`, `RouterKind`, `PagesContext`; `RouteProps`, `ParamsProps`, `SearchProps` (+ `*Input` sync-accepting forms) — annotate page/layout props with these; `InferRouteParams`, `SearchOutputOf`, `InferSearchInput`, `InferSearchOutput`, `InferStaticParams`, `InferHrefInput`, `HrefArgs`, `Href`, `StaticHrefOptions`; `SafeResult`, `RouteDecodeError`, `Issue`, `IssueReason`; `ParamsConfig`, `SearchConfig`, `ParamsSource`, `SearchSource`, `RawSearch`, `StandardSearchSchema`; `ParamourRegister` + `Registered*RoutePaths` (codegen augmentation targets); `CodecDescription`, `RouteDescription`, `ParamDescription`, `SearchDescription`, `CodecDefaultDescription`, `CodecFormatStyle`; `DecodeParamsOptions`.
|
|
37
|
+
|
|
38
|
+
## `@paramour-js/next` exports
|
|
39
|
+
|
|
40
|
+
| Entry point | Exports |
|
|
41
|
+
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `@paramour-js/next` | `withTypedRoutes(config, options?)` (`options: { outFile?, strict? }`), `RouteCollisionError`, types `WithTypedRoutesOptions`, `ParamourConfig` |
|
|
43
|
+
| `@paramour-js/next/app` | `useRouteParams`, `useRouteParamsOrThrow`, `useSearch`, `useSearchOrThrow` (all `(route, options?)` with `options: { select, equality?: "shallow" }`), type `SelectOptions` |
|
|
44
|
+
| `@paramour-js/next/pages` | `useRouteParams`, `useSearch` (return `RouterResult` = `SafeResult` + `{ status: "pending" }`), types `RouterResult`, `SelectOptions` |
|
|
45
|
+
| `@paramour-js/next/testing` | `ParamourTestingProvider`, `withParamourTesting(options?)`, type `ParamourTestingOptions` (`isReady, mounted, onReplace, params, pathname, search`) |
|
|
46
|
+
| `@paramour-js/next/devtools-seam` | Types-only seam contract consumed by `@paramour-js/devtools`; not needed in app code |
|
|
47
|
+
|
|
48
|
+
## CLI (`paramour <command>`, bin shipped by `@paramour-js/next`)
|
|
49
|
+
|
|
50
|
+
Exit-code contract for every command: `0` success; `1` ONLY a failed verification (`check` drift, `doctor` fail, `skills --check` missing/stale); `2` usage/config/operational errors.
|
|
51
|
+
|
|
52
|
+
| Command | Purpose | Flags |
|
|
53
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
54
|
+
| `generate` | Write `paramour-env.d.ts` from route dirs | `--app-dir <dir>`, `--pages-dir <dir>`, `--out-file <file>`, `--page-extensions <list>`, `--check`, `--watch` |
|
|
55
|
+
| `check` | Verify the artifact is current; exit 1 on drift or missing; never writes | `--app-dir`, `--pages-dir`, `--out-file`, `--page-extensions` |
|
|
56
|
+
| `init` | Set up paramour: scaffold config, wrap next.config, add script, first generate, agent skills + AGENTS.md snippet | `--dry-run`, `--force`, `--no-config`, `--no-generate`, `--no-script`, `--no-wrap`, `--no-skills`, `--no-agents-md` |
|
|
57
|
+
| `list` | Print every filesystem route with its params/search shape (evaluates route-definition modules) | `--json`, `--app-dir`, `--pages-dir`, `--page-extensions` |
|
|
58
|
+
| `doctor` | Diagnose setup: config validity, artifact freshness, next.config wrapping, versions, tsconfig | `--json`; exit 1 on any failing check, warnings exit 0 |
|
|
59
|
+
| `skills` | Install/sync this agent skill into detected tool dirs (`.claude/`, `.cursor/`, …) | `--check` (verify only; exit 1 on missing/stale), `--dry-run`, `--force` (overwrite user-edited files), `--json`, `--tool <t>` (agents, claude, codex, cursor; repeatable or comma-separated, overrides detection) |
|
|
60
|
+
|
|
61
|
+
All commands accept `--help`/`-h` and run against `process.cwd()` as the project root.
|
|
62
|
+
|
|
63
|
+
## `paramour.config.{ts,mjs,json}` (project root; first match wins; all fields optional)
|
|
64
|
+
|
|
65
|
+
Precedence: CLI flags → config file → discovery. Unknown keys are rejected (exit 2).
|
|
66
|
+
|
|
67
|
+
| Field | Default | Meaning |
|
|
68
|
+
| ---------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
69
|
+
| `appDir` | discovered `app/`/`src/app` | App directory, relative to project root |
|
|
70
|
+
| `pagesDir` | discovered `pages/`… | Pages directory |
|
|
71
|
+
| `outFile` | `paramour-env.d.ts` | Artifact path (monorepo escape hatch); also settable on `withTypedRoutes` |
|
|
72
|
+
| `pageExtensions` | `["tsx","ts","jsx","js"]` | No leading dots |
|
|
73
|
+
| `routeFiles` | automatic content scan | Globs of modules exporting route definitions — used by `list`/`doctor` only; generation never reads it |
|
|
74
|
+
|
|
75
|
+
`.ts`/`.mjs` files default-export the object (`export default {...} satisfies ParamourConfig`).
|
|
76
|
+
|
|
77
|
+
## Wire-format summary (numbered spec: docs/reference/wire-format on paramour.dev)
|
|
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) |
|
|
88
|
+
|
|
89
|
+
Facts agents trip on:
|
|
90
|
+
|
|
91
|
+
- Booleans serialize as exactly `true`/`false`; anything else fails to parse.
|
|
92
|
+
- Dates: `p.isoDate` is `YYYY-MM-DD`; `p.timestamp` is full ISO UTC (`Z` only, offsets rejected); both reject impossible calendar dates.
|
|
93
|
+
- Integers reject `1e3`, hex, whitespace, and unsafe-range values.
|
|
94
|
+
- Arrays: `p.array` repeats the key (`?t=a&t=b`); `p.csv` packs one key (`?t=a,b`). Same in-memory `string[]`, two deliberate wire spellings — do not swap them casually.
|
|
95
|
+
- Value-form `.default()` elides: building a URL with the default value emits nothing for that key; decoding the bare URL restores the default. Factory defaults never elide.
|
|
96
|
+
- An href with no emitted pairs has no `?` at all; a fully elided optional catch-all leaves the bare base path (`/docs`, never `/docs/`).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Setup: adding paramour to a project (greenfield)
|
|
2
|
+
|
|
3
|
+
## 1. Install both packages
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install paramour @paramour-js/next
|
|
7
|
+
# or
|
|
8
|
+
pnpm add paramour @paramour-js/next
|
|
9
|
+
# or
|
|
10
|
+
yarn add paramour @paramour-js/next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requirements: `next >= 15`, `react >= 18.2`, Node `>= 22.13` for the CLI. ESM-only packages.
|
|
14
|
+
|
|
15
|
+
## 2. Run `npx paramour init`
|
|
16
|
+
|
|
17
|
+
`init` is non-interactive and idempotent: it runs straight through with defaults and prints one status line per step. Steps:
|
|
18
|
+
|
|
19
|
+
1. Scaffold `paramour.config.ts` at the project root (all fields commented-out defaults — deleting the file changes nothing). Skipped if any `paramour.config.{ts,mjs,json}` already exists; `--force` overwrites/replaces it.
|
|
20
|
+
2. Wrap `next.config.*` with `withTypedRoutes` via an AST transform. If no next.config exists or the transform cannot apply safely, it prints a manual snippet to apply yourself — that is still a SUCCESS outcome (exit 0), not a failure.
|
|
21
|
+
3. Add `"paramour": "paramour generate"` to `package.json` scripts.
|
|
22
|
+
4. Run the first generate (skipped with a warning if no `app/` or `pages/` directory exists yet).
|
|
23
|
+
5. Install the paramour agent skill files into detected agent tool directories (`.claude/`, `.cursor/`, etc.); skip with `--no-skills`.
|
|
24
|
+
6. Append a marker-delimited (`<!-- paramour:start -->` … `<!-- paramour:end -->`) paramour section to an existing `AGENTS.md` (or, failing that, `CLAUDE.md`) pointing agents at the installed skill and the verify loop. Never creates the file; re-runs refresh the section in place; skip with `--no-agents-md`.
|
|
25
|
+
|
|
26
|
+
Then a `setup:` detect-and-verify summary (route dirs found, both packages declared, tsconfig `include` covers the artifact) — these are warn-level and never change the exit code — and a final reminder to commit the artifact.
|
|
27
|
+
|
|
28
|
+
Output marks to recognize:
|
|
29
|
+
|
|
30
|
+
- `✔ created paramour.config.ts` / `✔ wrapped next.config.ts with withTypedRoutes` / `✔ added "paramour" script to package.json` / `✔ wrote paramour-env.d.ts (N routes)` — step performed.
|
|
31
|
+
- `• ... already exists — skipped` / `• ... already wraps withTypedRoutes — skipped` / `• ... already up to date — skipped` — idempotent skip; fine.
|
|
32
|
+
- `→ could not transform ... — apply this yourself:` followed by an indented snippet — apply the printed snippet manually, then continue; exit is still 0.
|
|
33
|
+
- `⚠ no route directory yet — skipped generate` — create `app/` or `pages/`, then run `paramour generate`.
|
|
34
|
+
|
|
35
|
+
Flags: `--dry-run` (report every step, write nothing), `--force` (overwrite an existing paramour.config), `--no-config`, `--no-generate`, `--no-script`, `--no-wrap`, `--no-skills`, `--no-agents-md`, `--help`/`-h`.
|
|
36
|
+
|
|
37
|
+
## 3. Manual equivalent (when init cannot be used)
|
|
38
|
+
|
|
39
|
+
Scaffold `paramour.config.ts` (optional — only needed to override discovery; every value shown is the default):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import type { ParamourConfig } from "@paramour-js/next";
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
// appDir: "app",
|
|
46
|
+
// outFile: "paramour-env.d.ts",
|
|
47
|
+
// pageExtensions: ["tsx", "ts", "jsx", "js"],
|
|
48
|
+
// pagesDir: "pages",
|
|
49
|
+
// routeFiles: ["src/routes/**/*.ts"], // pin `paramour list`'s definition scan
|
|
50
|
+
} satisfies ParamourConfig;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Wrap next.config:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { withTypedRoutes } from "@paramour-js/next";
|
|
57
|
+
import type { NextConfig } from "next";
|
|
58
|
+
|
|
59
|
+
const nextConfig: NextConfig = {};
|
|
60
|
+
|
|
61
|
+
export default withTypedRoutes(nextConfig);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`withTypedRoutes(config, options?)` regenerates the artifact once per production build (drift warns; `{ strict: true }` fails the build on drift instead) and runs a debounced regeneration watcher during `next dev`. `{ outFile: "..." }` relocates the artifact (monorepo escape hatch). Generation is never load-bearing: a missing route dir or an incidental failure warns and continues with stale types — the two exceptions that throw are an app↔pages route collision and a populated-but-ignored route dir.
|
|
65
|
+
|
|
66
|
+
Add the script to `package.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "scripts": { "paramour": "paramour generate" } }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 4. Define a first route
|
|
73
|
+
|
|
74
|
+
Colocate a route definition next to its page (any module works; `route.def.ts` beside `page.tsx` is the common pattern):
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// app/product/[id]/route.def.ts
|
|
78
|
+
import { defineAppRoute, p } from "paramour";
|
|
79
|
+
|
|
80
|
+
export const productRoute = defineAppRoute("/product/[id]", {
|
|
81
|
+
params: { id: p.integer() },
|
|
82
|
+
search: {
|
|
83
|
+
page: p.integer().default(1),
|
|
84
|
+
q: p.string().optional(),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use it in the page. Annotate props with paramour's `RouteProps` (Next's promised `params`/`searchParams` are structurally assignable to it):
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// app/product/[id]/page.tsx
|
|
93
|
+
import type { RouteProps } from "paramour";
|
|
94
|
+
import { productRoute } from "./route.def";
|
|
95
|
+
|
|
96
|
+
export default async function ProductPage(props: RouteProps) {
|
|
97
|
+
// Throws ParamsDecodeError/SearchDecodeError on a malformed URL — pair
|
|
98
|
+
// with an error.tsx boundary, or use safeParse for a status-discriminated
|
|
99
|
+
// result instead of a throw.
|
|
100
|
+
const { params, search } = await productRoute.parse(props);
|
|
101
|
+
return (
|
|
102
|
+
<h1>
|
|
103
|
+
Product #{params.id} (page {search.page})
|
|
104
|
+
</h1>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Build links to it from anywhere:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { href } from "paramour";
|
|
113
|
+
import { productRoute } from "@/app/product/[id]/route.def";
|
|
114
|
+
|
|
115
|
+
const link = href(productRoute, { params: { id: 42 }, search: { q: "hi" } });
|
|
116
|
+
// "/product/42?q=hi" — page=1 elides because it equals the default
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 5. Generate, verify, commit
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx paramour generate # writes paramour-env.d.ts
|
|
123
|
+
npx paramour check # exit 0 = artifact current
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Commit `paramour-env.d.ts` with the route change. The artifact is a pure `.d.ts` module augmentation (`declare module "paramour" { interface ParamourRegister { ... } }`) that turns `defineAppRoute`/`definePagesRoute` path literals and `href("/static/path")` strings into filesystem-verified unions. Ensure the project tsconfig `include` covers it (Next's default `**/*.ts` include does).
|
|
127
|
+
|
|
128
|
+
## Exit codes (all CLI commands)
|
|
129
|
+
|
|
130
|
+
- `0` — success, including init's printed manual-fallback snippets and idempotent skips.
|
|
131
|
+
- `1` — ONLY "the thing you asked me to verify is not true": `check` (or `generate --check`) drift or missing artifact, `doctor` with a failing check, `skills --check` with missing/stale skill files.
|
|
132
|
+
- `2` — usage/config/operational errors: unknown command or flag, invalid `paramour.config` (unknown key, dotted `pageExtensions` entry), no `package.json` for init, no route directory for generate, app↔pages route collisions.
|
|
133
|
+
|
|
134
|
+
## Common failure modes
|
|
135
|
+
|
|
136
|
+
- `paramour check` exit 1 saying `out of date` or `is missing`, with a route diff: run `paramour generate` and commit. If it says `content differs from generator output` with no route diff, someone hand-edited the artifact — regenerate, never edit.
|
|
137
|
+
- Route collision (same path served by both `app/` and `pages/`): exit 2 from the CLI and a thrown error from `withTypedRoutes` — remove one of the duplicates; Next itself cannot build that state either.
|
|
138
|
+
- `no route directory` from `paramour generate`: run from the project root (the directory holding `app/`/`pages/` or `src/app/`/`src/pages/`), or set `appDir`/`pagesDir` in `paramour.config.ts`.
|
|
139
|
+
- Registered-path types not narrowing (any string accepted in `defineAppRoute`): the artifact is missing or not covered by tsconfig `include` — run `paramour doctor`.
|