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.
- package/README.md +11 -1
- 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 +536 -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 +174 -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 +207 -0
- package/skills/upgrade-pracht/SKILL.md +152 -0
- package/src/index.js +148 -10
|
@@ -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
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pre-deploy
|
|
3
|
+
version: 1.2.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
|
+
- If the app mounts `createImageHandler()` from `@pracht/image/node`, confirm
|
|
79
|
+
`sharp` is installed and `localOrigin` is the same trusted public origin as
|
|
80
|
+
`nodeAdapter({ canonicalOrigin })`. A production-relative image endpoint
|
|
81
|
+
without both values is an error.
|
|
82
|
+
- Reverse-proxy / TLS termination configured (out of scope for this skill —
|
|
83
|
+
flag for confirmation).
|
|
84
|
+
|
|
85
|
+
### Cloudflare Workers (`@pracht/adapter-cloudflare`)
|
|
86
|
+
|
|
87
|
+
- `wrangler.toml` (or `wrangler.jsonc`) present at repo root.
|
|
88
|
+
- `main` points to `dist/server/worker.js` — the thin deploy wrapper that
|
|
89
|
+
re-exports only the default handler and Cloudflare entrypoint classes.
|
|
90
|
+
Pointing `main` at `dist/server/server.js` is an **error**: workerd
|
|
91
|
+
validates every named export of the deploy entry and rejects the build
|
|
92
|
+
metadata (`buildTarget`, manifests, `resolvedApp`, ...) that `server.js`
|
|
93
|
+
exports for the prerender pass.
|
|
94
|
+
- `assets.directory` points to `dist/client`.
|
|
95
|
+
- `compatibility_date` is set and recent.
|
|
96
|
+
- Bindings declared in wrangler config for every `context.env.*` access in
|
|
97
|
+
loaders, middleware, and API routes (grep, then cross-check).
|
|
98
|
+
- **No Node-only APIs in the server bundle.** Grep the server files for:
|
|
99
|
+
`fs`, `path` (Node form), `process.cwd`, `Buffer`, `__dirname`,
|
|
100
|
+
`__filename`, `crypto.createHash` (use `crypto.subtle` instead),
|
|
101
|
+
`child_process`, `cluster`, `worker_threads`. Two nuances before flagging:
|
|
102
|
+
- Consult `compatibility_flags` in the wrangler config first — with
|
|
103
|
+
`nodejs_compat`, `Buffer` and several `node:` modules are legal in
|
|
104
|
+
workerd. Only flag APIs the active flags don't cover.
|
|
105
|
+
- Dev already runs inside workerd via `@cloudflare/vite-plugin`, so most
|
|
106
|
+
incompatibilities surface in dev; this check is the backstop for code
|
|
107
|
+
paths dev never hit.
|
|
108
|
+
- An API route importing `@pracht/image/node` is an error on Workers because
|
|
109
|
+
its optimizer requires `sharp`. Require `cloudflareLoader` (or
|
|
110
|
+
`passthroughLoader`) instead.
|
|
111
|
+
- ISG: worker-managed ISG via the per-colo Workers Cache API works out of the
|
|
112
|
+
box. If time-revalidated routes should use the edge-tier Workers Caching
|
|
113
|
+
upgrade instead, confirm both sides — `cloudflareAdapter({ cache: true })`
|
|
114
|
+
in vite config and `"cache": { "enabled": true }` in wrangler config.
|
|
115
|
+
- When Workers Caching is enabled, flag ISG routes reachable through unbounded
|
|
116
|
+
query strings. Require a bounded allowlist/canonical redirect or an uncached
|
|
117
|
+
gateway with a normalized `cf.cacheKey`; also check that markdown-capable
|
|
118
|
+
routes normalize `Accept` at the gateway when variant fan-out matters.
|
|
119
|
+
- Bundle size: measure what actually deploys — `dist/server/worker.js` plus
|
|
120
|
+
its `dist/server/server.js` import (wrangler bundles the import graph of
|
|
121
|
+
`main`; `worker.js` alone is a few lines). Workers limit is ~1 MB
|
|
122
|
+
compressed for free tier, ~10 MB on paid. Warn at 80% of the active limit.
|
|
123
|
+
|
|
124
|
+
### Vercel (`@pracht/adapter-vercel`)
|
|
125
|
+
|
|
126
|
+
- `.vercel/output/config.json` exists post-build.
|
|
127
|
+
- The render function exists at
|
|
128
|
+
`.vercel/output/functions/<functionName>.func/server.js`. The name defaults
|
|
129
|
+
to `render` but is configurable via `vercelAdapter({ functionName })` —
|
|
130
|
+
read the configured name from `vite.config.ts` instead of hardcoding
|
|
131
|
+
`render.func`.
|
|
132
|
+
- `.vercel/output/static/` populated.
|
|
133
|
+
- Required env vars are configured in the Vercel project (cannot verify from
|
|
134
|
+
CLI without `vercel env pull` — run that and diff against `process.env.*`
|
|
135
|
+
references).
|
|
136
|
+
- Edge runtime constraints: pracht **always** writes the function's
|
|
137
|
+
`.vc-config.json` with `runtime: "edge"` — there is no Node runtime
|
|
138
|
+
variant, so run the same Node-only API check as Cloudflare
|
|
139
|
+
**unconditionally** for Vercel builds. Do not skip it based on a runtime
|
|
140
|
+
probe.
|
|
141
|
+
- An API route importing `@pracht/image/node` is an error for the Vercel Edge
|
|
142
|
+
function. Require `vercelLoader` (with aligned allowed sizes) or
|
|
143
|
+
`passthroughLoader` instead.
|
|
144
|
+
- Build Output API v3 sanity: `config.json` has `version: 3`.
|
|
145
|
+
|
|
146
|
+
## Step 4: Cross-cutting checks
|
|
147
|
+
|
|
148
|
+
- Run `audit-secrets` to confirm no `process.env.*` or `context.env.*` values
|
|
149
|
+
flow into loader return values.
|
|
150
|
+
- Run `audit-headers` to confirm `applyDefaultSecurityHeaders` is in use on
|
|
151
|
+
user-facing responses (or that `headers()` exports cover the same ground).
|
|
152
|
+
- Confirm `git status` is clean (deploying uncommitted work is a footgun).
|
|
153
|
+
|
|
154
|
+
## Step 5: Report
|
|
155
|
+
|
|
156
|
+
Produce a checklist grouped by `Framework`, `Adapter`, `Cross-cutting`. Tag
|
|
157
|
+
each item with a primary severity — `error` (blocks deploy), `warn` (deploy
|
|
158
|
+
proceeds but risky), `info` — and keep pass/fail as the secondary per-item
|
|
159
|
+
status. End with a one-line verdict: `READY` / `BLOCKED (N errors)` /
|
|
160
|
+
`READY WITH WARNINGS (N warnings)`.
|
|
161
|
+
|
|
162
|
+
## Rules
|
|
163
|
+
|
|
164
|
+
1. Always run `pracht build` first. Do not lint a stale `dist/`.
|
|
165
|
+
2. Detect the adapter — never assume.
|
|
166
|
+
3. For Cloudflare/Vercel-edge, the Node-only API check is non-negotiable; an
|
|
167
|
+
API not covered by the active compatibility flags will crash the worker on
|
|
168
|
+
a code path that may never hit in dev.
|
|
169
|
+
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.
|
|
170
|
+
5. Do not deploy on the user's behalf. End the skill at the verdict.
|
|
171
|
+
6. If `pracht doctor` reports errors, do not run any other checks until those
|
|
172
|
+
are resolved — they will produce noisy false positives.
|
|
173
|
+
|
|
174
|
+
$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
|