create-pracht 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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`