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.
@@ -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.1.0
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 a small helper to construct
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(args("http://localhost<api-path>"));
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(args("http://localhost<api-path>", { method: "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(args("http://localhost<api-path>"));
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.json();
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.2.0
3
+ version: 1.3.0
4
4
  description: |
5
5
  Adapter-aware pre-deployment checklist for pracht apps targeting Node,
6
- Cloudflare Workers, or Vercel. Catches the issues that only surface in the
7
- production runtime: missing env vars, Node-only APIs in edge bundles,
8
- ISG manifest absence, oversized edge bundles, missing wrangler/vercel config.
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()`, or
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 recent.
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: pracht **always** writes the function's
138
- `.vc-config.json` with `runtime: "edge"` there is no Node runtime
139
- variant, so run the same Node-only API check as Cloudflare
140
- **unconditionally** for Vercel builds. Do not skip it based on a runtime
141
- probe.
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. 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.
171
- 5. Do not deploy on the user's behalf. End the skill at the verdict.
172
- 6. If `pracht doctor` reports errors, do not run any other checks until those
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.1.0
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`, mocks
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(args("http://localhost/<path>"));
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 request = new Request("http://localhost/dashboard");
182
- const response = await middleware(
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 request = new Request("http://localhost/dashboard", {
199
- headers: { cookie: "session=valid" },
200
- });
201
- const response = await middleware(
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(ok);
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 the recipe's `args()` helper shape for `BaseRouteArgs`/`LoaderArgs`
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` (must be set at build time) | See docs/ADAPTERS.md. |
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`