create-pracht 0.4.2 → 0.6.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 +24 -6
- package/package.json +1 -1
- package/skills/add-i18n/SKILL.md +308 -151
- package/skills/audit-auth/SKILL.md +32 -2
- package/skills/audit-headers/SKILL.md +14 -5
- package/skills/audit-secrets/SKILL.md +7 -4
- package/skills/configure-isg/SKILL.md +37 -18
- package/skills/migrate-nextjs/SKILL.md +17 -7
- package/skills/pracht-debug/SKILL.md +1 -1
- package/skills/pracht-deploy/SKILL.md +227 -8
- package/skills/pracht-scaffold/SKILL.md +11 -1
- package/skills/pracht-test-api/SKILL.md +13 -19
- package/skills/pre-deploy/SKILL.md +92 -15
- package/skills/scaffold-tests/SKILL.md +34 -50
- package/skills/tune-render-mode/SKILL.md +4 -1
- package/src/index.js +583 -42
|
@@ -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
|
|
@@ -92,10 +92,35 @@ target. From `pracht inspect api --json`:
|
|
|
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
94
|
- For each mutation handler (named method export or default handler) and each
|
|
95
|
-
HTTP-exposed capability, check whether
|
|
95
|
+
HTTP- or remote-MCP-exposed capability, check whether
|
|
96
96
|
`defineApp({ api: { middleware } })` applies a Gate, OR the handler/capability
|
|
97
97
|
reads and validates a session itself. App-level API middleware wraps generated
|
|
98
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.
|
|
99
124
|
- Common bug: dashboard route is protected by middleware, but
|
|
100
125
|
`POST /api/items` is not — attacker bypasses the UI entirely.
|
|
101
126
|
|
|
@@ -142,5 +167,10 @@ Severity is the primary scale; the verdict is a secondary domain label:
|
|
|
142
167
|
4. Public routes deliberately exposed (login, signup, marketing) should be
|
|
143
168
|
listed but not flagged.
|
|
144
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.
|
|
145
175
|
|
|
146
176
|
$ARGUMENTS
|
|
@@ -49,8 +49,11 @@ The audit surface is therefore:
|
|
|
49
49
|
- **(c)** HSTS and CSP, which genuinely need user action.
|
|
50
50
|
|
|
51
51
|
Prerequisites: `pracht inspect` requires a vite config that registers the
|
|
52
|
-
pracht plugin; `pracht inspect build` and
|
|
53
|
-
require a prior `pracht build`.
|
|
52
|
+
pracht plugin; `pracht inspect build` and
|
|
53
|
+
`dist/server/headers-manifest.json` require a prior `pracht build`. Every
|
|
54
|
+
serverful target currently publishes a client copy at
|
|
55
|
+
`dist/client/_pracht/headers.json`; only Cloudflare reads it there. Pure static
|
|
56
|
+
exports deliberately do not publish that copy.
|
|
54
57
|
|
|
55
58
|
## Step 1: Inventory header sources
|
|
56
59
|
|
|
@@ -134,9 +137,15 @@ the `headers()` sources statically to catch them **before** a build failure,
|
|
|
134
137
|
and report each as `error` with the prerender failure it would cause.
|
|
135
138
|
|
|
136
139
|
The real target is the `warn` class the framework cannot catch: innocuously
|
|
137
|
-
named headers carrying user-specific values
|
|
138
|
-
`dist/
|
|
139
|
-
users on static responses.
|
|
140
|
+
named headers carrying user-specific values in
|
|
141
|
+
`dist/server/headers-manifest.json`, which serverful adapters may replay across
|
|
142
|
+
users on static responses. Every serverful build currently also copies that
|
|
143
|
+
manifest into public client output; Cloudflare reads it through the assets
|
|
144
|
+
binding, while the other adapters retain the public copy for compatibility. A
|
|
145
|
+
pure static export omits the client copy and has no runtime to replay the server
|
|
146
|
+
manifest; instead, verify that the deployment's host-header configuration
|
|
147
|
+
mirrors only safe, non-user-specific entries. Missing host configuration means
|
|
148
|
+
route `headers()` values are not applied at all.
|
|
140
149
|
|
|
141
150
|
Secret VALUES in headers are owned by `audit-secrets`; this skill owns policy
|
|
142
151
|
headers. Cross-reference `audit-secrets` for value-level findings.
|
|
@@ -95,10 +95,13 @@ Check for accidental exposure outside loaders:
|
|
|
95
95
|
|
|
96
96
|
- `head()` returns: rare, but a `meta` value containing a token leaks into HTML.
|
|
97
97
|
- `headers()` returns: flag values that look like secrets. For SSG/ISG pages,
|
|
98
|
-
document headers
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
document headers enter `dist/server/headers-manifest.json`; every serverful
|
|
99
|
+
build currently also copies that manifest to public
|
|
100
|
+
`dist/client/_pracht/headers.json` (only Cloudflare reads it there). A pure
|
|
101
|
+
static export omits the client copy but may mirror the server manifest into
|
|
102
|
+
host configuration. This skill owns secret VALUES in headers; header policy
|
|
103
|
+
(CSP, HSTS, weakened defaults) is owned by `audit-headers` — cross-reference
|
|
104
|
+
it.
|
|
102
105
|
- `<Form>` `action` URLs containing tokens in the query string.
|
|
103
106
|
- `prefetchRouteState(url)` calls with sensitive query params.
|
|
104
107
|
- Inline `<script>` content emitted from custom shells.
|
|
@@ -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
|
|
@@ -75,23 +81,29 @@ webhooks naming them are `skipped` on Node/Cloudflare (nothing to refresh).
|
|
|
75
81
|
|
|
76
82
|
## Step 3: The revalidation webhook
|
|
77
83
|
|
|
78
|
-
All adapters expose `POST
|
|
79
|
-
from `@pracht/core`)
|
|
84
|
+
All adapters expose `POST <base>/__pracht/revalidate`
|
|
85
|
+
(`PRACHT_REVALIDATE_ENDPOINT` from `@pracht/core`). For example, an app with
|
|
86
|
+
`base: "/app/"` uses:
|
|
80
87
|
|
|
81
88
|
```sh
|
|
82
|
-
curl -X POST https://example.com/__pracht/revalidate \
|
|
89
|
+
curl -X POST https://example.com/app/__pracht/revalidate \
|
|
83
90
|
-H "Authorization: Bearer $PRACHT_REVALIDATE_TOKEN" \
|
|
84
91
|
-H "Content-Type: application/json" \
|
|
85
92
|
-d '{"paths":["/pricing"]}'
|
|
86
93
|
```
|
|
87
94
|
|
|
95
|
+
At the default `base: "/"`, omit `/app`. Keep request-body paths base-free;
|
|
96
|
+
they identify manifest routes rather than public deployment URLs.
|
|
97
|
+
|
|
88
98
|
- Auth: `PRACHT_REVALIDATE_TOKEN` env var; fails closed with `401` when unset
|
|
89
99
|
or wrong. Providers that can't send bearer auth may use the
|
|
90
100
|
`x-pracht-revalidate-token` header instead.
|
|
91
101
|
- Body: `paths` array, max 64 entries (else `400`). Response reports
|
|
92
|
-
`revalidated` / `skipped` / `failed` arrays
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
`revalidated` / `skipped` / `failed` arrays plus a `details` array naming why
|
|
103
|
+
each path was skipped (`not_a_route`, `not_isg`, `not_prerendered`,
|
|
104
|
+
`no_webhook_policy`) or failed — check `details` first when a webhook appears
|
|
105
|
+
to do nothing. Failed paths keep serving the previous copy. Regeneration is
|
|
106
|
+
single-flighted per path and never replays the caller's cookies/auth headers.
|
|
95
107
|
|
|
96
108
|
## Step 4: Adapter mechanics
|
|
97
109
|
|
|
@@ -100,7 +112,7 @@ curl -X POST https://example.com/__pracht/revalidate \
|
|
|
100
112
|
| Node | File mtime vs window; serves stale, refreshes in background | Regenerates the on-disk HTML synchronously |
|
|
101
113
|
| Cloudflare (default) | Worker-managed Cache API timestamp, `env.ASSETS` fallback — **per colo** | Overwrites the Cache API entry in the receiving colo only |
|
|
102
114
|
| 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
|
|
115
|
+
| 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
116
|
|
|
105
117
|
Cloudflare specifics (`docs/ADAPTERS.md#isg-via-workers-caching-cache`):
|
|
106
118
|
|
|
@@ -127,8 +139,9 @@ exact path **plus query string** (param order and trailing slash included), so
|
|
|
127
139
|
revalidation — and attacker-chosen query values create unbounded cold entries.
|
|
128
140
|
Before enabling `cache: true`, canonicalize or reject stray query params (the
|
|
129
141
|
docs describe an uncached-gateway pattern), and note that routes exporting
|
|
130
|
-
`markdown` carry `Vary: Accept`,
|
|
131
|
-
string. Vercel prerender functions are
|
|
142
|
+
`markdown` or declaring middleware-owned `markdown: true` carry `Vary: Accept`,
|
|
143
|
+
which multiplies variants per `Accept` string. Vercel prerender functions are
|
|
144
|
+
generated with `allowQuery: []`, so
|
|
132
145
|
query strings do not fragment that cache. Middleware never runs for cached ISG
|
|
133
146
|
hits on any adapter — keep per-visitor logic on SSR routes.
|
|
134
147
|
|
|
@@ -158,9 +171,15 @@ pracht typegen # if src/routes.ts changed
|
|
|
158
171
|
|
|
159
172
|
1. Never overwrite `wrangler.jsonc`/`wrangler.toml` or `vercel.json` — diff
|
|
160
173
|
and merge, confirming collisions with `AskUserQuestion`.
|
|
161
|
-
2. Never propose ISG for personalized responses
|
|
162
|
-
`
|
|
163
|
-
|
|
174
|
+
2. Never propose ISG for personalized responses. ISG HTML renders always run
|
|
175
|
+
on a sanitized request (`GET`, `Accept: text/html`, path only — no cookies,
|
|
176
|
+
credentials, query, or body); Cloudflare Workers Caching uses the same
|
|
177
|
+
isolation with `Accept: text/markdown` for its markdown cache variant. A
|
|
178
|
+
loader that reads the session therefore sees an anonymous visitor. On top of that, `Set-Cookie` or
|
|
179
|
+
`Cache-Control: private`/`no-store` output fails regeneration on Node and
|
|
180
|
+
Cloudflare (on Vercel the credential headers are stripped and the mismatch
|
|
181
|
+
is logged), and `Vary: Cookie`/`Authorization`/`*` is kept out of shared
|
|
182
|
+
caches by design.
|
|
164
183
|
3. Always pair `render: "isg"` with an explicit `revalidate` policy — without
|
|
165
184
|
one the route silently behaves like SSG.
|
|
166
185
|
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
|
|
|
@@ -78,13 +78,14 @@ For pages router projects, you can **skip manual manifest wiring entirely** (Pha
|
|
|
78
78
|
| `"use client"` (few, in a mostly-server app) | `hydration: "islands"` + `src/islands/` | Only islands ship JS; see the islands note in Phase 4 |
|
|
79
79
|
| `revalidatePath` / `res.revalidate()` | `webhookRevalidate()` + `POST /__pracht/revalidate` | On-demand ISG regeneration; combinable with `timeRevalidate(seconds)` |
|
|
80
80
|
| `useRouter()` (next/navigation) | `useNavigate()` from pracht | Accepts paths or typed route targets after `pracht typegen` |
|
|
81
|
-
| `useSearchParams()` | `
|
|
81
|
+
| `useSearchParams()` | `useSearchParams()` from pracht | Returns reactive read-only params; SSG receives the browser query after hydration, while loaders use `url.searchParams` |
|
|
82
82
|
| `useParams()` | `useParams()` from pracht | Direct equivalent; also available as `params` in loader args |
|
|
83
83
|
| `next/link` `<Link>` | `<Link route="...">` or plain `<a>` | Prefer typed `<Link>` for known app routes after `pracht typegen`; plain anchors still work |
|
|
84
84
|
| `next/link` `prefetch={false}` | `<Link prefetch="none">` | Pracht prefetches on hover/focus by default; also `"viewport"`, `"render"` |
|
|
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
|
|
@@ -443,13 +452,13 @@ async function createPost(formData: FormData) {
|
|
|
443
452
|
}
|
|
444
453
|
|
|
445
454
|
// Pracht — API route handler
|
|
446
|
-
import type
|
|
455
|
+
import { withBase, type ApiRouteArgs } from "@pracht/core";
|
|
447
456
|
|
|
448
457
|
export async function POST({ request }: ApiRouteArgs) {
|
|
449
458
|
const form = await request.formData();
|
|
450
459
|
await db.insert({ title: form.get("title") });
|
|
451
460
|
// revalidatePath("/posts") equivalent: regenerate the ISG page on demand
|
|
452
|
-
await fetch(new URL("/__pracht/revalidate", request.url), {
|
|
461
|
+
await fetch(new URL(withBase("/__pracht/revalidate"), request.url), {
|
|
453
462
|
method: "POST",
|
|
454
463
|
headers: {
|
|
455
464
|
authorization: `Bearer ${process.env.PRACHT_REVALIDATE_TOKEN}`,
|
|
@@ -459,7 +468,7 @@ export async function POST({ request }: ApiRouteArgs) {
|
|
|
459
468
|
});
|
|
460
469
|
return new Response(null, {
|
|
461
470
|
status: 303,
|
|
462
|
-
headers: { location: "/posts" },
|
|
471
|
+
headers: { location: withBase("/posts") },
|
|
463
472
|
});
|
|
464
473
|
}
|
|
465
474
|
```
|
|
@@ -502,7 +511,8 @@ export async function loader({ request }: LoaderArgs) {
|
|
|
502
511
|
| `next/image` | `@pracht/image` |
|
|
503
512
|
| `react` | `preact` |
|
|
504
513
|
| `react-dom` | `preact` |
|
|
505
|
-
|
|
|
514
|
+
| `next/font/local` | `defineFont()` from `@pracht/core` — register via `head() { return { fonts: [font] } }`, use `font.className`/`font.style` in components |
|
|
515
|
+
| `next/font/google` | Download the woff2 files into `public/fonts/` (e.g. via google-webfonts-helper), then `defineFont()` — pracht never fetches fonts at build time |
|
|
506
516
|
| `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
|
|
507
517
|
| `next-auth` | Direct integration in middleware/loaders |
|
|
508
518
|
| `next/og` | `@vercel/og` or custom solution |
|
|
@@ -28,7 +28,7 @@ The user will describe a symptom (error, unexpected behavior, blank page, etc.).
|
|
|
28
28
|
Before deep manual inspection, prefer running `pracht verify` (add `--changed` to scope the checks to git-changed files) for a fast agent loop or `pracht doctor` when the problem could be caused by broader broken app wiring or missing files.
|
|
29
29
|
When another agent/tool needs the framework's resolved graph, prefer `pracht inspect routes --json`, `pracht inspect api --json`, or `pracht inspect build --json` over reconstructing it from source files. Prerequisites: `pracht inspect` needs the pracht plugin registered in the project's vite config, and `pracht inspect build` needs a prior `pracht build`.
|
|
30
30
|
If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_routes`/`inspect_api`/`doctor`/`verify` MCP tools over shelling out — same payloads, structured results.
|
|
31
|
-
While the dev server is running, `GET /_pracht` serves a devtools page with the same resolved route/API graph (raw JSON at `/_pracht.json`) — useful when you have a browser or `curl` handy but no CLI access. Dev SSR responses also carry a `Server-Timing` header (`mw`, `loader`, `render` durations in ms) — check it in the browser Network panel or with `curl -sI` to see which phase makes a route slow.
|
|
31
|
+
While the dev server is running, `GET /_pracht` serves a devtools page with the same resolved route/API graph (raw JSON at `/_pracht.json`) — useful when you have a browser or `curl` handy but no CLI access. Under a Vite deploy base, prefix both paths with that base; links from the devtools and dev-404 pages already do so. Dev SSR responses also carry a `Server-Timing` header (`mw`, `loader`, `render` durations in ms) — check it in the browser Network panel or with `curl -sI` to see which phase makes a route slow.
|
|
32
32
|
|
|
33
33
|
## Iron Law
|
|
34
34
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pracht-deploy
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.0
|
|
4
4
|
description: |
|
|
5
5
|
Pracht deployment guide. Walks through adapter configuration, building, and
|
|
6
|
-
deploying to Node.js, Cloudflare Workers,
|
|
7
|
-
Docker and production checklist.
|
|
6
|
+
deploying to Node.js, Cloudflare Workers, Netlify, Vercel, or a pure static
|
|
7
|
+
host. Handles platform 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", "static
|
|
10
|
+
export", or
|
|
11
|
+
"production build".
|
|
10
12
|
allowed-tools:
|
|
11
13
|
- Bash
|
|
12
14
|
- Read
|
|
@@ -34,7 +36,9 @@ If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_build`
|
|
|
34
36
|
| ------------------ | ---------------------------- | ------ |
|
|
35
37
|
| Node.js | `@pracht/adapter-node` | Stable |
|
|
36
38
|
| Cloudflare Workers | `@pracht/adapter-cloudflare` | Stable |
|
|
39
|
+
| Netlify | `@pracht/adapter-netlify` | Stable |
|
|
37
40
|
| Vercel | `@pracht/adapter-vercel` | Stable |
|
|
41
|
+
| Static export | `@pracht/adapter-static` | Stable |
|
|
38
42
|
|
|
39
43
|
---
|
|
40
44
|
|
|
@@ -47,9 +51,35 @@ If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_build`
|
|
|
47
51
|
```ts
|
|
48
52
|
import { pracht } from "@pracht/vite-plugin";
|
|
49
53
|
import { nodeAdapter } from "@pracht/adapter-node";
|
|
50
|
-
export default {
|
|
54
|
+
export default {
|
|
55
|
+
plugins: [
|
|
56
|
+
pracht({
|
|
57
|
+
adapter: nodeAdapter({ canonicalOrigin: "https://app.example.com" }),
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
51
61
|
```
|
|
52
62
|
|
|
63
|
+
Pin `canonicalOrigin` in production so `request.url` does not depend on the
|
|
64
|
+
incoming `Host` header. `maxBodySize` is also available on `nodeAdapter()`.
|
|
65
|
+
Only custom entries behind a trusted proxy that overwrites forwarded headers
|
|
66
|
+
should use `createNodeRequestHandler({ trustProxy: true })`.
|
|
67
|
+
If that proxy strips Vite's deploy base from the forwarded path, set
|
|
68
|
+
`nodeAdapter({ basePathStripped: true })` (or the same option on a custom
|
|
69
|
+
`createNodeRequestHandler`). Do not infer this from the first path segment: a
|
|
70
|
+
route may legitimately begin with the same segment as the deploy base. The
|
|
71
|
+
adapter restores the public base before `createContext()`, loaders, and API
|
|
72
|
+
handlers receive the request.
|
|
73
|
+
The proxy must also own the public bare-base redirect (`/app` to `/app/`) in
|
|
74
|
+
this mode because the stripped origin cannot distinguish it from a legitimate
|
|
75
|
+
base-free `/app` route.
|
|
76
|
+
|
|
77
|
+
The Node adapter compresses responses by default (brotli/gzip negotiated via
|
|
78
|
+
`Accept-Encoding`, streaming for dynamic bodies, an in-memory LRU for static
|
|
79
|
+
assets). When the deployment sits behind a reverse proxy or CDN that already
|
|
80
|
+
compresses responses, set `nodeAdapter({ compression: false })` so bodies are
|
|
81
|
+
not compressed twice.
|
|
82
|
+
|
|
53
83
|
### Build
|
|
54
84
|
|
|
55
85
|
```bash
|
|
@@ -105,6 +135,25 @@ npx wrangler deploy
|
|
|
105
135
|
|
|
106
136
|
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
137
|
|
|
138
|
+
Wrangler owns the Worker's binding environment. Put local-only secrets such as
|
|
139
|
+
`PRACHT_CONFIRMATION_SECRET` and `PRACHT_REVALIDATE_TOKEN` in a gitignored
|
|
140
|
+
`.dev.vars`; prefixing the host command with those variables does not
|
|
141
|
+
automatically expose them inside the Worker. Keep production values in
|
|
142
|
+
`wrangler secret`.
|
|
143
|
+
|
|
144
|
+
If the config contains a custom-domain route, preview can listen on localhost
|
|
145
|
+
while `request.url` inside the Worker uses the custom domain. Sign that
|
|
146
|
+
effective `@authority` for Web Bot Auth or temporarily disable the route. To use
|
|
147
|
+
a separate local config, build first, then run:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pracht build
|
|
151
|
+
npx wrangler dev --config wrangler.local.jsonc --port 3000
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The local config must keep `main: "dist/server/worker.js"` and omit the
|
|
155
|
+
production route. `pracht preview` does not forward Wrangler's `--config` flag.
|
|
156
|
+
|
|
108
157
|
### Wrangler Configuration
|
|
109
158
|
|
|
110
159
|
```jsonc
|
|
@@ -112,7 +161,7 @@ To smoke-test the built worker locally first, run `pracht preview` — it builds
|
|
|
112
161
|
{
|
|
113
162
|
"name": "my-pracht-app",
|
|
114
163
|
"main": "dist/server/worker.js",
|
|
115
|
-
"compatibility_date": "
|
|
164
|
+
"compatibility_date": "2026-04-06",
|
|
116
165
|
"assets": {
|
|
117
166
|
"binding": "ASSETS",
|
|
118
167
|
"directory": "dist/client",
|
|
@@ -132,12 +181,33 @@ export async function loader({ context }: LoaderArgs) {
|
|
|
132
181
|
}
|
|
133
182
|
```
|
|
134
183
|
|
|
184
|
+
Keep Cloudflare binding reads inside the loader, API handler, capability
|
|
185
|
+
`run()`, or another request-time function. Although Workers permits top-level
|
|
186
|
+
`env.MY_KV`, Pracht graph inspection intentionally fails such module-initializer
|
|
187
|
+
reads because it cannot supply an authoritative binding without risking false
|
|
188
|
+
graph metadata.
|
|
189
|
+
|
|
135
190
|
### Custom Assets Binding
|
|
136
191
|
|
|
137
192
|
```ts
|
|
138
193
|
pracht({ adapter: cloudflareAdapter({ assetsBinding: "STATIC" }) });
|
|
139
194
|
```
|
|
140
195
|
|
|
196
|
+
### Named bindings and default-export handlers
|
|
197
|
+
|
|
198
|
+
Durable Object and Workflow classes are named Worker exports. Re-export them
|
|
199
|
+
from the module configured with `workerExportsFrom`. Queue consumers, Cron
|
|
200
|
+
Triggers, and Email Routing are instead methods on the default export; expose
|
|
201
|
+
named `queue`, `scheduled`, or `email` functions from the module configured
|
|
202
|
+
with `workerHandlersFrom`:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
cloudflareAdapter({
|
|
206
|
+
workerExportsFrom: "/src/cloudflare.ts",
|
|
207
|
+
workerHandlersFrom: "/src/worker-handlers.ts",
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
141
211
|
### ISG via Workers Caching
|
|
142
212
|
|
|
143
213
|
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 +225,8 @@ Before enabling it, audit ISG URLs for unbounded query strings. Workers Caching
|
|
|
155
225
|
keys the exact path and query string, including parameter order and trailing
|
|
156
226
|
slashes; use a bounded query allowlist/canonical redirect or an uncached gateway
|
|
157
227
|
with a pathname-only `cf.cacheKey`, and normalize `Accept` there for routes that
|
|
158
|
-
export markdown
|
|
228
|
+
export markdown or declare `markdown: true` for middleware-owned negotiation.
|
|
229
|
+
See `docs/ADAPTERS.md#cache-key-cardinality`.
|
|
159
230
|
|
|
160
231
|
Time-revalidated ISG pages then render on demand, are cached at the edge for
|
|
161
232
|
their `revalidate` window (stale pages served instantly while the Worker
|
|
@@ -165,6 +236,64 @@ build-time snapshots and the worker-managed path either way.
|
|
|
165
236
|
|
|
166
237
|
---
|
|
167
238
|
|
|
239
|
+
## Netlify Deployment
|
|
240
|
+
|
|
241
|
+
### Setup
|
|
242
|
+
|
|
243
|
+
1. Ensure `@pracht/adapter-netlify` and `netlify-cli` are installed.
|
|
244
|
+
2. In `vite.config.ts`:
|
|
245
|
+
```ts
|
|
246
|
+
import { pracht } from "@pracht/vite-plugin";
|
|
247
|
+
import { netlifyAdapter } from "@pracht/adapter-netlify";
|
|
248
|
+
export default { plugins: [pracht({ adapter: netlifyAdapter() })] };
|
|
249
|
+
```
|
|
250
|
+
3. Add `netlify.toml`:
|
|
251
|
+
```toml
|
|
252
|
+
[build]
|
|
253
|
+
command = "pnpm build"
|
|
254
|
+
publish = "dist/client"
|
|
255
|
+
|
|
256
|
+
[functions]
|
|
257
|
+
directory = "netlify/functions"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Build, Preview, and Deploy
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npx pracht build && npx netlify dev
|
|
264
|
+
npx netlify deploy --build --prod
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The build emits `netlify/functions/pracht.mjs`. Page requests go through that
|
|
268
|
+
function so Markdown negotiation and route-state requests remain correct;
|
|
269
|
+
hashed assets bypass it and stay outside the function bundle at the origin
|
|
270
|
+
root. With a Vite deploy base, the function instead bundles and serves the
|
|
271
|
+
base-free asset and `/_pracht` trees so `/app/...` requests remain inside the
|
|
272
|
+
mount. Custom `excludedPath` entries still bypass their literal origin-root
|
|
273
|
+
URLs, but matching files remain bundled for base-prefixed requests. The
|
|
274
|
+
generated config enumerates only client files the function can serve and roots
|
|
275
|
+
applicable exclusions at the function file so Netlify's tracer cannot re-add
|
|
276
|
+
bypassed trees. Netlify durable caching
|
|
277
|
+
implements time-based ISG and per-path cache tags implement authenticated
|
|
278
|
+
webhook revalidation. A trailing-slash ISG document request permanently
|
|
279
|
+
redirects to the canonical slashless URL before rendering, and webhook
|
|
280
|
+
revalidation normalizes either spelling before purging the cache tag.
|
|
281
|
+
Only `Cache-Control`, `CDN-Cache-Control`, and `Netlify-CDN-Cache-Control`
|
|
282
|
+
override the adapter's cache defaults; provider-specific headers for another
|
|
283
|
+
CDN do not. Set a cache window to `0` to disable stale serving or freshness.
|
|
284
|
+
`Netlify-Vary` owns route-state variants, while the standard `Vary: Accept`
|
|
285
|
+
header owns Markdown negotiation. Cacheable negotiated SSG representations use
|
|
286
|
+
the same `Netlify-Vary` instructions as their prerendered HTML. Shared ISG
|
|
287
|
+
renders strip visitor-specific request data and Netlify context metadata before
|
|
288
|
+
loaders or context factories run.
|
|
289
|
+
|
|
290
|
+
`pracht preview` exits with guidance because it cannot emulate Netlify's
|
|
291
|
+
Functions and CDN behavior. Build the generated function before using
|
|
292
|
+
`netlify dev` for the platform-shaped local runtime. Configure
|
|
293
|
+
`PRACHT_REVALIDATE_TOKEN` in Netlify when webhook revalidation is enabled.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
168
297
|
## Vercel Deployment
|
|
169
298
|
|
|
170
299
|
### Setup
|
|
@@ -186,6 +315,96 @@ npx vercel deploy --prebuilt
|
|
|
186
315
|
|
|
187
316
|
Produces: `.vercel/output/config.json`, `.vercel/output/static/`, `.vercel/output/functions/render.func/server.js`
|
|
188
317
|
|
|
318
|
+
There is no faithful local Vercel production runtime, so `pracht preview`
|
|
319
|
+
exits with guidance. Use `vercel build` or `vercel dev`. Set
|
|
320
|
+
`PRACHT_REVALIDATE_TOKEN` at build time when using webhook revalidation; its
|
|
321
|
+
Vercel bypass token is embedded in `.prerender-config.json`. Rename the main
|
|
322
|
+
Edge Function with `vercelAdapter({ functionName })` if its default `render`
|
|
323
|
+
name would collide with an ISG route. Custom entries must export the
|
|
324
|
+
`nodeListener` created by `createVercelNodeListener(handle)` for Node ISR
|
|
325
|
+
functions.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Static Export Deployment
|
|
330
|
+
|
|
331
|
+
For apps where every route is `render: "ssg"` (or loaderless, full-hydration
|
|
332
|
+
`"spa"`), with no
|
|
333
|
+
request middleware, API routes, or HTTP/MCP/WebMCP-exposed capabilities. SSG
|
|
334
|
+
loaders run only at build time and must produce HTML plus valid JSON route
|
|
335
|
+
state; dynamic SSG routes must export `getStaticPaths()`. Anything else fails the build with an error naming the
|
|
336
|
+
offenders — that is the signal to pick a serverful adapter instead. Only
|
|
337
|
+
manifest-registered capabilities participate; every registered capability
|
|
338
|
+
module must load successfully so exposure validation can fail closed. The
|
|
339
|
+
`notFound` page must use full hydration (the default), because the shared
|
|
340
|
+
`404.html` needs the client router to adopt the visitor's actual URL. Sub-path
|
|
341
|
+
deploys (GitHub Pages *project* sites, S3 key prefixes) set Vite `base` to that
|
|
342
|
+
path; CDN and document-relative bases (`""` / `"./"`) are build errors,
|
|
343
|
+
because they split assets from the deploy root or resolve them beneath nested
|
|
344
|
+
page directories. Under a base,
|
|
345
|
+
internal navigation must go through `<Link route>` / `href()` — a hand-written
|
|
346
|
+
`<a href="/about">` still means the origin root.
|
|
347
|
+
Pracht's preview and first-party serverful adapters redirect the bare base
|
|
348
|
+
(`/app`) to its trailing-slash form (`/app/`) before serving the root document;
|
|
349
|
+
custom adapters receive the same behavior through `handlePrachtRequest()`.
|
|
350
|
+
Framework-owned browser URLs from the default image loader and OpenAPI
|
|
351
|
+
companion artifacts pick up the same base automatically.
|
|
352
|
+
|
|
353
|
+
### Setup
|
|
354
|
+
|
|
355
|
+
1. Ensure `@pracht/adapter-static` is installed.
|
|
356
|
+
2. In `vite.config.ts`:
|
|
357
|
+
```ts
|
|
358
|
+
import { pracht } from "@pracht/vite-plugin";
|
|
359
|
+
import { staticAdapter } from "@pracht/adapter-static";
|
|
360
|
+
export default { plugins: [pracht({ adapter: staticAdapter() })] };
|
|
361
|
+
// With dynamic SPA routes, add { fallback: "200.html" } and configure the
|
|
362
|
+
// host to rewrite unmatched URLs to it. If the route or shell exports
|
|
363
|
+
// head(), also set generic fallbackHead metadata shared by every rewrite.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Build & Deploy
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
pracht build # dist/client/ is the whole deployment
|
|
370
|
+
pracht preview # local static file server over dist/client/
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Upload `dist/client/` to any static host (GitHub Pages, S3, nginx, Netlify).
|
|
374
|
+
`dist/server/` is build tooling only — never deploy it. The host must serve
|
|
375
|
+
`<dir>/index.html` for clean URLs and should use `404.html` as its error
|
|
376
|
+
document. A static `notFound` page must use full hydration so that shared
|
|
377
|
+
document can adopt the visitor's real URL. Client navigation fetches collision-safe
|
|
378
|
+
bounded opaque `.json` files under `_pracht/state/` for full-hydration SSG
|
|
379
|
+
routes whose loader or route/shell `head()` metadata participates in navigation;
|
|
380
|
+
equivalent raw-Unicode and percent-encoded URL segment spellings resolve to the
|
|
381
|
+
same state file. Explicitly loaderless and headless routes fetch no Pracht
|
|
382
|
+
state; loaderless routes with head metadata fetch static state for font-head
|
|
383
|
+
fragments but still use browser-side requests to an external API for live
|
|
384
|
+
data. Files under `public/_pracht/state/` may not occupy a generated
|
|
385
|
+
route-state path; the build rejects the collision instead of overwriting the
|
|
386
|
+
public file. Files copied from `public/` or emitted by Vite also may not occupy
|
|
387
|
+
the generated `404.html` or configured fallback path, including a case- or
|
|
388
|
+
Unicode-normalization-equivalent spelling; the build rejects the portable
|
|
389
|
+
collision instead of overwriting existing output. Generic `fallbackHead` fonts
|
|
390
|
+
remain registered while the fallback commits a loaderless dynamic SPA route.
|
|
391
|
+
See docs/ADAPTERS.md § Static Adapter for host header
|
|
392
|
+
configuration and limitations (markdown negotiation, base paths). Pages are
|
|
393
|
+
written to the percent-decoded output path, matching how static hosts resolve
|
|
394
|
+
requests; `pracht preview` decodes request segments the same way. The SPA fallback only client-renders matched SPA routes; dynamic
|
|
395
|
+
SSG paths omitted by `getStaticPaths()` render the app's not-found page with
|
|
396
|
+
the build-time loader data or handled error state carried over from `404.html`.
|
|
397
|
+
The host rewrite that serves the fallback answers unknown URLs with status 200 (soft 404), and an app
|
|
398
|
+
with no `notFound` page and no unshadowed client-routable SPA catch-all renders them blank — the build
|
|
399
|
+
warns about that shape. A dynamic SPA route, its shell, or the not-found page
|
|
400
|
+
with `head()` requires an explicit `fallbackHead`, because the shared static
|
|
401
|
+
document cannot evaluate URL-specific server metadata. Prerendered pages must
|
|
402
|
+
map to distinct portable filesystem paths; duplicate/case-folded or
|
|
403
|
+
Unicode-normalization-equivalent outputs, Windows-invalid or overlong filename
|
|
404
|
+
components, and file/directory conflicts such as `/` with `/index.html` fail
|
|
405
|
+
before any page is written. Fallback names likewise reject Windows reserved
|
|
406
|
+
device names and the portable 255-byte/code-unit component limit.
|
|
407
|
+
|
|
189
408
|
---
|
|
190
409
|
|
|
191
410
|
## Deployment Checklist
|
|
@@ -201,7 +420,7 @@ Produces: `.vercel/output/config.json`, `.vercel/output/static/`, `.vercel/outpu
|
|
|
201
420
|
|
|
202
421
|
1. Read `vite.config.ts` and `package.json` before giving advice.
|
|
203
422
|
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`.
|
|
423
|
+
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
424
|
4. If the user needs an adapter that isn't installed, help them add it (`pnpm add @pracht/adapter-*`).
|
|
206
425
|
5. Don't push to production without the user's explicit confirmation.
|
|
207
426
|
|