create-pracht 0.3.0 → 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.
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: scaffold-tests
3
+ version: 1.1.0
4
+ description: |
5
+ Scaffold Vitest unit/integration tests for pracht routes, loaders, and
6
+ middleware. Asks the user once whether to use vitest browser mode with
7
+ `vitest-browser-preact` (real DOM, real events) or classic JSDOM-based
8
+ tests with `@testing-library/preact`. Wires `vitest.config.ts`, mocks
9
+ `LoaderArgs`, and emits ready-to-run files.
10
+ Use when asked to "scaffold tests", "set up Vitest", "add unit tests",
11
+ "test this loader", or "test this route".
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Write
16
+ - Edit
17
+ - Grep
18
+ - Glob
19
+ - AskUserQuestion
20
+ ---
21
+
22
+ # Pracht Scaffold Tests
23
+
24
+ Generate Vitest tests aligned with pracht's testing recipe
25
+ (`examples/docs/src/routes/docs/recipes-testing.md`). Loaders and API handlers
26
+ are plain async functions — they test directly with no framework bootstrap.
27
+ Component tests need a renderer; the user picks the flavor.
28
+
29
+ ## Step 1: Pick the rendering strategy
30
+
31
+ Use `AskUserQuestion` to choose between:
32
+
33
+ 1. **Browser mode** — `vitest` with `@vitest/browser` and
34
+ `vitest-browser-preact`.
35
+ - Pros: real browser, real events, fewer hydration false positives,
36
+ screenshots, works for SPA-mode interaction tests.
37
+ - Cons: slower, heavier setup, requires a browser binary on CI.
38
+ 2. **JSDOM** — `vitest` with `@testing-library/preact`.
39
+ - Pros: fast, lightweight, runs anywhere.
40
+ - Cons: JSDOM lacks layout, certain DOM APIs; brittle for complex UIs.
41
+
42
+ If the project already has one configured, default to it and confirm.
43
+
44
+ ## Step 2: Install dependencies
45
+
46
+ Detect the package manager from the lockfile.
47
+
48
+ Common (both modes):
49
+
50
+ ```bash
51
+ pnpm add -D vitest @types/node
52
+ ```
53
+
54
+ Component-test extras — `@preact/preset-vite` must be an explicit dev
55
+ dependency: the configs in Step 3 import it, and a transitive-only copy
56
+ fails under pnpm's strict `node_modules`.
57
+
58
+ **Browser mode**:
59
+
60
+ ```bash
61
+ pnpm add -D @vitest/browser playwright vitest-browser-preact @preact/preset-vite
62
+ ```
63
+
64
+ **JSDOM mode**:
65
+
66
+ ```bash
67
+ pnpm add -D jsdom @testing-library/preact @testing-library/jest-dom @preact/preset-vite
68
+ ```
69
+
70
+ If the project only needs loader/middleware tests (no component rendering),
71
+ skip the extras entirely and use the plugin-less config in Step 3.
72
+
73
+ ## Step 3: Wire `vitest.config.ts`
74
+
75
+ **Browser mode**:
76
+
77
+ ```ts
78
+ import { defineConfig } from "vitest/config";
79
+ import preact from "@preact/preset-vite";
80
+
81
+ export default defineConfig({
82
+ plugins: [preact()],
83
+ test: {
84
+ browser: {
85
+ enabled: true,
86
+ provider: "playwright",
87
+ instances: [{ browser: "chromium" }],
88
+ },
89
+ },
90
+ });
91
+ ```
92
+
93
+ **JSDOM mode**:
94
+
95
+ ```ts
96
+ import { defineConfig } from "vitest/config";
97
+ import preact from "@preact/preset-vite";
98
+
99
+ export default defineConfig({
100
+ plugins: [preact()],
101
+ test: {
102
+ environment: "jsdom",
103
+ setupFiles: ["./test/setup.ts"],
104
+ },
105
+ });
106
+ ```
107
+
108
+ `test/setup.ts` (JSDOM only):
109
+
110
+ ```ts
111
+ import "@testing-library/jest-dom/vitest";
112
+ ```
113
+
114
+ **Loader/middleware-only mode** (no components) — matches the minimal config
115
+ in `recipes-testing.md`; no preset, no extra deps:
116
+
117
+ ```ts
118
+ import { defineConfig } from "vitest/config";
119
+
120
+ export default defineConfig({
121
+ test: {
122
+ // Exclude E2E tests (run those with Playwright)
123
+ exclude: ["e2e/**", "node_modules/**"],
124
+ },
125
+ });
126
+ ```
127
+
128
+ If `vitest.config.ts` already exists, merge — never clobber.
129
+
130
+ ## Step 4: Generate the tests
131
+
132
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
133
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
134
+ `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
135
+ config with the pracht plugin registered.
136
+
137
+ This skill covers **loaders, middleware, and components**. For API handlers
138
+ (`src/api/**`), delegate to `/pracht-test-api` — it owns handler enumeration and
139
+ per-method test generation; don't duplicate a weaker version here.
140
+
141
+ Use `pracht inspect routes --json` to find targets. Ask the user which subset
142
+ to scaffold, or pass paths via `$ARGUMENTS`.
143
+
144
+ ### Loader test template
145
+
146
+ ```ts
147
+ import { describe, it, expect } from "vitest";
148
+ import { loader } from "./<route-file>";
149
+
150
+ function args(url: string, init?: RequestInit) {
151
+ const request = new Request(url, init);
152
+ return {
153
+ request,
154
+ params: {} as Record<string, string>,
155
+ context: {} as never,
156
+ url: new URL(request.url),
157
+ signal: AbortSignal.timeout(5000),
158
+ route: {} as never,
159
+ };
160
+ }
161
+
162
+ describe("<route> loader", () => {
163
+ it("returns the expected shape", async () => {
164
+ const data = await loader(args("http://localhost/<path>"));
165
+ expect(data).toBeDefined();
166
+ });
167
+ });
168
+ ```
169
+
170
+ ### Middleware test template
171
+
172
+ ```ts
173
+ import { describe, it, expect } from "vitest";
174
+ import { middleware } from "./<middleware-file>";
175
+
176
+ describe("<name> middleware", () => {
177
+ const ok = new Response("ok", { status: 200 });
178
+ const next = async () => ok;
179
+
180
+ it("redirects unauthenticated requests", async () => {
181
+ const request = new Request("http://localhost/dashboard");
182
+ const response = await middleware(
183
+ {
184
+ request,
185
+ params: {},
186
+ context: {} as never,
187
+ url: new URL(request.url),
188
+ signal: AbortSignal.timeout(5000),
189
+ route: {} as never,
190
+ },
191
+ next,
192
+ );
193
+ expect(response.status).toBe(302);
194
+ expect(response.headers.get("location")).toMatch(/^\/login/);
195
+ });
196
+
197
+ it("calls through when authenticated", async () => {
198
+ const request = new Request("http://localhost/dashboard", {
199
+ headers: { cookie: "session=valid" },
200
+ });
201
+ const response = await middleware(
202
+ {
203
+ request,
204
+ params: {},
205
+ context: {} as never,
206
+ url: new URL(request.url),
207
+ signal: AbortSignal.timeout(5000),
208
+ route: {} as never,
209
+ },
210
+ next,
211
+ );
212
+ expect(response).toBe(ok);
213
+ });
214
+ });
215
+ ```
216
+
217
+ ### Component test template (browser mode)
218
+
219
+ ```tsx
220
+ import { describe, it, expect } from "vitest";
221
+ import { render } from "vitest-browser-preact";
222
+ import { Component } from "./<route-file>";
223
+
224
+ describe("<route> component", () => {
225
+ it("renders the heading", async () => {
226
+ const screen = render(<Component data={{ /* mock loader data */ }} params={{}} />);
227
+ await expect.element(screen.getByRole("heading")).toBeVisible();
228
+ });
229
+ });
230
+ ```
231
+
232
+ ### Component test template (JSDOM)
233
+
234
+ ```tsx
235
+ import { describe, it, expect } from "vitest";
236
+ import { render, screen } from "@testing-library/preact";
237
+ import { Component } from "./<route-file>";
238
+
239
+ describe("<route> component", () => {
240
+ it("renders the heading", () => {
241
+ render(<Component data={{ /* mock loader data */ }} params={{}} />);
242
+ expect(screen.getByRole("heading")).toBeInTheDocument();
243
+ });
244
+ });
245
+ ```
246
+
247
+ ## Step 5: Wire `package.json`
248
+
249
+ Add (or merge) scripts:
250
+
251
+ ```json
252
+ {
253
+ "scripts": {
254
+ "test": "vitest run",
255
+ "test:watch": "vitest"
256
+ }
257
+ }
258
+ ```
259
+
260
+ If `pnpm test` already exists, do not overwrite.
261
+
262
+ ## Step 6: Verify
263
+
264
+ ```bash
265
+ pnpm test
266
+ pracht verify --json
267
+ ```
268
+
269
+ If anything fails on first run, report the failure and the fix. Do not commit
270
+ broken scaffolding.
271
+
272
+ ## Rules
273
+
274
+ 1. Ask the rendering-strategy question once per project; persist by
275
+ inspecting `vitest.config.ts` on subsequent runs.
276
+ 2. Only test exports that exist — read the route file before generating.
277
+ 3. Use the recipe's `args()` helper shape for `BaseRouteArgs`/`LoaderArgs`
278
+ construction.
279
+ 4. For routes with `getStaticPaths`, scaffold a separate test that calls it.
280
+ 5. Generated tests should pass on first run with a placeholder assertion;
281
+ the user fills in real expectations.
282
+
283
+ $ARGUMENTS
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: tune-render-mode
3
+ version: 1.1.0
4
+ description: |
5
+ Recommend the right pracht render mode (ssg, isg, ssr, spa) for each route
6
+ based on what its loader actually does. Most apps pick a mode once and never
7
+ revisit; this skill surfaces routes that are mis-tuned.
8
+ Use when asked to "tune render modes", "make my site faster", "should this
9
+ route be SSG", "audit render modes", or "review SSG/ISG/SSR choices".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Edit
14
+ - Grep
15
+ - Glob
16
+ ---
17
+
18
+ # Pracht Tune Render Mode
19
+
20
+ Walk every route, read its loader, and recommend the cheapest render mode that
21
+ still satisfies the route's data dependencies.
22
+
23
+ This is a **tune** skill, not a report-only audit: it ends by applying edits.
24
+ The contract is propose-then-apply — produce the recommendation table and the
25
+ exact diffs first, then apply them **only after the user explicitly confirms**
26
+ (per route or as a batch). Never edit before that confirmation.
27
+
28
+ ## Decision Tree
29
+
30
+ For each route:
31
+
32
+ 1. **No `loader`, no `getStaticPaths`, no per-request data** → **`ssg`**
33
+ - Pure UI. Build once, serve from CDN. Highest performance.
34
+
35
+ 2. **Loader reads only build-time-stable data** (filesystem, static config,
36
+ typed CMS export, no `request`/`params`/`context.env` use) → **`ssg`** or
37
+ **`isg`**
38
+ - Pick `isg` with `timeRevalidate(seconds)` if the source can change between
39
+ deploys (CMS, pricing pages, public catalog).
40
+ - Pick `ssg` if the source only changes when you redeploy.
41
+
42
+ 3. **Loader reads `params` to fetch data, but not `request`/cookies** →
43
+ **`ssg`** with `getStaticPaths`, or **`isg`** if the universe of params is
44
+ open-ended (millions of slugs).
45
+
46
+ 4. **Loader reads `request`, but the data is shareable** (`request-static`:
47
+ request used only for cache keys like `Accept-Language`, never for
48
+ identity) → **`ssr`** by default; **`isg`** is possible on adapters whose
49
+ cache can key on the varying dimension (e.g. a normalized cache key at a
50
+ Cloudflare gateway). If the variant fan-out is unbounded or the adapter
51
+ cache can't express the Vary, stay on `ssr`.
52
+
53
+ 5. **Loader reads cookies, auth headers, `context.env` per-request, or
54
+ anything personalized** → **`ssr`**
55
+ - Auth dashboards, anything user-specific, anything that varies by user
56
+ identity at request time.
57
+
58
+ 6. **Heavy client interactivity, no SEO need, auth-gated** → **`spa`**
59
+ - Internal admin tools, post-login dashboards where the first paint can be a
60
+ skeleton.
61
+
62
+ ## Step 1: Enumerate
63
+
64
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
65
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
66
+ shelling out.
67
+
68
+ ```bash
69
+ pracht inspect routes --json
70
+ ```
71
+
72
+ Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
73
+ wired up.
74
+
75
+ Capture: `path`, `file`, current `render`, `revalidate`, `middleware`, and
76
+ the top-level `mode` field (manifest vs pages router — Step 4 depends on it).
77
+
78
+ ## Step 2: Read each loader
79
+
80
+ For each route, open the file and look at the `loader`/`getStaticPaths`
81
+ exports. Tag the loader with one of:
82
+
83
+ - `none` — no loader at all
84
+ - `static` — only reads imports / pure data
85
+ - `param-static` — reads `params` only
86
+ - `request-static` — reads `request` for cache keys but data is shareable
87
+ (e.g. `Accept-Language`) — decision-tree branch 4
88
+ - `request-personalized` — reads cookies, auth headers, user-specific
89
+ `context.env` lookups — decision-tree branch 5
90
+
91
+ ## Step 3: Recommend
92
+
93
+ Produce a table:
94
+
95
+ | Route | Current | Recommended | Severity | Reason |
96
+ | ----- | ------- | ----------- | -------- | ------ |
97
+
98
+ Severity: `error` (broken today, e.g. `ssg` with a loader that reads
99
+ `request` — cannot be prerendered correctly), `warn` (works but mis-tuned,
100
+ e.g. `ssr` with no loader), `info` (optional improvement, e.g. hydration
101
+ tuning).
102
+
103
+ Examples of recommendations:
104
+ - `ssr` → `ssg` when loader is empty: "no loader; no per-request data — make it
105
+ static." (`warn`)
106
+ - `ssr` → `isg(3600)` when loader fetches a public CMS: "shared data, freshness
107
+ acceptable at 1 hour." (`warn`)
108
+ - `ssg` → `ssr` when loader reads `request.headers.get('cookie')`: "reads
109
+ request — cannot be prerendered." (`error`)
110
+ - `spa` → `ssr` when route has SEO-relevant `head()` and unauthenticated
111
+ visitors should see content. (`warn`)
112
+
113
+ ## Step 3b: Consider the hydration mode too
114
+
115
+ Render mode controls when HTML is generated; **hydration mode** controls how
116
+ much JavaScript ships afterwards (`hydration: "full" | "islands" | "none"`,
117
+ default `"full"` — see `docs/ISLANDS.md`). A current `@pracht/cli` emits the
118
+ resolved `hydration` per route in the inspect JSON; if your CLI predates the
119
+ field (absent from the JSON), grep the manifest for `hydration:` (pages apps:
120
+ `HYDRATION` exports) instead. While tuning, also flag:
121
+
122
+ - Routes with **no interactivity at all** (no event handlers, no hooks) →
123
+ `hydration: "none"` — zero JS shipped.
124
+ - Content-heavy routes with **one or two isolated widgets** (counter, search
125
+ box, newsletter form) → `hydration: "islands"` with the widgets moved to
126
+ `src/islands/`.
127
+ - Caveats: islands routes use MPA-style full-document navigation (no client
128
+ router), island props must be JSON-serializable, and `render: "spa"` cannot
129
+ combine with `"islands"`/`"none"`.
130
+
131
+ ## Step 4: Propose diffs, then apply on confirmation
132
+
133
+ Present the exact edits and wait for approval. Where the edit lands depends
134
+ on the router `mode` from Step 1:
135
+
136
+ - **Manifest apps**: edit `src/routes.ts` to update the `render` field. For
137
+ ISG, add `revalidate: timeRevalidate(N)` and import `timeRevalidate` from
138
+ `@pracht/core`. Hydration changes update the `hydration` field the same
139
+ way.
140
+ - **Pages apps**: render mode is a per-file constant —
141
+ `export const RENDER_MODE = "ssg"` in the page module (valid values
142
+ `"ssr" | "ssg" | "isg" | "spa"`; the default is `"ssr"`, overridable
143
+ globally via `pracht({ pagesDefaultRender: "..." })` in vite config).
144
+ Hydration is `export const HYDRATION = "..."` in the same file. If most
145
+ pages want the same mode, prefer changing `pagesDefaultRender` over adding
146
+ a constant to every file.
147
+
148
+ Apply the edits only after the user confirms.
149
+
150
+ ## Rules
151
+
152
+ 1. Never silently change render modes. Always present the recommendation and
153
+ the exact diff first; apply only after explicit user approval.
154
+ 2. If a route uses `auth` middleware, default to `ssr` — auth implies cookies.
155
+ 3. All three adapters support ISG — the mechanisms differ. Confirm which
156
+ adapter is in play, then use this capability table:
157
+
158
+ | Adapter | ISG mechanism (default) | Notes |
159
+ | ---------- | -------------------------------------------------------------- | ----- |
160
+ | Node | Filesystem: `isg-manifest.json` + file-mtime revalidation | Serves stale immediately, refreshes in place. |
161
+ | Cloudflare | Worker-managed Workers Cache API, **per colo** — works without any extra config | `cloudflareAdapter({ cache: true })` + `"cache": { "enabled": true }` in wrangler config is an **optional upgrade** that moves time-revalidated routes to an edge-tier cache in front of the Worker; webhook-only routes stay worker-managed. Webhook invalidation on the default path is per-colo, not a global purge. |
162
+ | Vercel | Native ISR: Build Output API prerender functions with `expiration` from the time policy; `PRACHT_REVALIDATE_TOKEN` becomes the `bypassToken` (must be set at build time) | See docs/ADAPTERS.md. |
163
+
164
+ 4. For dynamic SSG/ISG routes, ensure `getStaticPaths` exists. Flag if missing.
165
+ 5. Use `pracht inspect routes --json` rather than reading `src/routes.ts`
166
+ manually — the resolved graph already accounts for groups and inheritance.
167
+
168
+ $ARGUMENTS
@@ -0,0 +1,207 @@
1
+ ---
2
+ name: typed-routes
3
+ version: 1.1.0
4
+ description: |
5
+ Add or maintain pracht typed routes, typed links, route-object navigation,
6
+ and generated href helpers. Use when asked to "add typed routes", "fix typed
7
+ links", "replace hard-coded hrefs", "run typegen", or make navigation route-id
8
+ based instead of string based.
9
+ allowed-tools:
10
+ - Bash
11
+ - Read
12
+ - Write
13
+ - Edit
14
+ - Grep
15
+ - Glob
16
+ - AskUserQuestion
17
+ ---
18
+
19
+ # Pracht Typed Routes
20
+
21
+ Use this workflow to keep route ids, params, links, and navigation type-safe.
22
+
23
+ ## Step 1: Inspect the resolved graph
24
+
25
+ The resolved app graph is the source of truth — not a manual glob of `src/`.
26
+
27
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
28
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
29
+ `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
30
+ config with the pracht plugin registered.
31
+
32
+ ```bash
33
+ pracht inspect routes --json
34
+ ```
35
+
36
+ Check every route has a stable id. Explicit `id` fields are preferred for routes
37
+ that app code links to, because fallback ids change when paths change.
38
+
39
+ ```ts
40
+ route("/products/:id", () => import("./routes/products/[id].tsx"), {
41
+ id: "product",
42
+ render: "ssr",
43
+ });
44
+ ```
45
+
46
+ Any route without an explicit `id` — manifest apps included, not just
47
+ pages-router apps — gets a fallback id derived from the route path (`/` →
48
+ `index`, `/blog/:slug` → `blog-slug`, `/*` → `splat`).
49
+
50
+ ## Step 2: Generate route types and helpers
51
+
52
+ Run:
53
+
54
+ ```bash
55
+ pracht typegen
56
+ ```
57
+
58
+ This writes:
59
+
60
+ - `src/pracht.d.ts` — module augmentation for route ids, params, loader data
61
+ types, and API route request/response types (consumed by `apiFetch()`).
62
+ - `src/pracht-routes.ts` — runtime `href()` helper backed by the same route map.
63
+ - `src/pracht-capabilities.d.ts` — only when the app registers capabilities:
64
+ input/output types from the capability schemas, so `invokeCapability()`,
65
+ `callCapability()`, and `<Form capability>`'s `onCapabilityResult` infer
66
+ types from the capability name.
67
+
68
+ Earlier versions wrote the declaration to `src/pracht-routes.d.ts`; typegen
69
+ removes that stale file automatically (TypeScript silently ignored it next to
70
+ the same-named `.ts` helper).
71
+
72
+ Do not hand-edit generated files. If they are stale, update the route graph and
73
+ run typegen again — or rely on `pracht dev`, which refreshes them when route
74
+ files are added, removed, or renamed and when the route manifest or an imported
75
+ definition module changes. The dev banner prompts for the initial typegen run
76
+ when `src/pracht.d.ts` does not exist. In CI, prefer:
77
+
78
+ ```bash
79
+ pracht typegen --check
80
+ ```
81
+
82
+ ## Step 3: Replace string navigation where it matters
83
+
84
+ ### Components
85
+
86
+ ```tsx
87
+ import { Link, useNavigate } from "@pracht/core";
88
+
89
+ export function ProductLink({ id }: { id: string }) {
90
+ const navigate = useNavigate();
91
+
92
+ return (
93
+ <>
94
+ <Link route="product" params={{ id }} search={{ ref: "home" }}>
95
+ View product
96
+ </Link>
97
+ <button onClick={() => void navigate({ route: "product", params: { id } })}>
98
+ Open product
99
+ </button>
100
+ </>
101
+ );
102
+ }
103
+ ```
104
+
105
+ `<Link>` renders a normal `<a>` and the client router intercepts it like any
106
+ same-origin anchor. It also accepts navigation-behavior props:
107
+ `prefetch="none" | "hover" | "intent" | "viewport" | "render"` (per-link
108
+ prefetch strategy, default `"intent"`), `preserveScroll` (keep the scroll
109
+ position), and `viewTransition` (animate the navigation with the View Transitions API
110
+ where supported). There is also an imperative `prefetch()` export and a
111
+ `useNavigation()` hook for pending navigation/submission state.
112
+
113
+ ### Outside components
114
+
115
+ ```ts
116
+ import { href } from "./pracht-routes";
117
+
118
+ const productUrl = href("product", {
119
+ params: { id: "123" },
120
+ search: { tab: "details" },
121
+ });
122
+ ```
123
+
124
+ Use `href()` in loaders that return URLs, sitemap helpers, menu config, test
125
+ fixtures, and other non-component code.
126
+
127
+ ### Loader data
128
+
129
+ After typegen, `useRouteData(routeId)` returns that route's loader data with
130
+ no generic — route ids autocomplete and the type follows the route's loader
131
+ (or its separate loader file from the manifest):
132
+
133
+ ```tsx
134
+ import { useRouteData } from "@pracht/core";
135
+
136
+ export function Component() {
137
+ const data = useRouteData("product");
138
+ return <h1>{data.product.name}</h1>;
139
+ }
140
+ ```
141
+
142
+ Prefer this over `useRouteData<typeof loader>()` when typegen runs; keep the
143
+ generic form for projects that do not generate route types. Routes without a
144
+ loader type their data as `undefined`. The id must be the active route — dev
145
+ mode warns on mismatches.
146
+
147
+ ### API routes
148
+
149
+ After typegen, `apiFetch()` type-checks API calls end to end — paths,
150
+ methods, params, bodies and queries (for `defineApi()` routes), and response
151
+ types:
152
+
153
+ ```ts
154
+ import { apiFetch } from "@pracht/core";
155
+
156
+ const item = await apiFetch("/api/items/:id", { params: { id: "42" } });
157
+ ```
158
+
159
+ See docs/API_VALIDATION.md for `defineApi()` and validation error handling.
160
+ Typegen discovers API route files without importing them, so it is safe for
161
+ route modules that initialize runtime-only services at module scope.
162
+
163
+ Query and params values cross the wire as strings — write schemas that accept
164
+ string input (`z.coerce.number()`, not `z.number()`); `apiFetch()` rejects
165
+ query and params keys without a string representation at compile time when the
166
+ schema exposes a concrete input type. Handlers that need a custom status keep
167
+ typed payloads with `json(value, { status })`.
168
+
169
+ ## Step 4: Param and search rules
170
+
171
+ Generated param types accept `RouteParamInput = string | number | boolean`
172
+ (values are stringified into the path), so:
173
+
174
+ - `:id` requires `params: { id: RouteParamInput }` — a `string` is typical,
175
+ but `number`/`boolean` also typecheck.
176
+ - `*` requires `params: { "*": RouteParamInput }`.
177
+ - `:path*` requires `params: { path: RouteParamInput }`.
178
+ - Routes with no dynamic segments should omit `params`.
179
+ - Missing and extra params should fail at typecheck time.
180
+ - `search` currently accepts `string`, `URLSearchParams`, or an object of
181
+ primitive values/arrays; route-specific search schemas can be added later.
182
+
183
+ ## Step 5: Verify
184
+
185
+ Run at least:
186
+
187
+ ```bash
188
+ pracht typegen --check
189
+ pnpm typecheck
190
+ pracht verify --json
191
+ ```
192
+
193
+ If navigation changed, add or update Playwright coverage for both the rendered
194
+ anchor `href` and client-side navigation without a full page reload.
195
+
196
+ ## Rules
197
+
198
+ 1. Always start from `pracht inspect routes --json` or `pracht typegen`; do not
199
+ infer the full route map from files by hand.
200
+ 2. Prefer adding explicit ids before converting links for important routes.
201
+ 3. Never edit `src/pracht.d.ts` or `src/pracht-routes.ts` manually.
202
+ 4. Keep plain `<a href="...">` where a URL is genuinely external, opaque, or
203
+ user-provided.
204
+ 5. After adding/removing/renaming routes, run `pracht typegen` and include the
205
+ generated file changes in the same commit.
206
+
207
+ $ARGUMENTS