create-pracht 0.4.1 → 0.5.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 +20 -4
- package/package.json +1 -1
- package/skills/audit-auth/SKILL.md +36 -4
- package/skills/audit-csrf/SKILL.md +2 -1
- package/skills/audit-secrets/SKILL.md +6 -0
- package/skills/configure-isg/SKILL.md +30 -15
- package/skills/migrate-nextjs/SKILL.md +11 -2
- package/skills/pracht-deploy/SKILL.md +124 -7
- package/skills/pracht-test-api/SKILL.md +13 -19
- package/skills/pre-deploy/SKILL.md +21 -8
- package/skills/scaffold-tests/SKILL.md +34 -50
- package/skills/tune-render-mode/SKILL.md +4 -1
- package/src/index.js +523 -34
package/README.md
CHANGED
|
@@ -17,10 +17,15 @@ npm run dev
|
|
|
17
17
|
- Detects the active package manager from the current environment.
|
|
18
18
|
- Lets the user choose between the Node.js, Cloudflare, and Vercel adapters.
|
|
19
19
|
- Optionally wires up Tailwind CSS (`tailwindcss` + `@tailwindcss/vite`, a global stylesheet, and the shell import).
|
|
20
|
-
- Scaffolds a minimal app with a route manifest or pages router, shell, home route, sample API route, runnable project README, and agent instructions.
|
|
20
|
+
- Scaffolds a minimal app with a route manifest or pages router, shell, home route, not-found page, sample API route, runnable project README, TypeScript typecheck script, and (with agent tooling enabled) agent instructions.
|
|
21
21
|
- Manifest scaffolds include a commented-out `constraints` example in `src/routes.ts`, ready for `pracht verify`.
|
|
22
22
|
- The generated `.gitignore` keeps `.pracht/app-graph.json` committable, and the README and agent instructions cover the `pracht verify` / `pracht plan` / `pracht report` loop.
|
|
23
|
-
-
|
|
23
|
+
- Every standalone pnpm scaffold includes a narrow lifecycle-script policy for
|
|
24
|
+
its required native dependencies in `pnpm-workspace.yaml`:
|
|
25
|
+
`onlyBuiltDependencies` for pnpm 10 or `allowBuilds` for pnpm 11. All adapters
|
|
26
|
+
allow `esbuild`, Cloudflare also allows `workerd`, and Tailwind starters also
|
|
27
|
+
allow `@tailwindcss/oxide`.
|
|
28
|
+
- Seeds the pracht Claude Code skills into `.claude/skills/`, writes a `.mcp.json` registering the `pracht mcp` server, and writes `AGENTS.md` (yes-default prompt; all of it skipped with `--no-agent-tools`, which leaves a project with no agent files at all).
|
|
24
29
|
- Initializes a git repository with an initial commit (skipped with `--no-git`, when git is unavailable, or when the target is already inside a repository).
|
|
25
30
|
- `--dry-run` uses pinned fallback versions and does not require npm registry access.
|
|
26
31
|
|
|
@@ -30,17 +35,18 @@ npm run dev
|
|
|
30
35
|
node ./packages/start/bin/create-pracht.js
|
|
31
36
|
node ./packages/start/bin/create-pracht.js my-app --adapter=node --skip-install
|
|
32
37
|
node ./packages/start/bin/create-pracht.js my-app --adapter=vercel --skip-install
|
|
38
|
+
node ./packages/start/bin/create-pracht.js my-app --adapter=netlify --skip-install
|
|
33
39
|
node ./packages/start/bin/create-pracht.js my-app --template=tailwind --yes
|
|
34
40
|
node ./packages/start/bin/create-pracht.js my-app --adapter=node --no-tailwind --no-git --yes
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
## Options
|
|
38
44
|
|
|
39
|
-
- `--adapter=node|cf|vercel` — choose the hosting adapter (default: node).
|
|
45
|
+
- `--adapter=node|cf|netlify|vercel` — choose the hosting adapter (default: node).
|
|
40
46
|
- `--router=manifest|pages` — choose the routing system (default: manifest).
|
|
41
47
|
- `--template=minimal|tailwind` — non-interactive template selection; `minimal` is the default output, `tailwind` is minimal plus Tailwind CSS wiring.
|
|
42
48
|
- `--tailwind` / `--no-tailwind` — enable or disable Tailwind CSS without going through the prompt.
|
|
43
|
-
- `--agent-tools` / `--no-agent-tools` — seed the Claude Code skills
|
|
49
|
+
- `--agent-tools` / `--no-agent-tools` — seed the Claude Code skills, `.mcp.json`, and `AGENTS.md`/`CLAUDE.md` (or skip all of them) without going through the prompt.
|
|
44
50
|
- `--no-git` — skip `git init` and the initial commit.
|
|
45
51
|
- `--skip-install` — skip dependency installation.
|
|
46
52
|
- `--yes`, `-y` — accept defaults (node adapter, manifest router, no Tailwind, agent tooling on) and skip all prompts.
|
|
@@ -53,11 +59,14 @@ node ./packages/start/bin/create-pracht.js my-app --adapter=node --no-tailwind -
|
|
|
53
59
|
- `vite.config.ts`
|
|
54
60
|
- `src/routes.ts`
|
|
55
61
|
- `src/routes/home.tsx`
|
|
62
|
+
- `src/routes/not-found.tsx` — the app's 404 page, wired via `notFound` in the manifest (pages scaffolds get `src/pages/404.tsx`, which pracht wires automatically)
|
|
56
63
|
- `src/shells/public.tsx`
|
|
57
64
|
- `src/api/health.ts`
|
|
58
65
|
- `.gitignore`
|
|
59
66
|
- `.claude/skills/<name>/SKILL.md` — the pracht agent skills (unless `--no-agent-tools`)
|
|
60
67
|
- `.mcp.json` — registers the `pracht mcp` server for MCP clients (unless `--no-agent-tools`)
|
|
68
|
+
- `AGENTS.md` (plus a `CLAUDE.md` symlink pointing at it) — project conventions for coding
|
|
69
|
+
agents (unless `--no-agent-tools`; `README.md` documents the same commands for humans)
|
|
61
70
|
|
|
62
71
|
The skills are copied from the repo's [skills/](../../skills/README.md) directory into this
|
|
63
72
|
package at build/publish time (`scripts/sync-skills.js`), so the published npm tarball is
|
|
@@ -76,10 +85,17 @@ Cloudflare scaffolds also include:
|
|
|
76
85
|
|
|
77
86
|
- `wrangler.jsonc`
|
|
78
87
|
|
|
88
|
+
Standalone pnpm scaffolds for every adapter include `pnpm-workspace.yaml` with
|
|
89
|
+
the version-appropriate lifecycle policy. When the new app belongs to an
|
|
90
|
+
ancestor pnpm workspace, that workspace owns the policy instead: the generated
|
|
91
|
+
README and completion message list the exact entries to add and no nested
|
|
92
|
+
workspace file is created.
|
|
93
|
+
|
|
79
94
|
## Generated Scripts
|
|
80
95
|
|
|
81
96
|
- `dev` -> `pracht dev`
|
|
82
97
|
- `build` -> `pracht build`
|
|
98
|
+
- `typecheck` -> `tsc --noEmit`
|
|
83
99
|
|
|
84
100
|
Node starters also include:
|
|
85
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-auth
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.3
|
|
4
4
|
description: |
|
|
5
5
|
Find pracht routes that look protected but aren't — missing auth middleware,
|
|
6
6
|
middleware that augments context but never gates, client-side auth checks
|
|
@@ -91,9 +91,36 @@ target. From `pracht inspect api --json`:
|
|
|
91
91
|
serves ALL methods but reports `methods: []` — treat
|
|
92
92
|
`hasDefaultHandler: true` as "every method exposed". On older CLIs where
|
|
93
93
|
the field is missing, grep the handler file for `export default` instead.
|
|
94
|
-
- For each mutation handler (named method export or default handler)
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
- For each mutation handler (named method export or default handler) and each
|
|
95
|
+
HTTP- or remote-MCP-exposed capability, check whether
|
|
96
|
+
`defineApp({ api: { middleware } })` applies a Gate, OR the handler/capability
|
|
97
|
+
reads and validates a session itself. App-level API middleware wraps generated
|
|
98
|
+
capability endpoints before capability-specific middleware.
|
|
99
|
+
- For remote MCP, cookie-bearing transport requests are rejected before
|
|
100
|
+
capability dispatch and only `Authorization` is forwarded. Flag MCP-exposed
|
|
101
|
+
capabilities whose gate depends on a browser session cookie or a custom
|
|
102
|
+
credential header that the projection does not carry.
|
|
103
|
+
- Treat `context.agent` as framework-owned, read-only verified identity. Flag
|
|
104
|
+
middleware or capability code that attempts to mutate or replace it instead
|
|
105
|
+
of deriving application authorization state on a separate context field.
|
|
106
|
+
- When a custom adapter supplies a frozen or sealed context, flag authorization
|
|
107
|
+
helpers that read `agent` or middleware-added fields through `this`. The
|
|
108
|
+
framework binds private-field methods to the immutable source receiver, which
|
|
109
|
+
cannot observe fields added on its extensible overlay. Callable fields keep
|
|
110
|
+
their own API and arrays keep their brand. Application-defined
|
|
111
|
+
`Symbol.toStringTag` branding does not affect whether an ordinary context can
|
|
112
|
+
be overlaid, but immutable native built-ins such as `Map` and `Date` fail
|
|
113
|
+
closed because an overlay cannot preserve their internal slots. Use a fresh
|
|
114
|
+
mutable wrapper when a context needs native built-ins or when receiver-bound
|
|
115
|
+
helpers depend on request state.
|
|
116
|
+
- Inspect every HTTP-, WebMCP-, or MCP-exposed capability body for
|
|
117
|
+
`invokeCapability()`. Direct composition never re-applies app-level API
|
|
118
|
+
middleware. Remote MCP additionally re-applies the callee's `agentPolicy`
|
|
119
|
+
and refuses destructive callees, but private non-destructive capabilities
|
|
120
|
+
stay composable and rely on their named middleware for authorization. For
|
|
121
|
+
HTTP/WebMCP composition, flag sensitive callees whose required transport
|
|
122
|
+
authorization or approval is absent from the composing capability and the
|
|
123
|
+
callee's named middleware.
|
|
97
124
|
- Common bug: dashboard route is protected by middleware, but
|
|
98
125
|
`POST /api/items` is not — attacker bypasses the UI entirely.
|
|
99
126
|
|
|
@@ -140,5 +167,10 @@ Severity is the primary scale; the verdict is a secondary domain label:
|
|
|
140
167
|
4. Public routes deliberately exposed (login, signup, marketing) should be
|
|
141
168
|
listed but not flagged.
|
|
142
169
|
5. Do not auto-add middleware. Auth wiring is policy.
|
|
170
|
+
6. Treat allowed composed capability reachability as transitive. MCP blocks
|
|
171
|
+
destructive callees and re-applies `agentPolicy`; named middleware remains
|
|
172
|
+
the authorization seam for private non-destructive composition. Audit events
|
|
173
|
+
identify every nested attempt with `transport: "server"` and trusted request
|
|
174
|
+
provenance in `via`, but observability is not an authorization gate.
|
|
143
175
|
|
|
144
176
|
$ARGUMENTS
|
|
@@ -128,7 +128,8 @@ The canonical shape is in `recipes-auth.md` (the `origin-check.ts` example).
|
|
|
128
128
|
|
|
129
129
|
Verify the wiring: the middleware name must appear in
|
|
130
130
|
`defineApp({ api: { middleware: [...] } })` — that single global list applies
|
|
131
|
-
to every API route. There is no
|
|
131
|
+
to every API route and generated capability HTTP endpoint. There is no
|
|
132
|
+
per-group API middleware, and
|
|
132
133
|
`pracht inspect api --json` output has no middleware field, so the manifest is
|
|
133
134
|
the only place to check.
|
|
134
135
|
|
|
@@ -115,6 +115,12 @@ Check for accidental exposure outside loaders:
|
|
|
115
115
|
intentionally public, so flag client-side `VITE_*` references unless they are
|
|
116
116
|
explicitly allowlisted and reviewed. Warn loudly if a public env name has a
|
|
117
117
|
secret-shaped name.
|
|
118
|
+
- Flag any client-side read of `import.meta.env` that is not a single-key
|
|
119
|
+
access — a bare reference, destructuring, a spread, or bracket access such as
|
|
120
|
+
`const env = import.meta.env` or `import.meta.env["MODE"]`. Vite replaces
|
|
121
|
+
those with an object literal holding **every** exposed variable, so the
|
|
122
|
+
`VITE_*` values land in the bundle with no accessor text left for a
|
|
123
|
+
name-based grep to find. Use `publicEnv` to enumerate public values.
|
|
118
124
|
- Confirm server-side env access uses `serverEnv` (from
|
|
119
125
|
`@pracht/core/env/server`) or `context.env` rather than ad-hoc globals.
|
|
120
126
|
|
|
@@ -62,12 +62,18 @@ route("/pricing", () => import("./routes/pricing.tsx"), {
|
|
|
62
62
|
- `revalidate` accepts one policy or an array (`RouteRevalidate`); the array
|
|
63
63
|
above means "hourly, or sooner when a webhook names this path".
|
|
64
64
|
|
|
65
|
-
**Pages router
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
**Pages router:** time-based ISG is expressed with two static page exports:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export const RENDER_MODE = "isg";
|
|
69
|
+
export const REVALIDATE = 3600;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`REVALIDATE` must be a positive integer literal number of seconds. Build,
|
|
73
|
+
`doctor`, and `verify` reject a missing or misplaced policy. Pages mode does
|
|
74
|
+
not accept policies on `_app` or `404`, and fenced Markdown/MDX examples are
|
|
75
|
+
ignored. It does not support webhook or combined policies; eject with `generateRoutesFile`
|
|
76
|
+
from `@pracht/vite-plugin/pages-router` for those.
|
|
71
77
|
|
|
72
78
|
For dynamic routes, `getStaticPaths()` enumerates the prerendered params.
|
|
73
79
|
Paths it did not enumerate render per-request without a cached copy, and
|
|
@@ -89,9 +95,11 @@ curl -X POST https://example.com/__pracht/revalidate \
|
|
|
89
95
|
or wrong. Providers that can't send bearer auth may use the
|
|
90
96
|
`x-pracht-revalidate-token` header instead.
|
|
91
97
|
- Body: `paths` array, max 64 entries (else `400`). Response reports
|
|
92
|
-
`revalidated` / `skipped` / `failed` arrays
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
`revalidated` / `skipped` / `failed` arrays plus a `details` array naming why
|
|
99
|
+
each path was skipped (`not_a_route`, `not_isg`, `not_prerendered`,
|
|
100
|
+
`no_webhook_policy`) or failed — check `details` first when a webhook appears
|
|
101
|
+
to do nothing. Failed paths keep serving the previous copy. Regeneration is
|
|
102
|
+
single-flighted per path and never replays the caller's cookies/auth headers.
|
|
95
103
|
|
|
96
104
|
## Step 4: Adapter mechanics
|
|
97
105
|
|
|
@@ -100,7 +108,7 @@ curl -X POST https://example.com/__pracht/revalidate \
|
|
|
100
108
|
| Node | File mtime vs window; serves stale, refreshes in background | Regenerates the on-disk HTML synchronously |
|
|
101
109
|
| Cloudflare (default) | Worker-managed Cache API timestamp, `env.ASSETS` fallback — **per colo** | Overwrites the Cache API entry in the receiving colo only |
|
|
102
110
|
| Cloudflare (`cache: true`) | Edge-tier Workers Caching in front of the Worker for time-revalidated routes | Webhook-only routes keep the worker-managed path; time+webhook routes also get their edge entry purged |
|
|
103
|
-
| Vercel | Build Output prerender functions: `.prerender-config.json` with `expiration` from the time policy and build HTML as fallback | `x-vercel-cache
|
|
111
|
+
| Vercel | Build Output prerender functions: `.prerender-config.json` with `expiration` from the time policy and build HTML as fallback, next to a Node Serverless Function per ISG route (Vercel rejects ISR on an Edge Function) | For webhook revalidation, `x-vercel-cache` verifies the bypass; `PRACHT_REVALIDATE_TOKEN` becomes the `bypassToken` and **must be set at build time** (runtime-only setting → webhook paths report `failed` until you rebuild). Time-only ISR does not require the token. |
|
|
104
112
|
|
|
105
113
|
Cloudflare specifics (`docs/ADAPTERS.md#isg-via-workers-caching-cache`):
|
|
106
114
|
|
|
@@ -127,8 +135,9 @@ exact path **plus query string** (param order and trailing slash included), so
|
|
|
127
135
|
revalidation — and attacker-chosen query values create unbounded cold entries.
|
|
128
136
|
Before enabling `cache: true`, canonicalize or reject stray query params (the
|
|
129
137
|
docs describe an uncached-gateway pattern), and note that routes exporting
|
|
130
|
-
`markdown` carry `Vary: Accept`,
|
|
131
|
-
string. Vercel prerender functions are
|
|
138
|
+
`markdown` or declaring middleware-owned `markdown: true` carry `Vary: Accept`,
|
|
139
|
+
which multiplies variants per `Accept` string. Vercel prerender functions are
|
|
140
|
+
generated with `allowQuery: []`, so
|
|
132
141
|
query strings do not fragment that cache. Middleware never runs for cached ISG
|
|
133
142
|
hits on any adapter — keep per-visitor logic on SSR routes.
|
|
134
143
|
|
|
@@ -158,9 +167,15 @@ pracht typegen # if src/routes.ts changed
|
|
|
158
167
|
|
|
159
168
|
1. Never overwrite `wrangler.jsonc`/`wrangler.toml` or `vercel.json` — diff
|
|
160
169
|
and merge, confirming collisions with `AskUserQuestion`.
|
|
161
|
-
2. Never propose ISG for personalized responses
|
|
162
|
-
`
|
|
163
|
-
|
|
170
|
+
2. Never propose ISG for personalized responses. ISG HTML renders always run
|
|
171
|
+
on a sanitized request (`GET`, `Accept: text/html`, path only — no cookies,
|
|
172
|
+
credentials, query, or body); Cloudflare Workers Caching uses the same
|
|
173
|
+
isolation with `Accept: text/markdown` for its markdown cache variant. A
|
|
174
|
+
loader that reads the session therefore sees an anonymous visitor. On top of that, `Set-Cookie` or
|
|
175
|
+
`Cache-Control: private`/`no-store` output fails regeneration on Node and
|
|
176
|
+
Cloudflare (on Vercel the credential headers are stripped and the mismatch
|
|
177
|
+
is logged), and `Vary: Cookie`/`Authorization`/`*` is kept out of shared
|
|
178
|
+
caches by design.
|
|
164
179
|
3. Always pair `render: "isg"` with an explicit `revalidate` policy — without
|
|
165
180
|
one the route silently behaves like SSG.
|
|
166
181
|
4. On Vercel, set `PRACHT_REVALIDATE_TOKEN` in the build environment, not
|
|
@@ -53,7 +53,7 @@ If the source Next.js project uses the **pages router** (`pages/` directory), pr
|
|
|
53
53
|
2. Copy `pages/` to `src/pages/`
|
|
54
54
|
3. Convert `_app.tsx` to pracht shell format (`Shell` export + `children` prop)
|
|
55
55
|
4. Convert `getServerSideProps`/`getStaticProps` to `loader` exports
|
|
56
|
-
5. Add `export const RENDER_MODE = "ssg"` to static pages, `"ssr"` for dynamic (default is `"ssr"`)
|
|
56
|
+
5. Add `export const RENDER_MODE = "ssg"` to static pages, `"ssr"` for dynamic (default is `"ssr"`). For time-revalidated pages, export `RENDER_MODE = "isg"` and a positive integer `REVALIDATE` in seconds. Webhook policies require ejection.
|
|
57
57
|
6. Run dev server, iterate on errors
|
|
58
58
|
7. Optionally run `generateRoutesFile` to eject to explicit manifest
|
|
59
59
|
|
|
@@ -85,6 +85,7 @@ For pages router projects, you can **skip manual manifest wiring entirely** (Pha
|
|
|
85
85
|
| `useLinkStatus()` / pending UI | `useNavigation()` | `{ state, location, formData }` — powers progress bars and optimistic UI |
|
|
86
86
|
| `next/image` | `<Image>` from `@pracht/image` | Responsive srcsets plus Node, Cloudflare, Vercel, or passthrough loaders |
|
|
87
87
|
| `next/head` or Metadata API | `head()` export on route/shell | Per-route and per-shell head merging |
|
|
88
|
+
| `next/script` `<Script>` | `<Script>` from `@pracht/core` | Strategies: `beforeHydration` (≈ `beforeInteractive`), `afterHydration` (≈ `afterInteractive`, default), `idle` (≈ `lazyOnload`), `visible` |
|
|
88
89
|
| `className` | `class` | Preact uses `class` attribute |
|
|
89
90
|
| `React.useState` etc. | `import { useState } from "preact/hooks"` | Preact hooks API is compatible |
|
|
90
91
|
| `React.useEffect` | `import { useEffect } from "preact/hooks"` | Same API |
|
|
@@ -117,7 +118,7 @@ For pages router projects, you can **skip manual manifest wiring entirely** (Pha
|
|
|
117
118
|
3. Update `package.json`:
|
|
118
119
|
- Replace `react`, `react-dom` → `preact`
|
|
119
120
|
- Replace `next` → `@pracht/core` (framework runtime), `@pracht/cli` (provides the `pracht` bin), `@pracht/vite-plugin`, and `@pracht/adapter-node` (or target adapter). There is no package named `pracht`.
|
|
120
|
-
- If the app imports `next/image`, add `@pracht/image`; add `sharp` only for the built-in Node optimization endpoint.
|
|
121
|
+
- If the app imports `next/image`, add `@pracht/image`; add `sharp` only for the built-in Node optimization endpoint or build-time `?pracht` imports (static imports / blur placeholders).
|
|
121
122
|
- Update scripts: `dev` → `pracht dev`, `build` → `pracht build`, `start` → `node dist/server/server.js` (Node.js) or a platform-specific deploy command; add `preview` → `pracht preview` to serve the production build locally
|
|
122
123
|
4. Remove Next.js config files: `next.config.*`, `next-env.d.ts`, `.next/`
|
|
123
124
|
5. If `tsconfig.json` has `"jsx": "preserve"`, change to `"jsx": "react-jsx"` and add `"jsxImportSource": "preact"`.
|
|
@@ -415,6 +416,14 @@ Choose the loader for the deployment target:
|
|
|
415
416
|
Preserve the original `width`, `height`, `fill`, `sizes`, `quality`, and
|
|
416
417
|
priority intent. See `docs/IMAGES.md` for the endpoint and loader wiring.
|
|
417
418
|
|
|
419
|
+
Static imports and blur placeholders migrate too: replace
|
|
420
|
+
`import photo from "./photo.jpg"` with `import photo from "./photo.jpg?pracht"`,
|
|
421
|
+
add `prachtImage()` (from `@pracht/image/vite`) to the Vite plugins, reference
|
|
422
|
+
the `@pracht/image/client` types once in a `.d.ts`, and keep
|
|
423
|
+
`<Image src={photo} placeholder="blur" />` as-is — the import supplies
|
|
424
|
+
`width`/`height`/`blurDataURL` exactly like Next's static imports. Pracht's
|
|
425
|
+
blur is CSS-only (no fade animation, no inline event handlers).
|
|
426
|
+
|
|
418
427
|
#### `useRouter` → navigation
|
|
419
428
|
|
|
420
429
|
```tsx
|
|
@@ -3,10 +3,11 @@ name: pracht-deploy
|
|
|
3
3
|
version: 1.1.0
|
|
4
4
|
description: |
|
|
5
5
|
Pracht deployment guide. Walks through adapter configuration, building, and
|
|
6
|
-
deploying to Node.js, Cloudflare Workers, or Vercel. Handles
|
|
7
|
-
Docker and production checklist.
|
|
6
|
+
deploying to Node.js, Cloudflare Workers, Netlify, or Vercel. Handles platform
|
|
7
|
+
config, Docker and production checklist.
|
|
8
8
|
Use when asked to "deploy", "set up deployment", "configure adapter",
|
|
9
|
-
"deploy to cloudflare", "deploy to
|
|
9
|
+
"deploy to cloudflare", "deploy to netlify", "deploy to vercel", or
|
|
10
|
+
"production build".
|
|
10
11
|
allowed-tools:
|
|
11
12
|
- Bash
|
|
12
13
|
- Read
|
|
@@ -34,6 +35,7 @@ If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_build`
|
|
|
34
35
|
| ------------------ | ---------------------------- | ------ |
|
|
35
36
|
| Node.js | `@pracht/adapter-node` | Stable |
|
|
36
37
|
| Cloudflare Workers | `@pracht/adapter-cloudflare` | Stable |
|
|
38
|
+
| Netlify | `@pracht/adapter-netlify` | Stable |
|
|
37
39
|
| Vercel | `@pracht/adapter-vercel` | Stable |
|
|
38
40
|
|
|
39
41
|
---
|
|
@@ -47,9 +49,20 @@ If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_build`
|
|
|
47
49
|
```ts
|
|
48
50
|
import { pracht } from "@pracht/vite-plugin";
|
|
49
51
|
import { nodeAdapter } from "@pracht/adapter-node";
|
|
50
|
-
export default {
|
|
52
|
+
export default {
|
|
53
|
+
plugins: [
|
|
54
|
+
pracht({
|
|
55
|
+
adapter: nodeAdapter({ canonicalOrigin: "https://app.example.com" }),
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
};
|
|
51
59
|
```
|
|
52
60
|
|
|
61
|
+
Pin `canonicalOrigin` in production so `request.url` does not depend on the
|
|
62
|
+
incoming `Host` header. `maxBodySize` is also available on `nodeAdapter()`.
|
|
63
|
+
Only custom entries behind a trusted proxy that overwrites forwarded headers
|
|
64
|
+
should use `createNodeRequestHandler({ trustProxy: true })`.
|
|
65
|
+
|
|
53
66
|
### Build
|
|
54
67
|
|
|
55
68
|
```bash
|
|
@@ -105,6 +118,25 @@ npx wrangler deploy
|
|
|
105
118
|
|
|
106
119
|
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
120
|
|
|
121
|
+
Wrangler owns the Worker's binding environment. Put local-only secrets such as
|
|
122
|
+
`PRACHT_CONFIRMATION_SECRET` and `PRACHT_REVALIDATE_TOKEN` in a gitignored
|
|
123
|
+
`.dev.vars`; prefixing the host command with those variables does not
|
|
124
|
+
automatically expose them inside the Worker. Keep production values in
|
|
125
|
+
`wrangler secret`.
|
|
126
|
+
|
|
127
|
+
If the config contains a custom-domain route, preview can listen on localhost
|
|
128
|
+
while `request.url` inside the Worker uses the custom domain. Sign that
|
|
129
|
+
effective `@authority` for Web Bot Auth or temporarily disable the route. To use
|
|
130
|
+
a separate local config, build first, then run:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pracht build
|
|
134
|
+
npx wrangler dev --config wrangler.local.jsonc --port 3000
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The local config must keep `main: "dist/server/worker.js"` and omit the
|
|
138
|
+
production route. `pracht preview` does not forward Wrangler's `--config` flag.
|
|
139
|
+
|
|
108
140
|
### Wrangler Configuration
|
|
109
141
|
|
|
110
142
|
```jsonc
|
|
@@ -112,7 +144,7 @@ To smoke-test the built worker locally first, run `pracht preview` — it builds
|
|
|
112
144
|
{
|
|
113
145
|
"name": "my-pracht-app",
|
|
114
146
|
"main": "dist/server/worker.js",
|
|
115
|
-
"compatibility_date": "
|
|
147
|
+
"compatibility_date": "2026-04-06",
|
|
116
148
|
"assets": {
|
|
117
149
|
"binding": "ASSETS",
|
|
118
150
|
"directory": "dist/client",
|
|
@@ -132,12 +164,33 @@ export async function loader({ context }: LoaderArgs) {
|
|
|
132
164
|
}
|
|
133
165
|
```
|
|
134
166
|
|
|
167
|
+
Keep Cloudflare binding reads inside the loader, API handler, capability
|
|
168
|
+
`run()`, or another request-time function. Although Workers permits top-level
|
|
169
|
+
`env.MY_KV`, Pracht graph inspection intentionally fails such module-initializer
|
|
170
|
+
reads because it cannot supply an authoritative binding without risking false
|
|
171
|
+
graph metadata.
|
|
172
|
+
|
|
135
173
|
### Custom Assets Binding
|
|
136
174
|
|
|
137
175
|
```ts
|
|
138
176
|
pracht({ adapter: cloudflareAdapter({ assetsBinding: "STATIC" }) });
|
|
139
177
|
```
|
|
140
178
|
|
|
179
|
+
### Named bindings and default-export handlers
|
|
180
|
+
|
|
181
|
+
Durable Object and Workflow classes are named Worker exports. Re-export them
|
|
182
|
+
from the module configured with `workerExportsFrom`. Queue consumers, Cron
|
|
183
|
+
Triggers, and Email Routing are instead methods on the default export; expose
|
|
184
|
+
named `queue`, `scheduled`, or `email` functions from the module configured
|
|
185
|
+
with `workerHandlersFrom`:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
cloudflareAdapter({
|
|
189
|
+
workerExportsFrom: "/src/cloudflare.ts",
|
|
190
|
+
workerHandlersFrom: "/src/worker-handlers.ts",
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
141
194
|
### ISG via Workers Caching
|
|
142
195
|
|
|
143
196
|
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:
|
|
@@ -155,7 +208,8 @@ Before enabling it, audit ISG URLs for unbounded query strings. Workers Caching
|
|
|
155
208
|
keys the exact path and query string, including parameter order and trailing
|
|
156
209
|
slashes; use a bounded query allowlist/canonical redirect or an uncached gateway
|
|
157
210
|
with a pathname-only `cf.cacheKey`, and normalize `Accept` there for routes that
|
|
158
|
-
export markdown
|
|
211
|
+
export markdown or declare `markdown: true` for middleware-owned negotiation.
|
|
212
|
+
See `docs/ADAPTERS.md#cache-key-cardinality`.
|
|
159
213
|
|
|
160
214
|
Time-revalidated ISG pages then render on demand, are cached at the edge for
|
|
161
215
|
their `revalidate` window (stale pages served instantly while the Worker
|
|
@@ -165,6 +219,60 @@ build-time snapshots and the worker-managed path either way.
|
|
|
165
219
|
|
|
166
220
|
---
|
|
167
221
|
|
|
222
|
+
## Netlify Deployment
|
|
223
|
+
|
|
224
|
+
### Setup
|
|
225
|
+
|
|
226
|
+
1. Ensure `@pracht/adapter-netlify` and `netlify-cli` are installed.
|
|
227
|
+
2. In `vite.config.ts`:
|
|
228
|
+
```ts
|
|
229
|
+
import { pracht } from "@pracht/vite-plugin";
|
|
230
|
+
import { netlifyAdapter } from "@pracht/adapter-netlify";
|
|
231
|
+
export default { plugins: [pracht({ adapter: netlifyAdapter() })] };
|
|
232
|
+
```
|
|
233
|
+
3. Add `netlify.toml`:
|
|
234
|
+
```toml
|
|
235
|
+
[build]
|
|
236
|
+
command = "pnpm build"
|
|
237
|
+
publish = "dist/client"
|
|
238
|
+
|
|
239
|
+
[functions]
|
|
240
|
+
directory = "netlify/functions"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Build, Preview, and Deploy
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npx pracht build && npx netlify dev
|
|
247
|
+
npx netlify deploy --build --prod
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The build emits `netlify/functions/pracht.mjs`. Page requests go through that
|
|
251
|
+
function so Markdown negotiation and route-state requests remain correct;
|
|
252
|
+
hashed assets bypass it and stay outside the function bundle. The generated
|
|
253
|
+
config enumerates only client files the function can serve and roots matching
|
|
254
|
+
exclusions at the function file so Netlify's tracer cannot re-add bypassed
|
|
255
|
+
trees. Netlify durable caching
|
|
256
|
+
implements time-based ISG and per-path cache tags implement authenticated
|
|
257
|
+
webhook revalidation. A trailing-slash ISG document request permanently
|
|
258
|
+
redirects to the canonical slashless URL before rendering, and webhook
|
|
259
|
+
revalidation normalizes either spelling before purging the cache tag.
|
|
260
|
+
Only `Cache-Control`, `CDN-Cache-Control`, and `Netlify-CDN-Cache-Control`
|
|
261
|
+
override the adapter's cache defaults; provider-specific headers for another
|
|
262
|
+
CDN do not. Set a cache window to `0` to disable stale serving or freshness.
|
|
263
|
+
`Netlify-Vary` owns route-state variants, while the standard `Vary: Accept`
|
|
264
|
+
header owns Markdown negotiation. Cacheable negotiated SSG representations use
|
|
265
|
+
the same `Netlify-Vary` instructions as their prerendered HTML. Shared ISG
|
|
266
|
+
renders strip visitor-specific request data and Netlify context metadata before
|
|
267
|
+
loaders or context factories run.
|
|
268
|
+
|
|
269
|
+
`pracht preview` exits with guidance because it cannot emulate Netlify's
|
|
270
|
+
Functions and CDN behavior. Build the generated function before using
|
|
271
|
+
`netlify dev` for the platform-shaped local runtime. Configure
|
|
272
|
+
`PRACHT_REVALIDATE_TOKEN` in Netlify when webhook revalidation is enabled.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
168
276
|
## Vercel Deployment
|
|
169
277
|
|
|
170
278
|
### Setup
|
|
@@ -186,6 +294,15 @@ npx vercel deploy --prebuilt
|
|
|
186
294
|
|
|
187
295
|
Produces: `.vercel/output/config.json`, `.vercel/output/static/`, `.vercel/output/functions/render.func/server.js`
|
|
188
296
|
|
|
297
|
+
There is no faithful local Vercel production runtime, so `pracht preview`
|
|
298
|
+
exits with guidance. Use `vercel build` or `vercel dev`. Set
|
|
299
|
+
`PRACHT_REVALIDATE_TOKEN` at build time when using webhook revalidation; its
|
|
300
|
+
Vercel bypass token is embedded in `.prerender-config.json`. Rename the main
|
|
301
|
+
Edge Function with `vercelAdapter({ functionName })` if its default `render`
|
|
302
|
+
name would collide with an ISG route. Custom entries must export the
|
|
303
|
+
`nodeListener` created by `createVercelNodeListener(handle)` for Node ISR
|
|
304
|
+
functions.
|
|
305
|
+
|
|
189
306
|
---
|
|
190
307
|
|
|
191
308
|
## Deployment Checklist
|
|
@@ -201,7 +318,7 @@ Produces: `.vercel/output/config.json`, `.vercel/output/static/`, `.vercel/outpu
|
|
|
201
318
|
|
|
202
319
|
1. Read `vite.config.ts` and `package.json` before giving advice.
|
|
203
320
|
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`.
|
|
321
|
+
3. Smoke-test the production runtime before pushing to production. For Node.js and Cloudflare, run `pracht preview`; for Netlify, run `pracht build && netlify dev`.
|
|
205
322
|
4. If the user needs an adapter that isn't installed, help them add it (`pnpm add @pracht/adapter-*`).
|
|
206
323
|
5. Don't push to production without the user's explicit confirmation.
|
|
207
324
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pracht-test-api
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.0
|
|
4
4
|
description: |
|
|
5
5
|
Auto-generate Vitest request/response tests for every handler in `src/api/`.
|
|
6
6
|
Each test instantiates a `Request`, calls the exported HTTP method handler
|
|
@@ -53,34 +53,28 @@ Ask the user which subset to scaffold or accept paths via `$ARGUMENTS`.
|
|
|
53
53
|
## Step 3: Generate one test per handler file
|
|
54
54
|
|
|
55
55
|
Place tests next to the handler with `.test.ts` suffix
|
|
56
|
-
(`src/api/users/[id].test.ts`). Use
|
|
57
|
-
`ApiRouteArgs`:
|
|
56
|
+
(`src/api/users/[id].test.ts`). Use `createApiArgs()` from `@pracht/test`
|
|
57
|
+
(add it as a dev dependency if missing) to construct `ApiRouteArgs`:
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
60
|
import { describe, it, expect } from "vitest";
|
|
61
|
+
import { createApiArgs, readJson } from "@pracht/test";
|
|
61
62
|
import { GET, POST /* import only what the handler exports */ } from "./<file>";
|
|
62
63
|
|
|
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
64
|
describe("<METHOD> <api-path>", () => {
|
|
76
65
|
it("returns 200 on a valid request", async () => {
|
|
77
|
-
const res = await GET(
|
|
66
|
+
const res = await GET(createApiArgs({ url: "<api-path>" }));
|
|
78
67
|
expect(res).toBeInstanceOf(Response);
|
|
79
68
|
expect(res.status).toBe(200);
|
|
80
69
|
});
|
|
81
70
|
});
|
|
82
71
|
```
|
|
83
72
|
|
|
73
|
+
Pass `params` for dynamic segments, `body` for JSON payloads (plain objects
|
|
74
|
+
are JSON-encoded with the right `Content-Type`), and use `submitForm()` for
|
|
75
|
+
form-encoded/multipart POSTs — it drives the same `FormData` parsing path
|
|
76
|
+
`defineApi()` uses.
|
|
77
|
+
|
|
84
78
|
## Step 4: Generate method-specific cases
|
|
85
79
|
|
|
86
80
|
For each exported method, emit the smallest realistic case:
|
|
@@ -108,7 +102,7 @@ middleware is in that list. If it is, scaffold an extra test:
|
|
|
108
102
|
|
|
109
103
|
```ts
|
|
110
104
|
it("rejects unauthenticated requests", async () => {
|
|
111
|
-
const res = await POST(
|
|
105
|
+
const res = await POST(createApiArgs({ url: "<api-path>", method: "POST" }));
|
|
112
106
|
expect([401, 403, 302]).toContain(res.status);
|
|
113
107
|
});
|
|
114
108
|
```
|
|
@@ -127,9 +121,9 @@ For handlers that return `Response.json(...)`, generate:
|
|
|
127
121
|
|
|
128
122
|
```ts
|
|
129
123
|
it("returns JSON with the expected keys", async () => {
|
|
130
|
-
const res = await GET(
|
|
124
|
+
const res = await GET(createApiArgs({ url: "<api-path>" }));
|
|
131
125
|
expect(res.headers.get("content-type")).toMatch(/application\/json/);
|
|
132
|
-
const body = await res
|
|
126
|
+
const body = await readJson(res);
|
|
133
127
|
expect(body).toEqual(expect.objectContaining({ /* fill in */ }));
|
|
134
128
|
});
|
|
135
129
|
```
|
|
@@ -77,8 +77,9 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
77
77
|
deployment environment. List them for the user.
|
|
78
78
|
- If the app mounts `createImageHandler()` from `@pracht/image/node`, confirm
|
|
79
79
|
`sharp` is installed and `localOrigin` is the same trusted public origin as
|
|
80
|
-
`nodeAdapter({ canonicalOrigin })`. A
|
|
81
|
-
|
|
80
|
+
`nodeAdapter({ canonicalOrigin })`. A relative image endpoint without both
|
|
81
|
+
values is an error in every environment; loopback-looking request origins
|
|
82
|
+
are intentionally not trusted.
|
|
82
83
|
- Reverse-proxy / TLS termination configured (out of scope for this skill —
|
|
83
84
|
flag for confirmation).
|
|
84
85
|
|
|
@@ -92,7 +93,11 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
92
93
|
metadata (`buildTarget`, manifests, `resolvedApp`, ...) that `server.js`
|
|
93
94
|
exports for the prerender pass.
|
|
94
95
|
- `assets.directory` points to `dist/client`.
|
|
95
|
-
- `compatibility_date` is set and
|
|
96
|
+
- `compatibility_date` is set, and is a date the installed workerd supports.
|
|
97
|
+
It must not be *newer* than the runtime: workerd refuses to start with
|
|
98
|
+
"This Worker requires compatibility date X, but the newest date supported
|
|
99
|
+
by this server binary is Y". Never set it to today's date — that is by
|
|
100
|
+
construction at or beyond the newest released workerd.
|
|
96
101
|
- Bindings declared in wrangler config for every `context.env.*` access in
|
|
97
102
|
loaders, middleware, and API routes (grep, then cross-check).
|
|
98
103
|
- **No Node-only APIs in the server bundle.** Grep the server files for:
|
|
@@ -133,11 +138,19 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
133
138
|
- Required env vars are configured in the Vercel project (cannot verify from
|
|
134
139
|
CLI without `vercel env pull` — run that and diff against `process.env.*`
|
|
135
140
|
references).
|
|
136
|
-
- Edge runtime constraints:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
- Edge runtime constraints: the render function's `.vc-config.json` is
|
|
142
|
+
**always** written with `runtime: "edge"`, so run the same Node-only API
|
|
143
|
+
check as Cloudflare **unconditionally** for Vercel builds. Do not skip it
|
|
144
|
+
based on a runtime probe — ISG routes run the same bundle on Node, but any
|
|
145
|
+
Node-only API still breaks the edge function.
|
|
146
|
+
- ISG functions: every `<route>.prerender-config.json` must sit next to a
|
|
147
|
+
**Serverless** `<route>.func` (`.vc-config.json` with `launcherType:
|
|
148
|
+
"Nodejs"`). Vercel rejects a prerender config paired with an edge function:
|
|
149
|
+
`Unexpected function type "EdgeFunction" at path "<route>"`.
|
|
150
|
+
- Region configuration: `vercelAdapter({ regions: "all" })` is valid for the
|
|
151
|
+
Edge render function, but generated Node ISG function configs must omit
|
|
152
|
+
`regions` so the project's default Serverless region applies. Node configs
|
|
153
|
+
may only contain arrays of concrete region identifiers.
|
|
141
154
|
- An API route importing `@pracht/image/node` is an error for the Vercel Edge
|
|
142
155
|
function. Require `vercelLoader` (with aligned allowed sizes) or
|
|
143
156
|
`passthroughLoader` instead.
|