create-pracht 0.2.6 → 0.4.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.
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: pre-deploy
3
+ version: 1.1.0
4
+ description: |
5
+ Adapter-aware pre-deployment checklist for pracht apps targeting Node,
6
+ Cloudflare Workers, or Vercel. Catches the issues that only surface in the
7
+ production runtime: missing env vars, Node-only APIs in edge bundles,
8
+ ISG manifest absence, oversized edge bundles, missing wrangler/vercel config.
9
+ Use when asked to "pre-deploy check", "ready to ship?", "deployment
10
+ checklist", "is my build production-safe", or before running `wrangler
11
+ deploy` / `vercel deploy`.
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Grep
16
+ - Glob
17
+ ---
18
+
19
+ # Pracht Pre-Deploy
20
+
21
+ Run this before every production deploy. Each adapter has a different runtime
22
+ contract; this skill enforces the contract that matches your build.
23
+
24
+ ## Step 1: Detect the adapter
25
+
26
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
27
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
28
+ shelling out.
29
+
30
+ Read `vite.config.ts` and look for `nodeAdapter()`, `cloudflareAdapter()`, or
31
+ `vercelAdapter()`. Confirm with:
32
+
33
+ ```bash
34
+ pracht inspect build --json
35
+ ```
36
+
37
+ The `adapterTarget` field is authoritative. Prerequisites: `pracht inspect`
38
+ needs a vite config with the pracht plugin, and `inspect build` reads
39
+ artifacts from a prior build — if `pracht build` has not been run recently,
40
+ run it first:
41
+
42
+ ```bash
43
+ pracht build
44
+ ```
45
+
46
+ ## Step 2: Run framework-wide checks
47
+
48
+ ```bash
49
+ pracht doctor --json
50
+ pracht verify --json
51
+ ```
52
+
53
+ If the app uses generated typed routes (`src/pracht-routes.ts` or
54
+ `src/pracht.d.ts` exists), also run:
55
+
56
+ ```bash
57
+ pracht typegen --check
58
+ ```
59
+
60
+ These catch app-graph wiring problems independent of the adapter — including
61
+ `defineApp({ constraints })` violations and a stale `.pracht/app-graph.json`
62
+ snapshot (fix the latter with `pracht plan --write`, then re-review the plan
63
+ output). Resolve all `status: "error"` entries before continuing.
64
+
65
+ When the deploy corresponds to a PR, `pracht report --base origin/main` produces
66
+ a markdown summary (graph diff + verify + budgets) worth attaching to it.
67
+
68
+ ## Step 3: Adapter-specific checklist
69
+
70
+ ### Node (`@pracht/adapter-node`)
71
+
72
+ - `dist/server/server.js` exists.
73
+ - `dist/client/.vite/manifest.json` exists.
74
+ - `dist/server/isg-manifest.json` exists if any route has `render: "isg"`.
75
+ - Smoke test: `pracht preview --skip-build` (or `node dist/server/server.js`) boots and `curl localhost:3000` returns 200.
76
+ - Required env vars (grep `process.env.*` across `src/`) are set in the
77
+ deployment environment. List them for the user.
78
+ - Reverse-proxy / TLS termination configured (out of scope for this skill —
79
+ flag for confirmation).
80
+
81
+ ### Cloudflare Workers (`@pracht/adapter-cloudflare`)
82
+
83
+ - `wrangler.toml` (or `wrangler.jsonc`) present at repo root.
84
+ - `main` points to `dist/server/worker.js` — the thin deploy wrapper that
85
+ re-exports only the default handler and Cloudflare entrypoint classes.
86
+ Pointing `main` at `dist/server/server.js` is an **error**: workerd
87
+ validates every named export of the deploy entry and rejects the build
88
+ metadata (`buildTarget`, manifests, `resolvedApp`, ...) that `server.js`
89
+ exports for the prerender pass.
90
+ - `assets.directory` points to `dist/client`.
91
+ - `compatibility_date` is set and recent.
92
+ - Bindings declared in wrangler config for every `context.env.*` access in
93
+ loaders, middleware, and API routes (grep, then cross-check).
94
+ - **No Node-only APIs in the server bundle.** Grep the server files for:
95
+ `fs`, `path` (Node form), `process.cwd`, `Buffer`, `__dirname`,
96
+ `__filename`, `crypto.createHash` (use `crypto.subtle` instead),
97
+ `child_process`, `cluster`, `worker_threads`. Two nuances before flagging:
98
+ - Consult `compatibility_flags` in the wrangler config first — with
99
+ `nodejs_compat`, `Buffer` and several `node:` modules are legal in
100
+ workerd. Only flag APIs the active flags don't cover.
101
+ - Dev already runs inside workerd via `@cloudflare/vite-plugin`, so most
102
+ incompatibilities surface in dev; this check is the backstop for code
103
+ paths dev never hit.
104
+ - ISG: worker-managed ISG via the per-colo Workers Cache API works out of the
105
+ box. If time-revalidated routes should use the edge-tier Workers Caching
106
+ upgrade instead, confirm both sides — `cloudflareAdapter({ cache: true })`
107
+ in vite config and `"cache": { "enabled": true }` in wrangler config.
108
+ - When Workers Caching is enabled, flag ISG routes reachable through unbounded
109
+ query strings. Require a bounded allowlist/canonical redirect or an uncached
110
+ gateway with a normalized `cf.cacheKey`; also check that markdown-capable
111
+ routes normalize `Accept` at the gateway when variant fan-out matters.
112
+ - Bundle size: measure what actually deploys — `dist/server/worker.js` plus
113
+ its `dist/server/server.js` import (wrangler bundles the import graph of
114
+ `main`; `worker.js` alone is a few lines). Workers limit is ~1 MB
115
+ compressed for free tier, ~10 MB on paid. Warn at 80% of the active limit.
116
+
117
+ ### Vercel (`@pracht/adapter-vercel`)
118
+
119
+ - `.vercel/output/config.json` exists post-build.
120
+ - The render function exists at
121
+ `.vercel/output/functions/<functionName>.func/server.js`. The name defaults
122
+ to `render` but is configurable via `vercelAdapter({ functionName })` —
123
+ read the configured name from `vite.config.ts` instead of hardcoding
124
+ `render.func`.
125
+ - `.vercel/output/static/` populated.
126
+ - Required env vars are configured in the Vercel project (cannot verify from
127
+ CLI without `vercel env pull` — run that and diff against `process.env.*`
128
+ references).
129
+ - Edge runtime constraints: pracht **always** writes the function's
130
+ `.vc-config.json` with `runtime: "edge"` — there is no Node runtime
131
+ variant, so run the same Node-only API check as Cloudflare
132
+ **unconditionally** for Vercel builds. Do not skip it based on a runtime
133
+ probe.
134
+ - Build Output API v3 sanity: `config.json` has `version: 3`.
135
+
136
+ ## Step 4: Cross-cutting checks
137
+
138
+ - Run `audit-secrets` to confirm no `process.env.*` or `context.env.*` values
139
+ flow into loader return values.
140
+ - Run `audit-headers` to confirm `applyDefaultSecurityHeaders` is in use on
141
+ user-facing responses (or that `headers()` exports cover the same ground).
142
+ - Confirm `git status` is clean (deploying uncommitted work is a footgun).
143
+
144
+ ## Step 5: Report
145
+
146
+ Produce a checklist grouped by `Framework`, `Adapter`, `Cross-cutting`. Tag
147
+ each item with a primary severity — `error` (blocks deploy), `warn` (deploy
148
+ proceeds but risky), `info` — and keep pass/fail as the secondary per-item
149
+ status. End with a one-line verdict: `READY` / `BLOCKED (N errors)` /
150
+ `READY WITH WARNINGS (N warnings)`.
151
+
152
+ ## Rules
153
+
154
+ 1. Always run `pracht build` first. Do not lint a stale `dist/`.
155
+ 2. Detect the adapter — never assume.
156
+ 3. For Cloudflare/Vercel-edge, the Node-only API check is non-negotiable; an
157
+ API not covered by the active compatibility flags will crash the worker on
158
+ a code path that may never hit in dev.
159
+ 4. If the app does not use generated typed route files yet, note that `pracht typegen --check` is optional; if it does, stale generated files block deployment.
160
+ 5. Do not deploy on the user's behalf. End the skill at the verdict.
161
+ 6. If `pracht doctor` reports errors, do not run any other checks until those
162
+ are resolved — they will produce noisy false positives.
163
+
164
+ $ARGUMENTS
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: scaffold-e2e
3
+ version: 1.1.0
4
+ description: |
5
+ Scaffold Playwright end-to-end tests for a pracht app: install Playwright,
6
+ generate `playwright.config.ts` that boots `pracht dev` (or the production
7
+ build via `pracht preview`), and emit a smoke test for every route in the
8
+ manifest that asserts 200, head/title, no console errors, and basic
9
+ navigation.
10
+ Use when asked to "scaffold E2E", "set up Playwright", "add browser tests",
11
+ or "create smoke tests for my routes".
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Write
16
+ - Edit
17
+ - Grep
18
+ - Glob
19
+ - AskUserQuestion
20
+ ---
21
+
22
+ # Pracht Scaffold E2E
23
+
24
+ Generate a Playwright suite that exercises the live app. Aligns with
25
+ `recipes-testing.md`.
26
+
27
+ ## Step 1: Decide which build the tests run against
28
+
29
+ Ask the user (default: `dev`):
30
+
31
+ 1. **`pracht dev`** — fast, HMR, but can mask production-only bugs (different
32
+ bundling, different SSG/ISG flow).
33
+ 2. **Production runtime** — `pracht preview`: purpose-built, it builds and
34
+ serves the production output locally for both Node and Cloudflare targets
35
+ (Vercel prints guidance instead). Use `pracht preview --skip-build` to
36
+ serve an existing build after an explicit `pracht build`. Prefer this over
37
+ hand-rolling `node dist/server/server.js`, which is Node-adapter-only and
38
+ skips the build.
39
+ 3. **External URL** — user provides `BASE_URL`; tests do not boot the server.
40
+
41
+ ## Step 2: Install
42
+
43
+ ```bash
44
+ pnpm add -D @playwright/test
45
+ pnpm exec playwright install chromium
46
+ ```
47
+
48
+ Add `firefox` and `webkit` only if the user requests them.
49
+
50
+ ## Step 3: Write `playwright.config.ts`
51
+
52
+ ```ts
53
+ import { defineConfig, devices } from "@playwright/test";
54
+
55
+ const PORT = Number(process.env.PORT ?? 3000);
56
+
57
+ export default defineConfig({
58
+ testDir: "./e2e",
59
+ fullyParallel: true,
60
+ retries: process.env.CI ? 2 : 0,
61
+ reporter: process.env.CI ? "github" : "list",
62
+ use: {
63
+ baseURL: process.env.BASE_URL ?? `http://localhost:${PORT}`,
64
+ trace: "on-first-retry",
65
+ screenshot: "only-on-failure",
66
+ },
67
+ webServer: process.env.BASE_URL
68
+ ? undefined
69
+ : {
70
+ command: "pnpm dev", // or "pracht preview" for the production runtime — choose at scaffold time
71
+ url: `http://localhost:${PORT}`,
72
+ reuseExistingServer: !process.env.CI,
73
+ timeout: 120_000,
74
+ },
75
+ projects: [
76
+ { name: "chromium", use: devices["Desktop Chrome"] },
77
+ ],
78
+ });
79
+ ```
80
+
81
+ If `playwright.config.ts` already exists, merge — do not clobber.
82
+
83
+ ## Step 4: Generate per-route smoke tests
84
+
85
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
86
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
87
+ `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
88
+ config with the pracht plugin registered.
89
+
90
+ ```bash
91
+ pracht inspect routes --json
92
+ ```
93
+
94
+ For every route entry, generate one spec under `e2e/smoke/`. Skip routes with
95
+ dynamic segments unless the user provides example params (ask via
96
+ `AskUserQuestion`).
97
+
98
+ Per-route template:
99
+
100
+ ```ts
101
+ import { test, expect } from "@playwright/test";
102
+
103
+ test.describe("smoke: <path>", () => {
104
+ test("loads with 200 and a title", async ({ page }) => {
105
+ const errors: string[] = [];
106
+ page.on("console", (msg) => {
107
+ if (msg.type() === "error") errors.push(msg.text());
108
+ });
109
+ page.on("pageerror", (err) => errors.push(err.message));
110
+
111
+ const response = await page.goto("<path>");
112
+ expect(response?.status(), "HTTP status").toBeLessThan(400);
113
+ await expect(page).toHaveTitle(/.+/);
114
+ expect(errors, "no console errors").toEqual([]);
115
+ });
116
+ });
117
+ ```
118
+
119
+ ## Step 5: Generate a navigation crawl
120
+
121
+ A single `e2e/navigation.spec.ts` that:
122
+
123
+ 1. Visits the home route.
124
+ 2. Waits for hydration with the documented readiness idiom before touching
125
+ anything — otherwise the crawl races hydration and clicks fall through to
126
+ full page loads:
127
+ ```ts
128
+ await page.waitForFunction(() => (window as any).__PRACHT_ROUTER_READY__);
129
+ ```
130
+ 3. Collects every same-origin `<a href>`.
131
+ 4. Clicks each in turn, asserts no console errors, no 4xx/5xx.
132
+
133
+ This catches client-router intercept regressions and broken links.
134
+
135
+ ## Step 6: Generate a hydration check (optional)
136
+
137
+ If any route is `render: "ssr"` or `"ssg"`/`"isg"`:
138
+
139
+ ```ts
140
+ test("server HTML matches client render", async ({ page }) => {
141
+ const errors: string[] = [];
142
+ page.on("console", (m) => m.type() === "error" && errors.push(m.text()));
143
+ await page.goto("<path>");
144
+ await page.waitForLoadState("networkidle");
145
+ expect(errors.filter((e) => /Hydration|hydrat/i.test(e))).toEqual([]);
146
+ });
147
+ ```
148
+
149
+ ## Step 7: Wire `package.json`
150
+
151
+ ```json
152
+ {
153
+ "scripts": {
154
+ "e2e": "playwright test",
155
+ "e2e:ui": "playwright test --ui"
156
+ }
157
+ }
158
+ ```
159
+
160
+ If an `e2e` script already exists in the project, confirm before
161
+ overwriting.
162
+
163
+ ## Step 8: Run
164
+
165
+ ```bash
166
+ pnpm e2e
167
+ pracht verify --json
168
+ ```
169
+
170
+ If specs fail on first run, report. Common first-run failures:
171
+ - Port collision (already-running dev server).
172
+ - Missing browser binary (`playwright install chromium`).
173
+ - Strict CSP blocking inline test scripts (rare).
174
+
175
+ ## Rules
176
+
177
+ 1. Source of routes is `pracht inspect routes --json`. Do not glob
178
+ `src/routes/**`.
179
+ 2. Skip dynamic-segment routes unless example params are provided.
180
+ 3. Never overwrite an existing `playwright.config.ts` without diffing first.
181
+ 4. If the app uses typed routes, run `pracht typegen --check` before generating specs and prefer imported `href()` for test URLs in app-owned route fixtures.
182
+ 5. Use `webServer` to boot `pracht dev` or `pracht preview` so CI works
183
+ out-of-the-box.
184
+ 6. Console-error capture is mandatory — silent JS errors are the most common
185
+ pracht hydration regression.
186
+ 7. Wait for `window.__PRACHT_ROUTER_READY__` before interacting with the
187
+ page in any spec that relies on client-side navigation.
188
+
189
+ $ARGUMENTS
@@ -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