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
|
@@ -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,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
|
```
|
|
@@ -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
|
|
|
@@ -93,7 +99,11 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
93
99
|
metadata (`buildTarget`, manifests, `resolvedApp`, ...) that `server.js`
|
|
94
100
|
exports for the prerender pass.
|
|
95
101
|
- `assets.directory` points to `dist/client`.
|
|
96
|
-
- `compatibility_date` is set and
|
|
102
|
+
- `compatibility_date` is set, and is a date the installed workerd supports.
|
|
103
|
+
It must not be *newer* than the runtime: workerd refuses to start with
|
|
104
|
+
"This Worker requires compatibility date X, but the newest date supported
|
|
105
|
+
by this server binary is Y". Never set it to today's date — that is by
|
|
106
|
+
construction at or beyond the newest released workerd.
|
|
97
107
|
- Bindings declared in wrangler config for every `context.env.*` access in
|
|
98
108
|
loaders, middleware, and API routes (grep, then cross-check).
|
|
99
109
|
- **No Node-only APIs in the server bundle.** Grep the server files for:
|
|
@@ -134,22 +144,85 @@ a markdown summary (graph diff + verify + budgets) worth attaching to it.
|
|
|
134
144
|
- Required env vars are configured in the Vercel project (cannot verify from
|
|
135
145
|
CLI without `vercel env pull` — run that and diff against `process.env.*`
|
|
136
146
|
references).
|
|
137
|
-
- Edge runtime constraints:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
- Edge runtime constraints: the render function's `.vc-config.json` is
|
|
148
|
+
**always** written with `runtime: "edge"`, so run the same Node-only API
|
|
149
|
+
check as Cloudflare **unconditionally** for Vercel builds. Do not skip it
|
|
150
|
+
based on a runtime probe — ISG routes run the same bundle on Node, but any
|
|
151
|
+
Node-only API still breaks the edge function.
|
|
152
|
+
- ISG functions: every `<route>.prerender-config.json` must sit next to a
|
|
153
|
+
**Serverless** `<route>.func` (`.vc-config.json` with `launcherType:
|
|
154
|
+
"Nodejs"`). Vercel rejects a prerender config paired with an edge function:
|
|
155
|
+
`Unexpected function type "EdgeFunction" at path "<route>"`.
|
|
156
|
+
- Region configuration: `vercelAdapter({ regions: "all" })` is valid for the
|
|
157
|
+
Edge render function, but generated Node ISG function configs must omit
|
|
158
|
+
`regions` so the project's default Serverless region applies. Node configs
|
|
159
|
+
may only contain arrays of concrete region identifiers.
|
|
142
160
|
- An API route importing `@pracht/image/node` is an error for the Vercel Edge
|
|
143
161
|
function. Require `vercelLoader` (with aligned allowed sizes) or
|
|
144
162
|
`passthroughLoader` instead.
|
|
145
163
|
- Build Output API v3 sanity: `config.json` has `version: 3`.
|
|
146
164
|
|
|
165
|
+
### Static export (`@pracht/adapter-static`)
|
|
166
|
+
|
|
167
|
+
`adapterTarget` is `"static"`. There is no server to get wrong, so the
|
|
168
|
+
checklist is about what the *host* must do and what the build cannot enforce.
|
|
169
|
+
|
|
170
|
+
- `dist/client/` exists and is the deploy root. `dist/server/` is build tooling
|
|
171
|
+
only — it must not be uploaded (it contains the prerender bundle).
|
|
172
|
+
- The build itself is the gate: it fails closed on `ssr`/`isg` routes, SPA
|
|
173
|
+
loaders, non-full SPA hydration, API routes, route/not-found middleware,
|
|
174
|
+
network-exposed capabilities, and any Vite `base` that is not `/` or a
|
|
175
|
+
root-absolute path (CDN and document-relative bases are rejected). If
|
|
176
|
+
`pracht build` succeeded, those contracts already hold — do not re-derive
|
|
177
|
+
them by hand. Report a failing build verbatim; the message names the routes.
|
|
178
|
+
- Host must serve `index.html` for directory URLs (clean URLs). Confirm the
|
|
179
|
+
host's setting: S3 website endpoints need an index document, nginx needs
|
|
180
|
+
`try_files $uri $uri/index.html`, GitHub Pages and Netlify do it by default.
|
|
181
|
+
- Host must map `404.html` as the error document, otherwise unknown URLs get
|
|
182
|
+
the host's generic error page instead of the app's `notFound` route. Verify
|
|
183
|
+
`dist/client/404.html` exists; if it does not, the app declares no `notFound`
|
|
184
|
+
page — flag it as a `warn`.
|
|
185
|
+
- **Security headers are not applied.** Every other adapter sets the four
|
|
186
|
+
default security headers at request time; a static host has no request
|
|
187
|
+
runtime. `dist/server/headers-manifest.json` records the headers each route
|
|
188
|
+
*would* have carried — mirror the ones you need in the host's own header
|
|
189
|
+
config (`_headers` on Netlify, CloudFront response header policies, nginx
|
|
190
|
+
`add_header`). This is an `error` for any app handling user input, and
|
|
191
|
+
`warn` otherwise. HSTS and CSP are host-side decisions either way.
|
|
192
|
+
- If `staticAdapter({ fallback })` is configured, the host needs a rewrite of
|
|
193
|
+
unmatched URLs to that file, and the rewrite must not shadow real files.
|
|
194
|
+
Note that it makes unknown URLs answer `200` (soft 404s). Without the
|
|
195
|
+
rewrite the fallback file is inert — deep links into dynamic `render: "spa"`
|
|
196
|
+
routes will 404.
|
|
197
|
+
- Smoke test the real output, not the dev server:
|
|
198
|
+
`pracht preview --skip-build` serves `dist/client/` the way a dumb host
|
|
199
|
+
would. Check `/`, one dynamic SSG path, one deep link into a SPA route, and
|
|
200
|
+
one unknown URL.
|
|
201
|
+
- Routes exporting `markdown` rely on server-side `Accept` negotiation, which
|
|
202
|
+
a static host cannot do — agents asking for `text/markdown` get HTML. The
|
|
203
|
+
build prints a note when this applies; publish `.md` files under `public/`
|
|
204
|
+
if a raw-markdown corpus matters.
|
|
205
|
+
- Deploying to a sub-path (GitHub Pages *project* site, S3 key prefix) needs
|
|
206
|
+
Vite `base` set to that path (`base: "/my-project/"`). Check it matches the
|
|
207
|
+
deploy path exactly — a mismatch 404s every asset. Then check the app has no
|
|
208
|
+
hand-written root-absolute internal links (`<a href="/about">`): those are
|
|
209
|
+
not base-prefixed and will leave the deploy. `grep -rn 'href="/' src/` and
|
|
210
|
+
confirm each hit is external, an asset under `public/`, or a `<Link route>`.
|
|
211
|
+
Framework-owned URLs from `@pracht/image`'s `defaultLoader` and the OpenAPI
|
|
212
|
+
companion UI/document already carry the base; do not flag their base-free
|
|
213
|
+
route declarations. Custom image loaders and OpenAPI provider asset URLs
|
|
214
|
+
still need to match the intended host.
|
|
215
|
+
CDN bases (`https://cdn…`) and document-relative bases (`""` / `"./"`) are
|
|
216
|
+
build errors, not sub-path deploys.
|
|
217
|
+
|
|
147
218
|
## Step 4: Cross-cutting checks
|
|
148
219
|
|
|
149
220
|
- Run `audit-secrets` to confirm no `process.env.*` or `context.env.*` values
|
|
150
221
|
flow into loader return values.
|
|
151
222
|
- Run `audit-headers` to confirm `applyDefaultSecurityHeaders` is in use on
|
|
152
223
|
user-facing responses (or that `headers()` exports cover the same ground).
|
|
224
|
+
On a static export this check moves entirely to the host's header config —
|
|
225
|
+
see the static section above.
|
|
153
226
|
- Confirm `git status` is clean (deploying uncommitted work is a footgun).
|
|
154
227
|
|
|
155
228
|
## Step 5: Report
|
|
@@ -167,9 +240,13 @@ status. End with a one-line verdict: `READY` / `BLOCKED (N errors)` /
|
|
|
167
240
|
3. For Cloudflare/Vercel-edge, the Node-only API check is non-negotiable; an
|
|
168
241
|
API not covered by the active compatibility flags will crash the worker on
|
|
169
242
|
a code path that may never hit in dev.
|
|
170
|
-
4.
|
|
171
|
-
|
|
172
|
-
|
|
243
|
+
4. For a static export, never report `READY` without naming the host settings
|
|
244
|
+
the deploy depends on (clean URLs, `404.html`, security headers, and the
|
|
245
|
+
fallback rewrite if configured). The build cannot verify any of them, so an
|
|
246
|
+
unqualified `READY` is the one way this skill can mislead.
|
|
247
|
+
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.
|
|
248
|
+
6. Do not deploy on the user's behalf. End the skill at the verdict.
|
|
249
|
+
7. If `pracht doctor` reports errors, do not run any other checks until those
|
|
173
250
|
are resolved — they will produce noisy false positives.
|
|
174
251
|
|
|
175
252
|
$ARGUMENTS
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scaffold-tests
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.0
|
|
4
4
|
description: |
|
|
5
5
|
Scaffold Vitest unit/integration tests for pracht routes, loaders, and
|
|
6
6
|
middleware. Asks the user once whether to use vitest browser mode with
|
|
7
7
|
`vitest-browser-preact` (real DOM, real events) or classic JSDOM-based
|
|
8
|
-
tests with `@testing-library/preact`. Wires `vitest.config.ts`,
|
|
9
|
-
`LoaderArgs`, and emits ready-to-run files.
|
|
8
|
+
tests with `@testing-library/preact`. Wires `vitest.config.ts`, builds
|
|
9
|
+
`LoaderArgs` with `@pracht/test`, and emits ready-to-run files.
|
|
10
10
|
Use when asked to "scaffold tests", "set up Vitest", "add unit tests",
|
|
11
11
|
"test this loader", or "test this route".
|
|
12
12
|
allowed-tools:
|
|
@@ -48,9 +48,14 @@ Detect the package manager from the lockfile.
|
|
|
48
48
|
Common (both modes):
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
pnpm add -D vitest @types/node
|
|
51
|
+
pnpm add -D vitest @types/node @pracht/test
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`@pracht/test` provides the typed args factories (`createLoaderArgs`,
|
|
55
|
+
`createApiArgs`, `createMiddlewareArgs`, `createApiMiddlewareArgs`), the
|
|
56
|
+
`runMiddleware()` chain runner, `submitForm()`, and the
|
|
57
|
+
`readJson()`/`readRedirect()` response readers used in the templates below.
|
|
58
|
+
|
|
54
59
|
Component-test extras — `@preact/preset-vite` must be an explicit dev
|
|
55
60
|
dependency: the configs in Step 3 import it, and a transitive-only copy
|
|
56
61
|
fails under pnpm's strict `node_modules`.
|
|
@@ -145,75 +150,54 @@ to scaffold, or pass paths via `$ARGUMENTS`.
|
|
|
145
150
|
|
|
146
151
|
```ts
|
|
147
152
|
import { describe, it, expect } from "vitest";
|
|
153
|
+
import { createLoaderArgs } from "@pracht/test";
|
|
148
154
|
import { loader } from "./<route-file>";
|
|
149
155
|
|
|
150
|
-
function args(url: string, init?: RequestInit) {
|
|
151
|
-
const request = new Request(url, init);
|
|
152
|
-
return {
|
|
153
|
-
request,
|
|
154
|
-
params: {} as Record<string, string>,
|
|
155
|
-
context: {} as never,
|
|
156
|
-
url: new URL(request.url),
|
|
157
|
-
signal: AbortSignal.timeout(5000),
|
|
158
|
-
route: {} as never,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
156
|
describe("<route> loader", () => {
|
|
163
157
|
it("returns the expected shape", async () => {
|
|
164
|
-
const data = await loader(
|
|
158
|
+
const data = await loader(createLoaderArgs({ url: "/<path>" }));
|
|
165
159
|
expect(data).toBeDefined();
|
|
166
160
|
});
|
|
167
161
|
});
|
|
168
162
|
```
|
|
169
163
|
|
|
164
|
+
Pass `params`, `headers`, a partial `context`, or a JSON-encoding `body` as
|
|
165
|
+
needed; the factory defaults everything else and exposes `controller` (the
|
|
166
|
+
`AbortController` behind `args.signal`) for cancellation tests.
|
|
167
|
+
|
|
170
168
|
### Middleware test template
|
|
171
169
|
|
|
172
170
|
```ts
|
|
173
171
|
import { describe, it, expect } from "vitest";
|
|
172
|
+
import { createMiddlewareArgs, readRedirect, runMiddleware } from "@pracht/test";
|
|
174
173
|
import { middleware } from "./<middleware-file>";
|
|
175
174
|
|
|
176
175
|
describe("<name> middleware", () => {
|
|
177
|
-
const ok = new Response("ok", { status: 200 });
|
|
178
|
-
const next = async () => ok;
|
|
179
|
-
|
|
180
176
|
it("redirects unauthenticated requests", async () => {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
request,
|
|
185
|
-
params: {},
|
|
186
|
-
context: {} as never,
|
|
187
|
-
url: new URL(request.url),
|
|
188
|
-
signal: AbortSignal.timeout(5000),
|
|
189
|
-
route: {} as never,
|
|
190
|
-
},
|
|
191
|
-
next,
|
|
192
|
-
);
|
|
193
|
-
expect(response.status).toBe(302);
|
|
194
|
-
expect(response.headers.get("location")).toMatch(/^\/login/);
|
|
177
|
+
const response = await runMiddleware(middleware, createMiddlewareArgs({ url: "/dashboard" }));
|
|
178
|
+
expect(readRedirect(response).location).toMatch(/^\/login/);
|
|
195
179
|
});
|
|
196
180
|
|
|
197
181
|
it("calls through when authenticated", async () => {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
request,
|
|
204
|
-
params: {},
|
|
205
|
-
context: {} as never,
|
|
206
|
-
url: new URL(request.url),
|
|
207
|
-
signal: AbortSignal.timeout(5000),
|
|
208
|
-
route: {} as never,
|
|
209
|
-
},
|
|
210
|
-
next,
|
|
182
|
+
const response = await runMiddleware(
|
|
183
|
+
middleware,
|
|
184
|
+
createMiddlewareArgs({ url: "/dashboard", headers: { cookie: "session=valid" } }),
|
|
185
|
+
async () => new Response("handler ran"),
|
|
211
186
|
);
|
|
212
|
-
expect(response).toBe(
|
|
187
|
+
expect(await response.text()).toBe("handler ran");
|
|
213
188
|
});
|
|
214
189
|
});
|
|
215
190
|
```
|
|
216
191
|
|
|
192
|
+
`runMiddleware()` accepts an array to test a chain in manifest order,
|
|
193
|
+
including `context` mutations flowing downstream and returned-response
|
|
194
|
+
short-circuits. A thrown `Response` resolves by default, matching page/API
|
|
195
|
+
outer normalization. For raw capability-chain behavior, pass
|
|
196
|
+
`undefined, { thrownResponse: "reject" }` after the args/final-handler slots;
|
|
197
|
+
prefer `createCapabilityTestHost()` so the test asserts the real typed
|
|
198
|
+
`internal_error` envelope. For middleware attached through `defineApp({ api })`,
|
|
199
|
+
use `createApiMiddlewareArgs()` so `route` has the API metadata shape.
|
|
200
|
+
|
|
217
201
|
### Component test template (browser mode)
|
|
218
202
|
|
|
219
203
|
```tsx
|
|
@@ -274,8 +258,8 @@ broken scaffolding.
|
|
|
274
258
|
1. Ask the rendering-strategy question once per project; persist by
|
|
275
259
|
inspecting `vitest.config.ts` on subsequent runs.
|
|
276
260
|
2. Only test exports that exist — read the route file before generating.
|
|
277
|
-
3. Use
|
|
278
|
-
construction.
|
|
261
|
+
3. Use `@pracht/test`'s factories for `BaseRouteArgs`/`LoaderArgs`
|
|
262
|
+
construction instead of hand-building args objects.
|
|
279
263
|
4. For routes with `getStaticPaths`, scaffold a separate test that calls it.
|
|
280
264
|
5. Generated tests should pass on first run with a placeholder assertion;
|
|
281
265
|
the user fills in real expectations.
|
|
@@ -141,6 +141,9 @@ on the router `mode` from Step 1:
|
|
|
141
141
|
`export const RENDER_MODE = "ssg"` in the page module (valid values
|
|
142
142
|
`"ssr" | "ssg" | "isg" | "spa"`; the default is `"ssr"`, overridable
|
|
143
143
|
globally via `pracht({ pagesDefaultRender: "..." })` in vite config).
|
|
144
|
+
For ISG, also add a positive integer time policy such as
|
|
145
|
+
`export const REVALIDATE = 3600`; pages mode requires it and supports
|
|
146
|
+
time-based revalidation only. Eject for webhook or combined policies.
|
|
144
147
|
Hydration is `export const HYDRATION = "..."` in the same file. If most
|
|
145
148
|
pages want the same mode, prefer changing `pagesDefaultRender` over adding
|
|
146
149
|
a constant to every file.
|
|
@@ -159,7 +162,7 @@ Apply the edits only after the user confirms.
|
|
|
159
162
|
| ---------- | -------------------------------------------------------------- | ----- |
|
|
160
163
|
| Node | Filesystem: `isg-manifest.json` + file-mtime revalidation | Serves stale immediately, refreshes in place. |
|
|
161
164
|
| Cloudflare | Worker-managed Workers Cache API, **per colo** — works without any extra config | `cloudflareAdapter({ cache: true })` + `"cache": { "enabled": true }` in wrangler config is an **optional upgrade** that moves time-revalidated routes to an edge-tier cache in front of the Worker; webhook-only routes stay worker-managed. Webhook invalidation on the default path is per-colo, not a global purge. |
|
|
162
|
-
| Vercel | Native ISR: Build Output API prerender functions with `expiration` from the time policy; `PRACHT_REVALIDATE_TOKEN` becomes the `bypassToken`
|
|
165
|
+
| Vercel | Native ISR: Build Output API prerender functions with `expiration` from the time policy; for webhook revalidation, `PRACHT_REVALIDATE_TOKEN` becomes the `bypassToken` and must be set at build time | ISG routes deploy as Node Serverless Functions (Vercel rejects ISR on an Edge Function) while SSR stays on the edge. Time-only ISR does not require the token. See docs/ADAPTERS.md. |
|
|
163
166
|
|
|
164
167
|
4. For dynamic SSG/ISG routes, ensure `getStaticPaths` exists. Flag if missing.
|
|
165
168
|
5. Use `pracht inspect routes --json` rather than reading `src/routes.ts`
|