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,208 @@
1
+ ---
2
+ name: pracht-deploy
3
+ version: 1.1.0
4
+ description: |
5
+ Pracht deployment guide. Walks through adapter configuration, building, and
6
+ deploying to Node.js, Cloudflare Workers, or Vercel. Handles wrangler config,
7
+ Docker and production checklist.
8
+ Use when asked to "deploy", "set up deployment", "configure adapter",
9
+ "deploy to cloudflare", "deploy to vercel", or "production build".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Write
14
+ - Edit
15
+ - Grep
16
+ - Glob
17
+ - AskUserQuestion
18
+ ---
19
+
20
+ # Pracht Deploy
21
+
22
+ Guided adapter setup and deployment for pracht applications.
23
+
24
+ ## Step 1: Determine the target
25
+
26
+ Read `vite.config.ts` and `package.json` first — don't assume the current adapter.
27
+ Ask the user where they want to deploy if not already clear from their message.
28
+
29
+ If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_build`/`doctor`/`verify` MCP tools over shelling out. Note: `inspect_build` (like `pracht inspect build`) needs a prior `pracht build`, and `pracht inspect` requires the pracht plugin registered in the vite config.
30
+
31
+ ## Supported Adapters
32
+
33
+ | Adapter | Package | Status |
34
+ | ------------------ | ---------------------------- | ------ |
35
+ | Node.js | `@pracht/adapter-node` | Stable |
36
+ | Cloudflare Workers | `@pracht/adapter-cloudflare` | Stable |
37
+ | Vercel | `@pracht/adapter-vercel` | Stable |
38
+
39
+ ---
40
+
41
+ ## Node.js Deployment
42
+
43
+ ### Setup
44
+
45
+ 1. Ensure `@pracht/adapter-node` is installed.
46
+ 2. In `vite.config.ts`:
47
+ ```ts
48
+ import { pracht } from "@pracht/vite-plugin";
49
+ import { nodeAdapter } from "@pracht/adapter-node";
50
+ export default { plugins: [pracht({ adapter: nodeAdapter() })] };
51
+ ```
52
+
53
+ ### Build
54
+
55
+ ```bash
56
+ pracht build
57
+ ```
58
+
59
+ Produces:
60
+
61
+ - `dist/client/` — static assets (JS, CSS, prerendered HTML)
62
+ - `dist/server/server.js` — Node server entry
63
+ - `dist/server/isg-manifest.json` — ISG revalidation config (if ISG routes exist)
64
+ - `dist/client/.vite/manifest.json` — asset manifest for script/style injection
65
+
66
+ ### Run
67
+
68
+ ```bash
69
+ node dist/server/server.js
70
+ ```
71
+
72
+ Port 3000 by default. For a local production smoke test, `pracht preview` builds and runs the server in one step (`--port <n>`, `--skip-build` to reuse an existing build). For production: reverse proxy (nginx, Caddy), process manager (PM2, systemd), `NODE_ENV=production`.
73
+
74
+ ### Docker
75
+
76
+ ```dockerfile
77
+ FROM node:22-alpine
78
+ WORKDIR /app
79
+ COPY dist/ dist/
80
+ COPY package.json .
81
+ EXPOSE 3000
82
+ CMD ["node", "dist/server/server.js"]
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Cloudflare Workers Deployment
88
+
89
+ ### Setup
90
+
91
+ 1. Ensure `@pracht/adapter-cloudflare` is installed.
92
+ 2. In `vite.config.ts`:
93
+ ```ts
94
+ import { pracht } from "@pracht/vite-plugin";
95
+ import { cloudflareAdapter } from "@pracht/adapter-cloudflare";
96
+ export default { plugins: [pracht({ adapter: cloudflareAdapter() })] };
97
+ ```
98
+
99
+ ### Build & Deploy
100
+
101
+ ```bash
102
+ pracht build
103
+ npx wrangler deploy
104
+ ```
105
+
106
+ To smoke-test the built worker locally first, run `pracht preview` — it builds and then delegates to `wrangler dev`, which serves the wrangler config's `main` entry, `dist/server/worker.js`.
107
+
108
+ ### Wrangler Configuration
109
+
110
+ ```jsonc
111
+ // wrangler.jsonc
112
+ {
113
+ "name": "my-pracht-app",
114
+ "main": "dist/server/worker.js",
115
+ "compatibility_date": "2024-01-01",
116
+ "assets": {
117
+ "binding": "ASSETS",
118
+ "directory": "dist/client",
119
+ "run_worker_first": true,
120
+ },
121
+ }
122
+ ```
123
+
124
+ `"binding": "ASSETS"` and `"run_worker_first": true` are required. Without the binding, the worker's `env.ASSETS` resolves to nothing and the runtime silently falls back to `null` — headers and ISG manifests load empty, so SSG serving, ISG revalidation, and per-route headers all silently no-op. The canonical config lives at `examples/cloudflare/wrangler.jsonc`. If you rename the binding with `assetsBinding` (below), the wrangler `binding` value must match.
125
+
126
+ ### Bindings (KV, D1, R2)
127
+
128
+ ```ts
129
+ export async function loader({ context }: LoaderArgs) {
130
+ const value = await context.env.MY_KV.get("key");
131
+ return { value };
132
+ }
133
+ ```
134
+
135
+ ### Custom Assets Binding
136
+
137
+ ```ts
138
+ pracht({ adapter: cloudflareAdapter({ assetsBinding: "STATIC" }) });
139
+ ```
140
+
141
+ ### ISG via Workers Caching
142
+
143
+ ISG works out of the box: without any cache option, the default worker-managed path serves the build-time snapshot, detects staleness, and regenerates pages in the background via the Workers Cache API — per colo — and `POST /__pracht/revalidate` triggers on-demand regeneration. Enabling `cache: true` moves ISG from that per-colo worker-managed path to edge-tier Workers Caching, on both sides:
144
+
145
+ ```ts
146
+ pracht({ adapter: cloudflareAdapter({ cache: true }) });
147
+ ```
148
+
149
+ ```jsonc
150
+ // wrangler.jsonc
151
+ { "cache": { "enabled": true } }
152
+ ```
153
+
154
+ Before enabling it, audit ISG URLs for unbounded query strings. Workers Caching
155
+ keys the exact path and query string, including parameter order and trailing
156
+ slashes; use a bounded query allowlist/canonical redirect or an uncached gateway
157
+ with a pathname-only `cf.cacheKey`, and normalize `Accept` there for routes that
158
+ export markdown. See `docs/ADAPTERS.md#cache-key-cardinality`.
159
+
160
+ Time-revalidated ISG pages then render on demand, are cached at the edge for
161
+ their `revalidate` window (stale pages served instantly while the Worker
162
+ re-renders in the background), and can be purged early with `purgeCache()` from
163
+ `@pracht/adapter-cloudflare/cache`. Webhook-only ISG routes keep their
164
+ build-time snapshots and the worker-managed path either way.
165
+
166
+ ---
167
+
168
+ ## Vercel Deployment
169
+
170
+ ### Setup
171
+
172
+ 1. Ensure `@pracht/adapter-vercel` is installed.
173
+ 2. In `vite.config.ts`:
174
+ ```ts
175
+ import { pracht } from "@pracht/vite-plugin";
176
+ import { vercelAdapter } from "@pracht/adapter-vercel";
177
+ export default { plugins: [pracht({ adapter: vercelAdapter() })] };
178
+ ```
179
+
180
+ ### Build & Deploy
181
+
182
+ ```bash
183
+ pracht build
184
+ npx vercel deploy --prebuilt
185
+ ```
186
+
187
+ Produces: `.vercel/output/config.json`, `.vercel/output/static/`, `.vercel/output/functions/render.func/server.js`
188
+
189
+ ---
190
+
191
+ ## Deployment Checklist
192
+
193
+ 1. **Build**: Run `pracht build` and verify `dist/` output.
194
+ 2. **Environment variables**: Ensure secrets/config needed by loaders are available at runtime.
195
+ 3. **Static assets**: Verify `dist/client/` contains prerendered HTML for SSG routes (and ISG routes — except time-revalidated ISG routes on Cloudflare with Workers Caching enabled, which render on demand; webhook-only ISG routes keep their build-time snapshots).
196
+ 4. **ISG routes**: Confirm the ISG manifest (`dist/server/isg-manifest.json`; on Cloudflare also `dist/client/_pracht/isg.json`) exists if using incremental static generation.
197
+ 5. **API routes**: Test API endpoints work in the production runtime. For Node.js, run `pracht preview` (or `node dist/server/server.js`).
198
+ 6. **Middleware**: Verify auth/redirect middleware behaves correctly in production.
199
+
200
+ ## Rules
201
+
202
+ 1. Read `vite.config.ts` and `package.json` before giving advice.
203
+ 2. Run `pracht build` to verify the build succeeds before deploying.
204
+ 3. Smoke-test the production runtime before pushing to production. For Node.js and Cloudflare, run `pracht preview`.
205
+ 4. If the user needs an adapter that isn't installed, help them add it (`pnpm add @pracht/adapter-*`).
206
+ 5. Don't push to production without the user's explicit confirmation.
207
+
208
+ $ARGUMENTS
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: pracht-scaffold
3
+ version: 1.1.0
4
+ description: |
5
+ Pracht code scaffolding. Prefer the framework-native CLI generators
6
+ (`pracht generate route|shell|middleware|api`) and only fall back to manual
7
+ edits when the CLI flags cannot express the requested shape. Knows pracht
8
+ conventions (Preact idioms, render modes, route manifest).
9
+ Use when asked to "scaffold", "generate a route", "create a new page",
10
+ "add middleware", "add an API route", or "create a shell".
11
+ allowed-tools:
12
+ - Bash
13
+ - Read
14
+ - Write
15
+ - Edit
16
+ - Grep
17
+ - Glob
18
+ - AskUserQuestion
19
+ ---
20
+
21
+ # Pracht Scaffold
22
+
23
+ Generate pracht framework modules with correct types, exports, and manifest wiring.
24
+
25
+ ## First Choice
26
+
27
+ Use the CLI first:
28
+
29
+ ```bash
30
+ pracht generate route --path /dashboard --render ssr
31
+ pracht generate shell --name app
32
+ pracht generate middleware --name auth
33
+ pracht generate api --path /health --methods GET,POST
34
+ ```
35
+
36
+ `pracht generate route` supports the full flag matrix below — do not fall back to manual edits for shapes it already covers:
37
+
38
+ | Flag | Meaning |
39
+ | ------------------ | ------------------------------------------------------------------------------------ |
40
+ | `--path` (required) | Route path, e.g. `/dashboard` or `/blog/:slug` |
41
+ | `--render` | Render mode: `ssr` (default), `spa`, `ssg`, or `isg` |
42
+ | `--shell` | Registered shell name (manifest apps only) |
43
+ | `--middleware` | Registered middleware names, comma-separated (manifest apps only) |
44
+ | `--loader` | Include a `loader` export |
45
+ | `--error-boundary` | Include an `ErrorBoundary` export |
46
+ | `--static-paths` | Include `getStaticPaths` (added automatically for dynamic `ssg`/`isg` paths) |
47
+ | `--title` | Page title used in the `head()` export |
48
+ | `--revalidate` | ISG revalidation window in seconds (`isg` only, default 3600) |
49
+ | `--json` | Machine-readable output |
50
+
51
+ `generate shell` and `generate middleware` take `--name`; `generate api` takes `--path` and `--methods` (comma-separated). All subcommands accept `--json`.
52
+
53
+ - `--shell`/`--middleware` names must already be registered in the app manifest — the CLI errors otherwise. Generate the shell/middleware first, then the route that references it.
54
+ - If the pracht MCP server is registered (docs/MCP.md), call the `generate_route`/`generate_shell`/`generate_middleware`/`generate_api` MCP tools instead of Bash — same behavior, structured results.
55
+ - Add `--json` when another agent/tool needs machine-readable output.
56
+ - `generate route` also emits a Playwright smoke test in `e2e/` when the app has a Playwright setup (`playwright.config.*` or an `e2e/` directory). Pass `--no-test` to skip it, `--test` to force it. Keep the generated test — it is the output-level proof the route works.
57
+ - Use `pracht inspect routes --json` or `pracht inspect api --json` to confirm current wiring before manual edits when the existing graph matters. `pracht inspect` requires the pracht plugin registered in the project's vite config.
58
+ - If the app has typed routes (`src/pracht-routes.ts` / `.d.ts`) or the user asks for typed links, run `pracht typegen` after adding or renaming routes.
59
+ - If the app commits `.pracht/app-graph.json`, run `pracht plan --write` after changing routes and include the refreshed snapshot — `pracht verify` fails when it is stale.
60
+ - If `src/routes.ts` declares `constraints:`, respect them (e.g. put new `/app/**` routes behind the required middleware). Never delete or weaken a constraint to make `pracht verify` pass — that is a policy change the user must approve.
61
+ - If the CLI can express the request, do not reimplement the scaffold by hand.
62
+ - Only edit files manually when the CLI cannot cover the requested shape.
63
+
64
+ The user will describe what they want to create. Parse their request and generate the appropriate module(s). Always ask if anything is ambiguous (e.g. render mode, shell assignment).
65
+
66
+ ## What You Can Scaffold
67
+
68
+ | Kind | Directory | Key exports | Example |
69
+ | ---------- | ----------------- | -------------------------------------------------------------------- | ------------------------------ |
70
+ | Route | `src/routes/` | `loader`, `head`, `Component`, `ErrorBoundary`, `getStaticPaths` | `src/routes/blog.tsx` |
71
+ | Shell | `src/shells/` | `Shell`, `head` | `src/shells/marketing.tsx` |
72
+ | Middleware | `src/middleware/` | `middleware` | `src/middleware/rate-limit.ts` |
73
+ | API route | `src/api/` | Named HTTP method handlers (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) or one default method dispatcher | `src/api/users/[id].ts` |
74
+
75
+ ## Templates (manual fallback)
76
+
77
+ Use these only when the CLI cannot express the requested shape.
78
+
79
+ ### Route
80
+
81
+ ```tsx
82
+ export function head() {
83
+ return { title: "Page Title" };
84
+ }
85
+
86
+ export function Component() {
87
+ return <section>{/* route UI */}</section>;
88
+ }
89
+ ```
90
+
91
+ - Include a `loader` only when the route needs server data (matches the CLI, which omits it unless `--loader` is passed):
92
+
93
+ ```tsx
94
+ import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
95
+
96
+ export async function loader(_args: LoaderArgs) {
97
+ return {
98
+ /* loader data */
99
+ };
100
+ }
101
+
102
+ export function Component({ data }: RouteComponentProps<typeof loader>) {
103
+ return <section>{/* route UI */}</section>;
104
+ }
105
+ ```
106
+
107
+ - Include `ErrorBoundary` only if requested.
108
+ - Include `getStaticPaths` only for SSG/ISG routes with dynamic segments.
109
+ - Use `RouteComponentProps<typeof loader>` for typed `data` prop.
110
+
111
+ ### Shell
112
+
113
+ ```tsx
114
+ import type { ShellProps } from "@pracht/core";
115
+
116
+ export function Shell({ children }: ShellProps) {
117
+ return (
118
+ <div class="shell-name">
119
+ <nav>{/* navigation */}</nav>
120
+ <main>{children}</main>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ export function head() {
126
+ return { title: "Shell Title" };
127
+ }
128
+ ```
129
+
130
+ ### Middleware
131
+
132
+ Middleware wraps the rest of the request via `next()`:
133
+
134
+ ```ts
135
+ import { redirect, type MiddlewareFn } from "@pracht/core";
136
+
137
+ export const middleware: MiddlewareFn = async ({ context, request }, next) => {
138
+ // Mutate context, validate auth, etc.
139
+ // - Call `return next()` to continue
140
+ // - Return `redirect("/path", { request })` to short-circuit with a redirect
141
+ // - Return any `Response` to short-circuit
142
+ // - Wrap `await next()` in try/catch/finally for tracing/logging
143
+ return next();
144
+ };
145
+ ```
146
+
147
+ ### API Route
148
+
149
+ ```ts
150
+ import type { ApiRouteArgs } from "@pracht/core";
151
+
152
+ export function GET({ params, url }: ApiRouteArgs) {
153
+ return Response.json({
154
+ /* response data */
155
+ });
156
+ }
157
+ ```
158
+
159
+ - Only include the HTTP methods the user needs.
160
+ - Use a default export only when the user wants to branch on `request.method` manually.
161
+ - Use `request.json()`, `request.formData()`, etc. for body parsing.
162
+ - Always return `Response` objects (typically `Response.json()`).
163
+ - Dynamic segments use bracket syntax in filenames: `[id].ts`, `[...slug].ts`.
164
+
165
+ ## Wiring Into the Manifest (manual fallback only)
166
+
167
+ The CLI generators wire the manifest themselves: `pracht generate route` inserts the `route(...)` call into `src/routes.ts` (adding `route`/`timeRevalidate` imports as needed), and `generate shell`/`generate middleware` upsert their registry entries. **Do not re-edit the manifest after a successful `pracht generate` run.**
168
+
169
+ Only when you created module files by hand, update `src/routes.ts` to register the new module:
170
+
171
+ - **Routes**: Add a `route("/path", () => import("./routes/filename.tsx"), { id: "name", render: "ssr" })` call inside the appropriate group or at the top level. Plain strings like `"./routes/filename.tsx"` also work.
172
+ - **Shells**: Add to the `shells` record: `shellName: () => import("./shells/filename.tsx")` (or `"./shells/filename.tsx"`).
173
+ - **Middleware**: Add to the `middleware` record: `mwName: () => import("./middleware/filename.ts")` (or `"./middleware/filename.ts"`).
174
+ - **API routes**: No manifest change needed — auto-discovered from `src/api/` by the Vite plugin.
175
+
176
+ Available render modes: `"ssr"` (default), `"ssg"` (static at build), `"isg"` (incremental static with `revalidate: timeRevalidate(seconds)`), `"spa"` (client-only).
177
+
178
+ Import `timeRevalidate` from `"@pracht/core"` when using ISG.
179
+
180
+ ## Rules
181
+
182
+ 1. Prefer `pracht generate ...` over manual edits.
183
+ 2. Read the project's existing `src/routes.ts` to determine current shells, middleware, and route structure before adding when the CLI cannot finish the job on its own.
184
+ 3. Place files in the conventional directories (`src/routes/`, `src/shells/`, `src/middleware/`, `src/api/`).
185
+ 4. Keep generated code minimal — only include exports the user actually needs.
186
+ 5. Use Preact idioms: `class` not `className`, functional components, `import type` for type-only imports.
187
+ 6. When route ids/paths change in a typed-routes app, run `pracht typegen` and include the generated route files.
188
+ 7. Finish with `pracht verify` (and `pracht plan --write` when the app commits an app-graph snapshot).
189
+ 8. After scaffolding, summarize what was created and how it was wired.
190
+
191
+ $ARGUMENTS
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: pracht-test-api
3
+ version: 1.1.0
4
+ description: |
5
+ Auto-generate Vitest request/response tests for every handler in `src/api/`.
6
+ Each test instantiates a `Request`, calls the exported HTTP method handler
7
+ directly, and asserts on the returned `Response` — no server boot required.
8
+ Use when asked to "test my API routes", "scaffold API tests", "generate
9
+ tests for src/api", or "add tests for this endpoint".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Write
14
+ - Edit
15
+ - Grep
16
+ - Glob
17
+ - AskUserQuestion
18
+ ---
19
+
20
+ # Pracht Test API
21
+
22
+ Pracht API handlers are plain functions: `(args: ApiRouteArgs) => Response |
23
+ Promise<Response>` (`ApiRouteArgs` is `BaseRouteArgs` with `route` narrowed
24
+ to `ResolvedApiRoute`). They test cleanly in Vitest without booting the
25
+ framework.
26
+
27
+ ## Step 1: Confirm Vitest is installed
28
+
29
+ If the project has no `vitest` dependency, run `scaffold-tests` first (or
30
+ prompt the user to). This skill does not handle Vitest setup.
31
+
32
+ ## Step 2: Enumerate API handlers
33
+
34
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
35
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
36
+ `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
37
+ config with the pracht plugin registered.
38
+
39
+ ```bash
40
+ pracht inspect api --json
41
+ ```
42
+
43
+ For each entry, capture: `path` (URL), `file` (source path), and the exported
44
+ `methods` (e.g., `["GET", "POST"]`). Note: `methods` only lists named HTTP
45
+ method exports — a default-export dispatcher yields `methods: []`, never
46
+ `["default"]`. Current `@pracht/cli` versions also report a
47
+ `hasDefaultHandler` boolean; use it when present. If the field is absent
48
+ (older CLI) and `methods` is empty, fall back to the grep in Step 7 to detect
49
+ a default dispatcher.
50
+
51
+ Ask the user which subset to scaffold or accept paths via `$ARGUMENTS`.
52
+
53
+ ## Step 3: Generate one test per handler file
54
+
55
+ Place tests next to the handler with `.test.ts` suffix
56
+ (`src/api/users/[id].test.ts`). Use a small helper to construct
57
+ `ApiRouteArgs`:
58
+
59
+ ```ts
60
+ import { describe, it, expect } from "vitest";
61
+ import { GET, POST /* import only what the handler exports */ } from "./<file>";
62
+
63
+ function args(url: string, init?: RequestInit, params: Record<string, string> = {}) {
64
+ const request = new Request(url, init);
65
+ return {
66
+ request,
67
+ params,
68
+ context: {} as never,
69
+ url: new URL(request.url),
70
+ signal: AbortSignal.timeout(5000),
71
+ route: {} as never,
72
+ };
73
+ }
74
+
75
+ describe("<METHOD> <api-path>", () => {
76
+ it("returns 200 on a valid request", async () => {
77
+ const res = await GET(args("http://localhost<api-path>"));
78
+ expect(res).toBeInstanceOf(Response);
79
+ expect(res.status).toBe(200);
80
+ });
81
+ });
82
+ ```
83
+
84
+ ## Step 4: Generate method-specific cases
85
+
86
+ For each exported method, emit the smallest realistic case:
87
+
88
+ | Method | Default case |
89
+ | ------- | --------------------------------------------------------------- |
90
+ | `GET` | Plain GET → `expect(res.status).toBeLessThan(400)` |
91
+ | `POST` | POST with empty `FormData` → assert validation behavior |
92
+ | `PUT` | PUT with JSON body → assert 200 or auth-required (401/403) |
93
+ | `PATCH` | PATCH with partial body → assert 200 or 422 |
94
+ | `DELETE`| DELETE on a real-shaped path → assert 200/204 or 401 |
95
+
96
+ For dynamic segments (`[id].ts` → `/api/users/:id`), pick a placeholder param
97
+ (e.g., `id: "test-1"`) and pass it via `params`. Surface in the report that
98
+ the user may need to provide a real fixture.
99
+
100
+ ## Step 5: Detect auth-gated APIs
101
+
102
+ API routes do NOT appear in `pracht inspect routes --json` (that report
103
+ covers page routes only) and `pracht inspect api --json` has no middleware
104
+ field. API middleware is the single global list configured as
105
+ `defineApp({ api: { middleware: [...] } })` — read the manifest source
106
+ (`src/routes.ts` or wherever `defineApp` lives) to see whether an auth
107
+ middleware is in that list. If it is, scaffold an extra test:
108
+
109
+ ```ts
110
+ it("rejects unauthenticated requests", async () => {
111
+ const res = await POST(args("http://localhost<api-path>", { method: "POST" }));
112
+ expect([401, 403, 302]).toContain(res.status);
113
+ });
114
+ ```
115
+
116
+ Note: middleware does NOT run when calling the handler directly — this test
117
+ verifies the handler's own defense if it has one. If the only defense is
118
+ middleware, mention that in the report and recommend an integration test that
119
+ goes through the framework's request pipeline (out of scope for this skill).
120
+ The same applies to the framework's built-in CSRF check: `requireSameOrigin`
121
+ (on by default) rejects cross-origin state-changing API requests in the real
122
+ pipeline but never runs in direct-invocation tests.
123
+
124
+ ## Step 6: Validate JSON shape
125
+
126
+ For handlers that return `Response.json(...)`, generate:
127
+
128
+ ```ts
129
+ it("returns JSON with the expected keys", async () => {
130
+ const res = await GET(args("http://localhost<api-path>"));
131
+ expect(res.headers.get("content-type")).toMatch(/application\/json/);
132
+ const body = await res.json();
133
+ expect(body).toEqual(expect.objectContaining({ /* fill in */ }));
134
+ });
135
+ ```
136
+
137
+ ## Step 7: Default-handler dispatchers
138
+
139
+ If the handler exports `default` (one function dispatching on
140
+ `request.method`), generate a test per HTTP method the handler appears to
141
+ support (grep for `request.method ===` patterns inside the file).
142
+
143
+ ## Step 8: Run
144
+
145
+ ```bash
146
+ pnpm test
147
+ pracht verify --json
148
+ ```
149
+
150
+ Report passes/failures. Mark generated assertions as TODO so the user knows
151
+ to tighten them.
152
+
153
+ ## Rules
154
+
155
+ 1. Use `pracht inspect api --json` as the inventory — do not glob.
156
+ 2. Only import methods the handler actually exports; otherwise the test fails
157
+ to load.
158
+ 3. Direct-handler invocation skips middleware AND the default-on
159
+ `requireSameOrigin` CSRF check. Be explicit in the report.
160
+ 4. Test files live next to handlers with `.test.ts` suffix unless the
161
+ project already uses a `__tests__/` convention (detect and match).
162
+ 5. Never overwrite existing test files; emit a `.next.test.ts` and tell the
163
+ user to merge.
164
+
165
+ $ARGUMENTS