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.
- package/README.md +38 -0
- package/package.json +3 -2
- package/skills/add-auth/SKILL.md +346 -0
- package/skills/add-db/SKILL.md +316 -0
- package/skills/add-i18n/SKILL.md +239 -0
- package/skills/add-observability/SKILL.md +270 -0
- package/skills/audit-a11y/SKILL.md +170 -0
- package/skills/audit-auth/SKILL.md +144 -0
- package/skills/audit-bundles/SKILL.md +167 -0
- package/skills/audit-csrf/SKILL.md +179 -0
- package/skills/audit-deps/SKILL.md +138 -0
- package/skills/audit-headers/SKILL.md +186 -0
- package/skills/audit-islands/SKILL.md +144 -0
- package/skills/audit-loaders/SKILL.md +135 -0
- package/skills/audit-redirects/SKILL.md +146 -0
- package/skills/audit-secrets/SKILL.md +150 -0
- package/skills/audit-seo/SKILL.md +164 -0
- package/skills/audit-shells/SKILL.md +142 -0
- package/skills/configure-isg/SKILL.md +172 -0
- package/skills/migrate-nextjs/SKILL.md +519 -0
- package/skills/pracht-debug/SKILL.md +146 -0
- package/skills/pracht-deploy/SKILL.md +208 -0
- package/skills/pracht-scaffold/SKILL.md +191 -0
- package/skills/pracht-test-api/SKILL.md +165 -0
- package/skills/pre-deploy/SKILL.md +164 -0
- package/skills/scaffold-e2e/SKILL.md +189 -0
- package/skills/scaffold-tests/SKILL.md +283 -0
- package/skills/tune-render-mode/SKILL.md +168 -0
- package/skills/typed-routes/SKILL.md +203 -0
- package/skills/upgrade-pracht/SKILL.md +151 -0
- package/src/index.js +472 -31
|
@@ -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
|