create-pracht 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/package.json +1 -1
- package/skills/add-i18n/SKILL.md +308 -151
- package/skills/audit-bundles/SKILL.md +8 -0
- package/skills/audit-headers/SKILL.md +14 -5
- package/skills/audit-secrets/SKILL.md +7 -4
- package/skills/configure-isg/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +15 -5
- package/skills/pracht-debug/SKILL.md +9 -1
- package/skills/pracht-deploy/SKILL.md +118 -12
- package/skills/pracht-scaffold/SKILL.md +11 -1
- package/skills/pre-deploy/SKILL.md +82 -11
- package/skills/typed-routes/SKILL.md +6 -2
- package/skills/upgrade-pracht/SKILL.md +5 -2
- package/src/index.js +81 -17
|
@@ -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.
|
|
@@ -81,16 +81,20 @@ webhooks naming them are `skipped` on Node/Cloudflare (nothing to refresh).
|
|
|
81
81
|
|
|
82
82
|
## Step 3: The revalidation webhook
|
|
83
83
|
|
|
84
|
-
All adapters expose `POST
|
|
85
|
-
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:
|
|
86
87
|
|
|
87
88
|
```sh
|
|
88
|
-
curl -X POST https://example.com/__pracht/revalidate \
|
|
89
|
+
curl -X POST https://example.com/app/__pracht/revalidate \
|
|
89
90
|
-H "Authorization: Bearer $PRACHT_REVALIDATE_TOKEN" \
|
|
90
91
|
-H "Content-Type: application/json" \
|
|
91
92
|
-d '{"paths":["/pricing"]}'
|
|
92
93
|
```
|
|
93
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
|
+
|
|
94
98
|
- Auth: `PRACHT_REVALIDATE_TOKEN` env var; fails closed with `401` when unset
|
|
95
99
|
or wrong. Providers that can't send bearer auth may use the
|
|
96
100
|
`x-pracht-revalidate-token` header instead.
|
|
@@ -78,7 +78,7 @@ 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"` |
|
|
@@ -424,6 +424,15 @@ the `@pracht/image/client` types once in a `.d.ts`, and keep
|
|
|
424
424
|
`width`/`height`/`blurDataURL` exactly like Next's static imports. Pracht's
|
|
425
425
|
blur is CSS-only (no fade animation, no inline event handlers).
|
|
426
426
|
|
|
427
|
+
For apps that relied on `next/image` producing files during a static export,
|
|
428
|
+
use `?pracht&pracht-static` instead. It emits cached responsive WebP variants
|
|
429
|
+
and bypasses the runtime loader while retaining plain, hydration-free `<img>`
|
|
430
|
+
markup. When Markdown content contains relative images, prefer
|
|
431
|
+
`defineMarkdownCollection()` from `@pracht/markdown`; it applies the same
|
|
432
|
+
static pipeline to normal `` syntax. Keep root-relative
|
|
433
|
+
`public/` and remote image URLs unchanged, and use an absolute Vite `base` for
|
|
434
|
+
static variants.
|
|
435
|
+
|
|
427
436
|
#### `useRouter` → navigation
|
|
428
437
|
|
|
429
438
|
```tsx
|
|
@@ -452,13 +461,13 @@ async function createPost(formData: FormData) {
|
|
|
452
461
|
}
|
|
453
462
|
|
|
454
463
|
// Pracht — API route handler
|
|
455
|
-
import type
|
|
464
|
+
import { withBase, type ApiRouteArgs } from "@pracht/core";
|
|
456
465
|
|
|
457
466
|
export async function POST({ request }: ApiRouteArgs) {
|
|
458
467
|
const form = await request.formData();
|
|
459
468
|
await db.insert({ title: form.get("title") });
|
|
460
469
|
// revalidatePath("/posts") equivalent: regenerate the ISG page on demand
|
|
461
|
-
await fetch(new URL("/__pracht/revalidate", request.url), {
|
|
470
|
+
await fetch(new URL(withBase("/__pracht/revalidate"), request.url), {
|
|
462
471
|
method: "POST",
|
|
463
472
|
headers: {
|
|
464
473
|
authorization: `Bearer ${process.env.PRACHT_REVALIDATE_TOKEN}`,
|
|
@@ -468,7 +477,7 @@ export async function POST({ request }: ApiRouteArgs) {
|
|
|
468
477
|
});
|
|
469
478
|
return new Response(null, {
|
|
470
479
|
status: 303,
|
|
471
|
-
headers: { location: "/posts" },
|
|
480
|
+
headers: { location: withBase("/posts") },
|
|
472
481
|
});
|
|
473
482
|
}
|
|
474
483
|
```
|
|
@@ -511,7 +520,8 @@ export async function loader({ request }: LoaderArgs) {
|
|
|
511
520
|
| `next/image` | `@pracht/image` |
|
|
512
521
|
| `react` | `preact` |
|
|
513
522
|
| `react-dom` | `preact` |
|
|
514
|
-
|
|
|
523
|
+
| `next/font/local` | `defineFont()` from `@pracht/core` — register via `head() { return { fonts: [font] } }`, use `font.className`/`font.style` in components |
|
|
524
|
+
| `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 |
|
|
515
525
|
| `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
|
|
516
526
|
| `next-auth` | Direct integration in middleware/loaders |
|
|
517
527
|
| `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
|
|
|
@@ -72,6 +72,14 @@ Work through these in order, stopping when you find the root cause:
|
|
|
72
72
|
- Date/time rendering differences
|
|
73
73
|
- Browser-only APIs used during SSR (`window`, `document`, `localStorage`)
|
|
74
74
|
- Conditional rendering based on client state
|
|
75
|
+
- Two copies of `@pracht/core` in the SSR module graph. The tell is that
|
|
76
|
+
`useLocation()` returns `/`, `useParams()` returns `{}`, and
|
|
77
|
+
`useRouteData()` returns `undefined` in the server-rendered HTML for
|
|
78
|
+
*every* page, while the hydrated client is correct — the provider and the
|
|
79
|
+
hooks hold different `createContext()` objects. The plugin prevents this by
|
|
80
|
+
keeping `@pracht/*` in `ssr.noExternal` (dev and build alike); listing a
|
|
81
|
+
`@pracht/*` package in `ssr.external` overrides that and brings the split
|
|
82
|
+
back.
|
|
75
83
|
- **Missing shell**: Referencing an unregistered shell name throws at manifest resolution — `Unknown shell "..." for route "...". Did you mean "..."? Registered shells: ...` — and shows up in the dev error overlay as soon as the server loads the manifest. Verify the shell is registered in `defineApp({ shells: { ... } })` and assigned to the route/group.
|
|
76
84
|
- **404 page**: Route not matched — check manifest wiring (step 1). In `pracht dev`, unmatched navigations render a dev-only 404 page listing every registered route with its render mode; compare the requested path against that table. The route table is also printed on dev-server startup and available via `pracht inspect routes`. Apps that declare `defineApp({ notFound })` render their own 404 page instead (in dev and production alike), so the route table is not shown — check `pracht inspect routes` directly. A 404 on a URL you *do* expect to work usually means the loader threw `notFound()`, not that matching failed.
|
|
77
85
|
|
|
@@ -1,12 +1,13 @@
|
|
|
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, Netlify, or
|
|
7
|
-
config, 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 netlify", "deploy to vercel",
|
|
9
|
+
"deploy to cloudflare", "deploy to netlify", "deploy to vercel", "static
|
|
10
|
+
export", or
|
|
10
11
|
"production build".
|
|
11
12
|
allowed-tools:
|
|
12
13
|
- Bash
|
|
@@ -37,6 +38,7 @@ If the pracht MCP server is registered (docs/MCP.md), prefer the `inspect_build`
|
|
|
37
38
|
| Cloudflare Workers | `@pracht/adapter-cloudflare` | Stable |
|
|
38
39
|
| Netlify | `@pracht/adapter-netlify` | Stable |
|
|
39
40
|
| Vercel | `@pracht/adapter-vercel` | Stable |
|
|
41
|
+
| Static export | `@pracht/adapter-static` | Stable |
|
|
40
42
|
|
|
41
43
|
---
|
|
42
44
|
|
|
@@ -62,6 +64,21 @@ Pin `canonicalOrigin` in production so `request.url` does not depend on the
|
|
|
62
64
|
incoming `Host` header. `maxBodySize` is also available on `nodeAdapter()`.
|
|
63
65
|
Only custom entries behind a trusted proxy that overwrites forwarded headers
|
|
64
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.
|
|
65
82
|
|
|
66
83
|
### Build
|
|
67
84
|
|
|
@@ -116,7 +133,7 @@ pracht build
|
|
|
116
133
|
npx wrangler deploy
|
|
117
134
|
```
|
|
118
135
|
|
|
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`.
|
|
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`. Keep `no_bundle: true` and the JavaScript `ESModule` rule: Pracht's Vite output is already bundled and can contain lazy server chunks that Wrangler must upload separately.
|
|
120
137
|
|
|
121
138
|
Wrangler owns the Worker's binding environment. Put local-only secrets such as
|
|
122
139
|
`PRACHT_CONFIRMATION_SECRET` and `PRACHT_REVALIDATE_TOKEN` in a gitignored
|
|
@@ -134,8 +151,10 @@ pracht build
|
|
|
134
151
|
npx wrangler dev --config wrangler.local.jsonc --port 3000
|
|
135
152
|
```
|
|
136
153
|
|
|
137
|
-
The local config must keep `main: "dist/server/worker.js"
|
|
138
|
-
|
|
154
|
+
The local config must keep `main: "dist/server/worker.js"`, keep
|
|
155
|
+
`no_bundle: true`, include the JavaScript `ESModule` rule, and omit the
|
|
156
|
+
production route. `pracht preview` does not forward Wrangler's `--config`
|
|
157
|
+
flag.
|
|
139
158
|
|
|
140
159
|
### Wrangler Configuration
|
|
141
160
|
|
|
@@ -144,6 +163,8 @@ production route. `pracht preview` does not forward Wrangler's `--config` flag.
|
|
|
144
163
|
{
|
|
145
164
|
"name": "my-pracht-app",
|
|
146
165
|
"main": "dist/server/worker.js",
|
|
166
|
+
"no_bundle": true,
|
|
167
|
+
"rules": [{ "type": "ESModule", "globs": ["**/*.js", "**/*.mjs"] }],
|
|
147
168
|
"compatibility_date": "2026-04-06",
|
|
148
169
|
"assets": {
|
|
149
170
|
"binding": "ASSETS",
|
|
@@ -153,7 +174,7 @@ production route. `pracht preview` does not forward Wrangler's `--config` flag.
|
|
|
153
174
|
}
|
|
154
175
|
```
|
|
155
176
|
|
|
156
|
-
`"binding": "ASSETS"
|
|
177
|
+
`"no_bundle": true`, the JavaScript `ESModule` rule, `"binding": "ASSETS"`, and `"run_worker_first": true` are required. Without the first two settings, Wrangler either re-bundles Pracht's Vite output and folds lazy server chunks into the entry or omits those chunks from the upload. Without the binding, the worker's `env.ASSETS` resolves to nothing and the runtime silently falls back to `null` — headers and ISG manifests load empty, so SSG serving, ISG revalidation, and per-route headers all silently no-op. The canonical config lives at `examples/cloudflare/wrangler.jsonc`. If you rename the binding with `assetsBinding` (below), the wrangler `binding` value must match.
|
|
157
178
|
|
|
158
179
|
### Bindings (KV, D1, R2)
|
|
159
180
|
|
|
@@ -249,10 +270,14 @@ npx netlify deploy --build --prod
|
|
|
249
270
|
|
|
250
271
|
The build emits `netlify/functions/pracht.mjs`. Page requests go through that
|
|
251
272
|
function so Markdown negotiation and route-state requests remain correct;
|
|
252
|
-
hashed assets bypass it and stay outside the function bundle
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
273
|
+
hashed assets bypass it and stay outside the function bundle at the origin
|
|
274
|
+
root. With a Vite deploy base, the function instead bundles and serves the
|
|
275
|
+
base-free asset and `/_pracht` trees so `/app/...` requests remain inside the
|
|
276
|
+
mount. Custom `excludedPath` entries still bypass their literal origin-root
|
|
277
|
+
URLs, but matching files remain bundled for base-prefixed requests. The
|
|
278
|
+
generated config enumerates only client files the function can serve and roots
|
|
279
|
+
applicable exclusions at the function file so Netlify's tracer cannot re-add
|
|
280
|
+
bypassed trees. Netlify durable caching
|
|
256
281
|
implements time-based ISG and per-path cache tags implement authenticated
|
|
257
282
|
webhook revalidation. A trailing-slash ISG document request permanently
|
|
258
283
|
redirects to the canonical slashless URL before rendering, and webhook
|
|
@@ -305,6 +330,87 @@ functions.
|
|
|
305
330
|
|
|
306
331
|
---
|
|
307
332
|
|
|
333
|
+
## Static Export Deployment
|
|
334
|
+
|
|
335
|
+
For apps where every route is `render: "ssg"` (or loaderless, full-hydration
|
|
336
|
+
`"spa"`), with no
|
|
337
|
+
request middleware, API routes, or HTTP/MCP/WebMCP-exposed capabilities. SSG
|
|
338
|
+
loaders run only at build time and must produce HTML plus valid JSON route
|
|
339
|
+
state; dynamic SSG routes must export `getStaticPaths()`. Anything else fails the build with an error naming the
|
|
340
|
+
offenders — that is the signal to pick a serverful adapter instead. Only
|
|
341
|
+
manifest-registered capabilities participate; every registered capability
|
|
342
|
+
module must load successfully so exposure validation can fail closed. The
|
|
343
|
+
`notFound` page must use full hydration (the default), because the shared
|
|
344
|
+
`404.html` needs the client router to adopt the visitor's actual URL. Sub-path
|
|
345
|
+
deploys (GitHub Pages *project* sites, S3 key prefixes) set Vite `base` to that
|
|
346
|
+
path; CDN and document-relative bases (`""` / `"./"`) are build errors,
|
|
347
|
+
because they split assets from the deploy root or resolve them beneath nested
|
|
348
|
+
page directories. Under a base,
|
|
349
|
+
internal navigation must go through `<Link route>` / `href()` — a hand-written
|
|
350
|
+
`<a href="/about">` still means the origin root.
|
|
351
|
+
Pracht's preview and first-party serverful adapters redirect the bare base
|
|
352
|
+
(`/app`) to its trailing-slash form (`/app/`) before serving the root document;
|
|
353
|
+
custom adapters receive the same behavior through `handlePrachtRequest()`.
|
|
354
|
+
Framework-owned browser URLs from the default image loader and OpenAPI
|
|
355
|
+
companion artifacts pick up the same base automatically.
|
|
356
|
+
|
|
357
|
+
### Setup
|
|
358
|
+
|
|
359
|
+
1. Ensure `@pracht/adapter-static` is installed.
|
|
360
|
+
2. In `vite.config.ts`:
|
|
361
|
+
```ts
|
|
362
|
+
import { pracht } from "@pracht/vite-plugin";
|
|
363
|
+
import { staticAdapter } from "@pracht/adapter-static";
|
|
364
|
+
export default { plugins: [pracht({ adapter: staticAdapter() })] };
|
|
365
|
+
// With dynamic SPA routes, add { fallback: "200.html" } and configure the
|
|
366
|
+
// host to rewrite unmatched URLs to it. If the route or shell exports
|
|
367
|
+
// head(), also set generic fallbackHead metadata shared by every rewrite.
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Build & Deploy
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
pracht build # dist/client/ is the whole deployment
|
|
374
|
+
pracht preview # local static file server over dist/client/
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Upload `dist/client/` to any static host (GitHub Pages, S3, nginx, Netlify).
|
|
378
|
+
`dist/server/` is build tooling only — never deploy it. The host must serve
|
|
379
|
+
`<dir>/index.html` for clean URLs and should use `404.html` as its error
|
|
380
|
+
document. A static `notFound` page must use full hydration so that shared
|
|
381
|
+
document can adopt the visitor's real URL. Client navigation fetches collision-safe
|
|
382
|
+
bounded opaque `.json` files under `_pracht/state/` for full-hydration SSG
|
|
383
|
+
routes whose loader or route/shell `head()` metadata participates in navigation;
|
|
384
|
+
equivalent raw-Unicode and percent-encoded URL segment spellings resolve to the
|
|
385
|
+
same state file. Explicitly loaderless and headless routes fetch no Pracht
|
|
386
|
+
state; loaderless routes with head metadata fetch static state for font-head
|
|
387
|
+
fragments but still use browser-side requests to an external API for live
|
|
388
|
+
data. Files under `public/_pracht/state/` may not occupy a generated
|
|
389
|
+
route-state path; the build rejects the collision instead of overwriting the
|
|
390
|
+
public file. Files copied from `public/` or emitted by Vite also may not occupy
|
|
391
|
+
the generated `404.html` or configured fallback path, including a case- or
|
|
392
|
+
Unicode-normalization-equivalent spelling; the build rejects the portable
|
|
393
|
+
collision instead of overwriting existing output. Generic `fallbackHead` fonts
|
|
394
|
+
remain registered while the fallback commits a loaderless dynamic SPA route.
|
|
395
|
+
See docs/ADAPTERS.md § Static Adapter for host header
|
|
396
|
+
configuration and limitations (markdown negotiation, base paths). Pages are
|
|
397
|
+
written to the percent-decoded output path, matching how static hosts resolve
|
|
398
|
+
requests; `pracht preview` decodes request segments the same way. The SPA fallback only client-renders matched SPA routes; dynamic
|
|
399
|
+
SSG paths omitted by `getStaticPaths()` render the app's not-found page with
|
|
400
|
+
the build-time loader data or handled error state carried over from `404.html`.
|
|
401
|
+
The host rewrite that serves the fallback answers unknown URLs with status 200 (soft 404), and an app
|
|
402
|
+
with no `notFound` page and no unshadowed client-routable SPA catch-all renders them blank — the build
|
|
403
|
+
warns about that shape. A dynamic SPA route, its shell, or the not-found page
|
|
404
|
+
with `head()` requires an explicit `fallbackHead`, because the shared static
|
|
405
|
+
document cannot evaluate URL-specific server metadata. Prerendered pages must
|
|
406
|
+
map to distinct portable filesystem paths; duplicate/case-folded or
|
|
407
|
+
Unicode-normalization-equivalent outputs, Windows-invalid or overlong filename
|
|
408
|
+
components, and file/directory conflicts such as `/` with `/index.html` fail
|
|
409
|
+
before any page is written. Fallback names likewise reject Windows reserved
|
|
410
|
+
device names and the portable 255-byte/code-unit component limit.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
308
414
|
## Deployment Checklist
|
|
309
415
|
|
|
310
416
|
1. **Build**: Run `pracht build` and verify `dist/` output.
|
|
@@ -53,7 +53,7 @@ pracht generate api --path /health --methods GET,POST
|
|
|
53
53
|
- `--shell`/`--middleware` names must already be registered in the app manifest — the CLI errors otherwise. Generate the shell/middleware first, then the route that references it.
|
|
54
54
|
- If the pracht MCP server is registered (docs/MCP.md), call the `generate_route`/`generate_shell`/`generate_middleware`/`generate_api` MCP tools instead of Bash — same behavior, structured results.
|
|
55
55
|
- Add `--json` when another agent/tool needs machine-readable output.
|
|
56
|
-
- `generate route` also emits a Playwright smoke test in `e2e/` when the app has a Playwright setup (`playwright.config.*` or an `e2e/` directory). Pass `--no-test` to skip it, `--test` to force it. Keep the generated test — it is the output-level proof the route works.
|
|
56
|
+
- `generate route` also emits a Playwright smoke test in `e2e/` when the app has a Playwright setup (`playwright.config.*` or an `e2e/` directory). Pass `--no-test` to skip it, `--test` to force it. The test imports `@playwright/test`; if that dependency is absent, follow the generator's install note before typechecking. Keep the generated test — it is the output-level proof the route works.
|
|
57
57
|
- Use `pracht inspect routes --json` or `pracht inspect api --json` to confirm current wiring before manual edits when the existing graph matters. `pracht inspect` requires the pracht plugin registered in the project's vite config.
|
|
58
58
|
- If the app has typed routes (`src/pracht-routes.ts` / `.d.ts`) or the user asks for typed links, run `pracht typegen` after adding or renaming routes.
|
|
59
59
|
- If the app commits `.pracht/app-graph.json`, run `pracht plan --write` after changing routes and include the refreshed snapshot — `pracht verify` fails when it is stale.
|
|
@@ -161,6 +161,16 @@ export function GET({ params, url }: ApiRouteArgs) {
|
|
|
161
161
|
- Use `request.json()`, `request.formData()`, etc. for body parsing.
|
|
162
162
|
- Always return `Response` objects (typically `Response.json()`).
|
|
163
163
|
- Dynamic segments use bracket syntax in filenames: `[id].ts`, `[...slug].ts`.
|
|
164
|
+
- For live server→client updates, use Server-Sent Events:
|
|
165
|
+
`createEventStream(request, { keepAlive: 15 })` from `@pracht/core/server`
|
|
166
|
+
returns `{ response, send, close }` — return `response`, push with
|
|
167
|
+
`send({ data, event?, id? })`, and stop producing when `send()` returns
|
|
168
|
+
`false` (client disconnected). Consume in components with
|
|
169
|
+
`useEventSource(url, { json: true })` from `@pracht/core`. Works on all
|
|
170
|
+
adapters. For WebSockets use `isUpgradeRequest(request)` plus the
|
|
171
|
+
per-adapter recipes in `docs/ADAPTERS.md` (Cloudflare: API route + Durable
|
|
172
|
+
Object; Node: `nodeAdapter({ configureServerFrom })`; Vercel: unsupported —
|
|
173
|
+
use SSE).
|
|
164
174
|
|
|
165
175
|
## Wiring Into the Manifest (manual fallback only)
|
|
166
176
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pre-deploy
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.3.0
|
|
4
4
|
description: |
|
|
5
5
|
Adapter-aware pre-deployment checklist for pracht apps targeting Node,
|
|
6
|
-
Cloudflare Workers, or
|
|
7
|
-
production runtime: missing env vars, Node-only APIs in
|
|
8
|
-
ISG manifest absence, oversized edge bundles, missing
|
|
6
|
+
Cloudflare Workers, Vercel, or a pure static export. Catches the issues that
|
|
7
|
+
only surface in the production runtime: missing env vars, Node-only APIs in
|
|
8
|
+
edge bundles, ISG manifest absence, oversized edge bundles, missing
|
|
9
|
+
wrangler/vercel config, and static hosts missing clean-URL, 404, or security
|
|
10
|
+
header configuration.
|
|
9
11
|
Use when asked to "pre-deploy check", "ready to ship?", "deployment
|
|
10
12
|
checklist", "is my build production-safe", or before running `wrangler
|
|
11
13
|
deploy` / `vercel deploy`.
|
|
@@ -27,8 +29,8 @@ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
|
27
29
|
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
28
30
|
shelling out.
|
|
29
31
|
|
|
30
|
-
Read `vite.config.ts` and look for `nodeAdapter()`, `cloudflareAdapter()`,
|
|
31
|
-
`vercelAdapter()`. Confirm with:
|
|
32
|
+
Read `vite.config.ts` and look for `nodeAdapter()`, `cloudflareAdapter()`,
|
|
33
|
+
`vercelAdapter()`, or `staticAdapter()`. Confirm with:
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
36
|
pracht inspect build --json
|
|
@@ -82,6 +84,10 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
82
84
|
are intentionally not trusted.
|
|
83
85
|
- Reverse-proxy / TLS termination configured (out of scope for this skill —
|
|
84
86
|
flag for confirmation).
|
|
87
|
+
- If the proxy strips Vite's deploy base, confirm
|
|
88
|
+
`nodeAdapter({ basePathStripped: true })`; application code should still
|
|
89
|
+
observe the public base in `request.url`, and the proxy must own the public
|
|
90
|
+
bare-base redirect (`/app` to `/app/`).
|
|
85
91
|
|
|
86
92
|
### Cloudflare Workers (`@pracht/adapter-cloudflare`)
|
|
87
93
|
|
|
@@ -92,6 +98,10 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
92
98
|
validates every named export of the deploy entry and rejects the build
|
|
93
99
|
metadata (`buildTarget`, manifests, `resolvedApp`, ...) that `server.js`
|
|
94
100
|
exports for the prerender pass.
|
|
101
|
+
- `no_bundle` is `true`, with an `ESModule` rule whose globs include
|
|
102
|
+
`"**/*.js"`. Pracht's Vite output is already bundled and may contain lazy
|
|
103
|
+
server chunks; these settings make Wrangler upload the chunks as separate
|
|
104
|
+
modules instead of folding them into the entry file.
|
|
95
105
|
- `assets.directory` points to `dist/client`.
|
|
96
106
|
- `compatibility_date` is set, and is a date the installed workerd supports.
|
|
97
107
|
It must not be *newer* than the runtime: workerd refuses to start with
|
|
@@ -122,8 +132,10 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
122
132
|
gateway with a normalized `cf.cacheKey`; also check that markdown-capable
|
|
123
133
|
routes normalize `Accept` at the gateway when variant fan-out matters.
|
|
124
134
|
- Bundle size: measure what actually deploys — `dist/server/worker.js` plus
|
|
125
|
-
its `dist/server/server.js` import
|
|
126
|
-
|
|
135
|
+
its `dist/server/server.js` import and lazy chunks (`no_bundle: true` plus
|
|
136
|
+
the JavaScript `ESModule` rule uploads the pre-built module graph;
|
|
137
|
+
`worker.js` alone is a few lines).
|
|
138
|
+
Workers limit is ~1 MB
|
|
127
139
|
compressed for free tier, ~10 MB on paid. Warn at 80% of the active limit.
|
|
128
140
|
|
|
129
141
|
### Vercel (`@pracht/adapter-vercel`)
|
|
@@ -156,12 +168,67 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
156
168
|
`passthroughLoader` instead.
|
|
157
169
|
- Build Output API v3 sanity: `config.json` has `version: 3`.
|
|
158
170
|
|
|
171
|
+
### Static export (`@pracht/adapter-static`)
|
|
172
|
+
|
|
173
|
+
`adapterTarget` is `"static"`. There is no server to get wrong, so the
|
|
174
|
+
checklist is about what the *host* must do and what the build cannot enforce.
|
|
175
|
+
|
|
176
|
+
- `dist/client/` exists and is the deploy root. `dist/server/` is build tooling
|
|
177
|
+
only — it must not be uploaded (it contains the prerender bundle).
|
|
178
|
+
- The build itself is the gate: it fails closed on `ssr`/`isg` routes, SPA
|
|
179
|
+
loaders, non-full SPA hydration, API routes, route/not-found middleware,
|
|
180
|
+
network-exposed capabilities, and any Vite `base` that is not `/` or a
|
|
181
|
+
root-absolute path (CDN and document-relative bases are rejected). If
|
|
182
|
+
`pracht build` succeeded, those contracts already hold — do not re-derive
|
|
183
|
+
them by hand. Report a failing build verbatim; the message names the routes.
|
|
184
|
+
- Host must serve `index.html` for directory URLs (clean URLs). Confirm the
|
|
185
|
+
host's setting: S3 website endpoints need an index document, nginx needs
|
|
186
|
+
`try_files $uri $uri/index.html`, GitHub Pages and Netlify do it by default.
|
|
187
|
+
- Host must map `404.html` as the error document, otherwise unknown URLs get
|
|
188
|
+
the host's generic error page instead of the app's `notFound` route. Verify
|
|
189
|
+
`dist/client/404.html` exists; if it does not, the app declares no `notFound`
|
|
190
|
+
page — flag it as a `warn`.
|
|
191
|
+
- **Security headers are not applied.** Every other adapter sets the four
|
|
192
|
+
default security headers at request time; a static host has no request
|
|
193
|
+
runtime. `dist/server/headers-manifest.json` records the headers each route
|
|
194
|
+
*would* have carried — mirror the ones you need in the host's own header
|
|
195
|
+
config (`_headers` on Netlify, CloudFront response header policies, nginx
|
|
196
|
+
`add_header`). This is an `error` for any app handling user input, and
|
|
197
|
+
`warn` otherwise. HSTS and CSP are host-side decisions either way.
|
|
198
|
+
- If `staticAdapter({ fallback })` is configured, the host needs a rewrite of
|
|
199
|
+
unmatched URLs to that file, and the rewrite must not shadow real files.
|
|
200
|
+
Note that it makes unknown URLs answer `200` (soft 404s). Without the
|
|
201
|
+
rewrite the fallback file is inert — deep links into dynamic `render: "spa"`
|
|
202
|
+
routes will 404.
|
|
203
|
+
- Smoke test the real output, not the dev server:
|
|
204
|
+
`pracht preview --skip-build` serves `dist/client/` the way a dumb host
|
|
205
|
+
would. Check `/`, one dynamic SSG path, one deep link into a SPA route, and
|
|
206
|
+
one unknown URL.
|
|
207
|
+
- Routes exporting `markdown` rely on server-side `Accept` negotiation, which
|
|
208
|
+
a static host cannot do — agents asking for `text/markdown` get HTML. The
|
|
209
|
+
build prints a note when this applies; publish `.md` files under `public/`
|
|
210
|
+
if a raw-markdown corpus matters.
|
|
211
|
+
- Deploying to a sub-path (GitHub Pages *project* site, S3 key prefix) needs
|
|
212
|
+
Vite `base` set to that path (`base: "/my-project/"`). Check it matches the
|
|
213
|
+
deploy path exactly — a mismatch 404s every asset. Then check the app has no
|
|
214
|
+
hand-written root-absolute internal links (`<a href="/about">`): those are
|
|
215
|
+
not base-prefixed and will leave the deploy. `grep -rn 'href="/' src/` and
|
|
216
|
+
confirm each hit is external, an asset under `public/`, or a `<Link route>`.
|
|
217
|
+
Framework-owned URLs from `@pracht/image`'s `defaultLoader` and the OpenAPI
|
|
218
|
+
companion UI/document already carry the base; do not flag their base-free
|
|
219
|
+
route declarations. Custom image loaders and OpenAPI provider asset URLs
|
|
220
|
+
still need to match the intended host.
|
|
221
|
+
CDN bases (`https://cdn…`) and document-relative bases (`""` / `"./"`) are
|
|
222
|
+
build errors, not sub-path deploys.
|
|
223
|
+
|
|
159
224
|
## Step 4: Cross-cutting checks
|
|
160
225
|
|
|
161
226
|
- Run `audit-secrets` to confirm no `process.env.*` or `context.env.*` values
|
|
162
227
|
flow into loader return values.
|
|
163
228
|
- Run `audit-headers` to confirm `applyDefaultSecurityHeaders` is in use on
|
|
164
229
|
user-facing responses (or that `headers()` exports cover the same ground).
|
|
230
|
+
On a static export this check moves entirely to the host's header config —
|
|
231
|
+
see the static section above.
|
|
165
232
|
- Confirm `git status` is clean (deploying uncommitted work is a footgun).
|
|
166
233
|
|
|
167
234
|
## Step 5: Report
|
|
@@ -179,9 +246,13 @@ status. End with a one-line verdict: `READY` / `BLOCKED (N errors)` /
|
|
|
179
246
|
3. For Cloudflare/Vercel-edge, the Node-only API check is non-negotiable; an
|
|
180
247
|
API not covered by the active compatibility flags will crash the worker on
|
|
181
248
|
a code path that may never hit in dev.
|
|
182
|
-
4.
|
|
183
|
-
|
|
184
|
-
|
|
249
|
+
4. For a static export, never report `READY` without naming the host settings
|
|
250
|
+
the deploy depends on (clean URLs, `404.html`, security headers, and the
|
|
251
|
+
fallback rewrite if configured). The build cannot verify any of them, so an
|
|
252
|
+
unqualified `READY` is the one way this skill can mislead.
|
|
253
|
+
5. 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.
|
|
254
|
+
6. Do not deploy on the user's behalf. End the skill at the verdict.
|
|
255
|
+
7. If `pracht doctor` reports errors, do not run any other checks until those
|
|
185
256
|
are resolved — they will produce noisy false positives.
|
|
186
257
|
|
|
187
258
|
$ARGUMENTS
|
|
@@ -107,8 +107,12 @@ same-origin anchor. It also accepts navigation-behavior props:
|
|
|
107
107
|
`prefetch="none" | "hover" | "intent" | "viewport" | "render"` (per-link
|
|
108
108
|
prefetch strategy, default `"intent"`), `preserveScroll` (keep the scroll
|
|
109
109
|
position), and `viewTransition` (animate the navigation with the View Transitions API
|
|
110
|
-
where supported).
|
|
111
|
-
|
|
110
|
+
where supported). Set `speculate={false}` on links that browser speculation
|
|
111
|
+
rules must not prefetch or prerender. Because it is independent of the JS
|
|
112
|
+
`prefetch` strategy, use both `speculate={false}` and `prefetch="none"` for GET
|
|
113
|
+
links with side effects. There is also an imperative
|
|
114
|
+
`prefetch()` export and a `useNavigation()` hook for pending
|
|
115
|
+
navigation/submission state.
|
|
112
116
|
|
|
113
117
|
### Outside components
|
|
114
118
|
|
|
@@ -33,8 +33,9 @@ pnpm list --depth 1 --json | grep -A2 '@pracht/' # or read package.json + lock
|
|
|
33
33
|
|
|
34
34
|
The family: `@pracht/core`, `@pracht/cli`, `@pracht/vite-plugin`,
|
|
35
35
|
`@pracht/adapter-node`, `@pracht/adapter-cloudflare`, `@pracht/adapter-vercel`,
|
|
36
|
-
`@pracht/preact-ssr-precompile`, `@pracht/
|
|
37
|
-
|
|
36
|
+
`@pracht/preact-ssr-precompile`, `@pracht/content`, `@pracht/markdown`,
|
|
37
|
+
`@pracht/image`. Get the latest published versions with
|
|
38
|
+
`npm view <pkg> version`.
|
|
38
39
|
|
|
39
40
|
## Step 2: Understand the versioning model
|
|
40
41
|
|
|
@@ -75,6 +76,8 @@ https://raw.githubusercontent.com/JoviDeCroock/pracht/main/packages/<dir>/CHANGE
|
|
|
75
76
|
| `@pracht/vite-plugin` | `packages/vite-plugin` |
|
|
76
77
|
| `@pracht/adapter-node` / `-cloudflare` / `-vercel` | `packages/adapter-*` |
|
|
77
78
|
| `@pracht/preact-ssr-precompile` | `packages/preact-ssr-precompile` |
|
|
79
|
+
| `@pracht/content` | `packages/content` |
|
|
80
|
+
| `@pracht/markdown` | `packages/markdown` |
|
|
78
81
|
| `@pracht/image` | `packages/image` |
|
|
79
82
|
|
|
80
83
|
Changelogs are changesets-generated: `## X.Y.Z` sections containing
|