create-pracht 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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