create-pracht 0.3.0 → 0.4.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 +11 -1
- package/package.json +3 -2
- package/skills/add-auth/SKILL.md +346 -0
- package/skills/add-db/SKILL.md +316 -0
- package/skills/add-i18n/SKILL.md +239 -0
- package/skills/add-observability/SKILL.md +270 -0
- package/skills/audit-a11y/SKILL.md +170 -0
- package/skills/audit-auth/SKILL.md +144 -0
- package/skills/audit-bundles/SKILL.md +167 -0
- package/skills/audit-csrf/SKILL.md +179 -0
- package/skills/audit-deps/SKILL.md +138 -0
- package/skills/audit-headers/SKILL.md +186 -0
- package/skills/audit-islands/SKILL.md +144 -0
- package/skills/audit-loaders/SKILL.md +135 -0
- package/skills/audit-redirects/SKILL.md +146 -0
- package/skills/audit-secrets/SKILL.md +150 -0
- package/skills/audit-seo/SKILL.md +164 -0
- package/skills/audit-shells/SKILL.md +142 -0
- package/skills/configure-isg/SKILL.md +172 -0
- package/skills/migrate-nextjs/SKILL.md +536 -0
- package/skills/pracht-debug/SKILL.md +146 -0
- package/skills/pracht-deploy/SKILL.md +208 -0
- package/skills/pracht-scaffold/SKILL.md +191 -0
- package/skills/pracht-test-api/SKILL.md +165 -0
- package/skills/pre-deploy/SKILL.md +174 -0
- package/skills/scaffold-e2e/SKILL.md +189 -0
- package/skills/scaffold-tests/SKILL.md +283 -0
- package/skills/tune-render-mode/SKILL.md +168 -0
- package/skills/typed-routes/SKILL.md +207 -0
- package/skills/upgrade-pracht/SKILL.md +152 -0
- package/src/index.js +148 -10
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scaffold-tests
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Scaffold Vitest unit/integration tests for pracht routes, loaders, and
|
|
6
|
+
middleware. Asks the user once whether to use vitest browser mode with
|
|
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.
|
|
10
|
+
Use when asked to "scaffold tests", "set up Vitest", "add unit tests",
|
|
11
|
+
"test this loader", or "test this route".
|
|
12
|
+
allowed-tools:
|
|
13
|
+
- Bash
|
|
14
|
+
- Read
|
|
15
|
+
- Write
|
|
16
|
+
- Edit
|
|
17
|
+
- Grep
|
|
18
|
+
- Glob
|
|
19
|
+
- AskUserQuestion
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Pracht Scaffold Tests
|
|
23
|
+
|
|
24
|
+
Generate Vitest tests aligned with pracht's testing recipe
|
|
25
|
+
(`examples/docs/src/routes/docs/recipes-testing.md`). Loaders and API handlers
|
|
26
|
+
are plain async functions — they test directly with no framework bootstrap.
|
|
27
|
+
Component tests need a renderer; the user picks the flavor.
|
|
28
|
+
|
|
29
|
+
## Step 1: Pick the rendering strategy
|
|
30
|
+
|
|
31
|
+
Use `AskUserQuestion` to choose between:
|
|
32
|
+
|
|
33
|
+
1. **Browser mode** — `vitest` with `@vitest/browser` and
|
|
34
|
+
`vitest-browser-preact`.
|
|
35
|
+
- Pros: real browser, real events, fewer hydration false positives,
|
|
36
|
+
screenshots, works for SPA-mode interaction tests.
|
|
37
|
+
- Cons: slower, heavier setup, requires a browser binary on CI.
|
|
38
|
+
2. **JSDOM** — `vitest` with `@testing-library/preact`.
|
|
39
|
+
- Pros: fast, lightweight, runs anywhere.
|
|
40
|
+
- Cons: JSDOM lacks layout, certain DOM APIs; brittle for complex UIs.
|
|
41
|
+
|
|
42
|
+
If the project already has one configured, default to it and confirm.
|
|
43
|
+
|
|
44
|
+
## Step 2: Install dependencies
|
|
45
|
+
|
|
46
|
+
Detect the package manager from the lockfile.
|
|
47
|
+
|
|
48
|
+
Common (both modes):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add -D vitest @types/node
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Component-test extras — `@preact/preset-vite` must be an explicit dev
|
|
55
|
+
dependency: the configs in Step 3 import it, and a transitive-only copy
|
|
56
|
+
fails under pnpm's strict `node_modules`.
|
|
57
|
+
|
|
58
|
+
**Browser mode**:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm add -D @vitest/browser playwright vitest-browser-preact @preact/preset-vite
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**JSDOM mode**:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pnpm add -D jsdom @testing-library/preact @testing-library/jest-dom @preact/preset-vite
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If the project only needs loader/middleware tests (no component rendering),
|
|
71
|
+
skip the extras entirely and use the plugin-less config in Step 3.
|
|
72
|
+
|
|
73
|
+
## Step 3: Wire `vitest.config.ts`
|
|
74
|
+
|
|
75
|
+
**Browser mode**:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { defineConfig } from "vitest/config";
|
|
79
|
+
import preact from "@preact/preset-vite";
|
|
80
|
+
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
plugins: [preact()],
|
|
83
|
+
test: {
|
|
84
|
+
browser: {
|
|
85
|
+
enabled: true,
|
|
86
|
+
provider: "playwright",
|
|
87
|
+
instances: [{ browser: "chromium" }],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**JSDOM mode**:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { defineConfig } from "vitest/config";
|
|
97
|
+
import preact from "@preact/preset-vite";
|
|
98
|
+
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
plugins: [preact()],
|
|
101
|
+
test: {
|
|
102
|
+
environment: "jsdom",
|
|
103
|
+
setupFiles: ["./test/setup.ts"],
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`test/setup.ts` (JSDOM only):
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import "@testing-library/jest-dom/vitest";
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Loader/middleware-only mode** (no components) — matches the minimal config
|
|
115
|
+
in `recipes-testing.md`; no preset, no extra deps:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { defineConfig } from "vitest/config";
|
|
119
|
+
|
|
120
|
+
export default defineConfig({
|
|
121
|
+
test: {
|
|
122
|
+
// Exclude E2E tests (run those with Playwright)
|
|
123
|
+
exclude: ["e2e/**", "node_modules/**"],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If `vitest.config.ts` already exists, merge — never clobber.
|
|
129
|
+
|
|
130
|
+
## Step 4: Generate the tests
|
|
131
|
+
|
|
132
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
133
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
|
|
134
|
+
`generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
|
|
135
|
+
config with the pracht plugin registered.
|
|
136
|
+
|
|
137
|
+
This skill covers **loaders, middleware, and components**. For API handlers
|
|
138
|
+
(`src/api/**`), delegate to `/pracht-test-api` — it owns handler enumeration and
|
|
139
|
+
per-method test generation; don't duplicate a weaker version here.
|
|
140
|
+
|
|
141
|
+
Use `pracht inspect routes --json` to find targets. Ask the user which subset
|
|
142
|
+
to scaffold, or pass paths via `$ARGUMENTS`.
|
|
143
|
+
|
|
144
|
+
### Loader test template
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { describe, it, expect } from "vitest";
|
|
148
|
+
import { loader } from "./<route-file>";
|
|
149
|
+
|
|
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
|
+
describe("<route> loader", () => {
|
|
163
|
+
it("returns the expected shape", async () => {
|
|
164
|
+
const data = await loader(args("http://localhost/<path>"));
|
|
165
|
+
expect(data).toBeDefined();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Middleware test template
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { describe, it, expect } from "vitest";
|
|
174
|
+
import { middleware } from "./<middleware-file>";
|
|
175
|
+
|
|
176
|
+
describe("<name> middleware", () => {
|
|
177
|
+
const ok = new Response("ok", { status: 200 });
|
|
178
|
+
const next = async () => ok;
|
|
179
|
+
|
|
180
|
+
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/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
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,
|
|
211
|
+
);
|
|
212
|
+
expect(response).toBe(ok);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Component test template (browser mode)
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import { describe, it, expect } from "vitest";
|
|
221
|
+
import { render } from "vitest-browser-preact";
|
|
222
|
+
import { Component } from "./<route-file>";
|
|
223
|
+
|
|
224
|
+
describe("<route> component", () => {
|
|
225
|
+
it("renders the heading", async () => {
|
|
226
|
+
const screen = render(<Component data={{ /* mock loader data */ }} params={{}} />);
|
|
227
|
+
await expect.element(screen.getByRole("heading")).toBeVisible();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Component test template (JSDOM)
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { describe, it, expect } from "vitest";
|
|
236
|
+
import { render, screen } from "@testing-library/preact";
|
|
237
|
+
import { Component } from "./<route-file>";
|
|
238
|
+
|
|
239
|
+
describe("<route> component", () => {
|
|
240
|
+
it("renders the heading", () => {
|
|
241
|
+
render(<Component data={{ /* mock loader data */ }} params={{}} />);
|
|
242
|
+
expect(screen.getByRole("heading")).toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Step 5: Wire `package.json`
|
|
248
|
+
|
|
249
|
+
Add (or merge) scripts:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"scripts": {
|
|
254
|
+
"test": "vitest run",
|
|
255
|
+
"test:watch": "vitest"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
If `pnpm test` already exists, do not overwrite.
|
|
261
|
+
|
|
262
|
+
## Step 6: Verify
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
pnpm test
|
|
266
|
+
pracht verify --json
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
If anything fails on first run, report the failure and the fix. Do not commit
|
|
270
|
+
broken scaffolding.
|
|
271
|
+
|
|
272
|
+
## Rules
|
|
273
|
+
|
|
274
|
+
1. Ask the rendering-strategy question once per project; persist by
|
|
275
|
+
inspecting `vitest.config.ts` on subsequent runs.
|
|
276
|
+
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.
|
|
279
|
+
4. For routes with `getStaticPaths`, scaffold a separate test that calls it.
|
|
280
|
+
5. Generated tests should pass on first run with a placeholder assertion;
|
|
281
|
+
the user fills in real expectations.
|
|
282
|
+
|
|
283
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tune-render-mode
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Recommend the right pracht render mode (ssg, isg, ssr, spa) for each route
|
|
6
|
+
based on what its loader actually does. Most apps pick a mode once and never
|
|
7
|
+
revisit; this skill surfaces routes that are mis-tuned.
|
|
8
|
+
Use when asked to "tune render modes", "make my site faster", "should this
|
|
9
|
+
route be SSG", "audit render modes", or "review SSG/ISG/SSR choices".
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Edit
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Pracht Tune Render Mode
|
|
19
|
+
|
|
20
|
+
Walk every route, read its loader, and recommend the cheapest render mode that
|
|
21
|
+
still satisfies the route's data dependencies.
|
|
22
|
+
|
|
23
|
+
This is a **tune** skill, not a report-only audit: it ends by applying edits.
|
|
24
|
+
The contract is propose-then-apply — produce the recommendation table and the
|
|
25
|
+
exact diffs first, then apply them **only after the user explicitly confirms**
|
|
26
|
+
(per route or as a batch). Never edit before that confirmation.
|
|
27
|
+
|
|
28
|
+
## Decision Tree
|
|
29
|
+
|
|
30
|
+
For each route:
|
|
31
|
+
|
|
32
|
+
1. **No `loader`, no `getStaticPaths`, no per-request data** → **`ssg`**
|
|
33
|
+
- Pure UI. Build once, serve from CDN. Highest performance.
|
|
34
|
+
|
|
35
|
+
2. **Loader reads only build-time-stable data** (filesystem, static config,
|
|
36
|
+
typed CMS export, no `request`/`params`/`context.env` use) → **`ssg`** or
|
|
37
|
+
**`isg`**
|
|
38
|
+
- Pick `isg` with `timeRevalidate(seconds)` if the source can change between
|
|
39
|
+
deploys (CMS, pricing pages, public catalog).
|
|
40
|
+
- Pick `ssg` if the source only changes when you redeploy.
|
|
41
|
+
|
|
42
|
+
3. **Loader reads `params` to fetch data, but not `request`/cookies** →
|
|
43
|
+
**`ssg`** with `getStaticPaths`, or **`isg`** if the universe of params is
|
|
44
|
+
open-ended (millions of slugs).
|
|
45
|
+
|
|
46
|
+
4. **Loader reads `request`, but the data is shareable** (`request-static`:
|
|
47
|
+
request used only for cache keys like `Accept-Language`, never for
|
|
48
|
+
identity) → **`ssr`** by default; **`isg`** is possible on adapters whose
|
|
49
|
+
cache can key on the varying dimension (e.g. a normalized cache key at a
|
|
50
|
+
Cloudflare gateway). If the variant fan-out is unbounded or the adapter
|
|
51
|
+
cache can't express the Vary, stay on `ssr`.
|
|
52
|
+
|
|
53
|
+
5. **Loader reads cookies, auth headers, `context.env` per-request, or
|
|
54
|
+
anything personalized** → **`ssr`**
|
|
55
|
+
- Auth dashboards, anything user-specific, anything that varies by user
|
|
56
|
+
identity at request time.
|
|
57
|
+
|
|
58
|
+
6. **Heavy client interactivity, no SEO need, auth-gated** → **`spa`**
|
|
59
|
+
- Internal admin tools, post-login dashboards where the first paint can be a
|
|
60
|
+
skeleton.
|
|
61
|
+
|
|
62
|
+
## Step 1: Enumerate
|
|
63
|
+
|
|
64
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
65
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
66
|
+
shelling out.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pracht inspect routes --json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
|
|
73
|
+
wired up.
|
|
74
|
+
|
|
75
|
+
Capture: `path`, `file`, current `render`, `revalidate`, `middleware`, and
|
|
76
|
+
the top-level `mode` field (manifest vs pages router — Step 4 depends on it).
|
|
77
|
+
|
|
78
|
+
## Step 2: Read each loader
|
|
79
|
+
|
|
80
|
+
For each route, open the file and look at the `loader`/`getStaticPaths`
|
|
81
|
+
exports. Tag the loader with one of:
|
|
82
|
+
|
|
83
|
+
- `none` — no loader at all
|
|
84
|
+
- `static` — only reads imports / pure data
|
|
85
|
+
- `param-static` — reads `params` only
|
|
86
|
+
- `request-static` — reads `request` for cache keys but data is shareable
|
|
87
|
+
(e.g. `Accept-Language`) — decision-tree branch 4
|
|
88
|
+
- `request-personalized` — reads cookies, auth headers, user-specific
|
|
89
|
+
`context.env` lookups — decision-tree branch 5
|
|
90
|
+
|
|
91
|
+
## Step 3: Recommend
|
|
92
|
+
|
|
93
|
+
Produce a table:
|
|
94
|
+
|
|
95
|
+
| Route | Current | Recommended | Severity | Reason |
|
|
96
|
+
| ----- | ------- | ----------- | -------- | ------ |
|
|
97
|
+
|
|
98
|
+
Severity: `error` (broken today, e.g. `ssg` with a loader that reads
|
|
99
|
+
`request` — cannot be prerendered correctly), `warn` (works but mis-tuned,
|
|
100
|
+
e.g. `ssr` with no loader), `info` (optional improvement, e.g. hydration
|
|
101
|
+
tuning).
|
|
102
|
+
|
|
103
|
+
Examples of recommendations:
|
|
104
|
+
- `ssr` → `ssg` when loader is empty: "no loader; no per-request data — make it
|
|
105
|
+
static." (`warn`)
|
|
106
|
+
- `ssr` → `isg(3600)` when loader fetches a public CMS: "shared data, freshness
|
|
107
|
+
acceptable at 1 hour." (`warn`)
|
|
108
|
+
- `ssg` → `ssr` when loader reads `request.headers.get('cookie')`: "reads
|
|
109
|
+
request — cannot be prerendered." (`error`)
|
|
110
|
+
- `spa` → `ssr` when route has SEO-relevant `head()` and unauthenticated
|
|
111
|
+
visitors should see content. (`warn`)
|
|
112
|
+
|
|
113
|
+
## Step 3b: Consider the hydration mode too
|
|
114
|
+
|
|
115
|
+
Render mode controls when HTML is generated; **hydration mode** controls how
|
|
116
|
+
much JavaScript ships afterwards (`hydration: "full" | "islands" | "none"`,
|
|
117
|
+
default `"full"` — see `docs/ISLANDS.md`). A current `@pracht/cli` emits the
|
|
118
|
+
resolved `hydration` per route in the inspect JSON; if your CLI predates the
|
|
119
|
+
field (absent from the JSON), grep the manifest for `hydration:` (pages apps:
|
|
120
|
+
`HYDRATION` exports) instead. While tuning, also flag:
|
|
121
|
+
|
|
122
|
+
- Routes with **no interactivity at all** (no event handlers, no hooks) →
|
|
123
|
+
`hydration: "none"` — zero JS shipped.
|
|
124
|
+
- Content-heavy routes with **one or two isolated widgets** (counter, search
|
|
125
|
+
box, newsletter form) → `hydration: "islands"` with the widgets moved to
|
|
126
|
+
`src/islands/`.
|
|
127
|
+
- Caveats: islands routes use MPA-style full-document navigation (no client
|
|
128
|
+
router), island props must be JSON-serializable, and `render: "spa"` cannot
|
|
129
|
+
combine with `"islands"`/`"none"`.
|
|
130
|
+
|
|
131
|
+
## Step 4: Propose diffs, then apply on confirmation
|
|
132
|
+
|
|
133
|
+
Present the exact edits and wait for approval. Where the edit lands depends
|
|
134
|
+
on the router `mode` from Step 1:
|
|
135
|
+
|
|
136
|
+
- **Manifest apps**: edit `src/routes.ts` to update the `render` field. For
|
|
137
|
+
ISG, add `revalidate: timeRevalidate(N)` and import `timeRevalidate` from
|
|
138
|
+
`@pracht/core`. Hydration changes update the `hydration` field the same
|
|
139
|
+
way.
|
|
140
|
+
- **Pages apps**: render mode is a per-file constant —
|
|
141
|
+
`export const RENDER_MODE = "ssg"` in the page module (valid values
|
|
142
|
+
`"ssr" | "ssg" | "isg" | "spa"`; the default is `"ssr"`, overridable
|
|
143
|
+
globally via `pracht({ pagesDefaultRender: "..." })` in vite config).
|
|
144
|
+
Hydration is `export const HYDRATION = "..."` in the same file. If most
|
|
145
|
+
pages want the same mode, prefer changing `pagesDefaultRender` over adding
|
|
146
|
+
a constant to every file.
|
|
147
|
+
|
|
148
|
+
Apply the edits only after the user confirms.
|
|
149
|
+
|
|
150
|
+
## Rules
|
|
151
|
+
|
|
152
|
+
1. Never silently change render modes. Always present the recommendation and
|
|
153
|
+
the exact diff first; apply only after explicit user approval.
|
|
154
|
+
2. If a route uses `auth` middleware, default to `ssr` — auth implies cookies.
|
|
155
|
+
3. All three adapters support ISG — the mechanisms differ. Confirm which
|
|
156
|
+
adapter is in play, then use this capability table:
|
|
157
|
+
|
|
158
|
+
| Adapter | ISG mechanism (default) | Notes |
|
|
159
|
+
| ---------- | -------------------------------------------------------------- | ----- |
|
|
160
|
+
| Node | Filesystem: `isg-manifest.json` + file-mtime revalidation | Serves stale immediately, refreshes in place. |
|
|
161
|
+
| 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. |
|
|
163
|
+
|
|
164
|
+
4. For dynamic SSG/ISG routes, ensure `getStaticPaths` exists. Flag if missing.
|
|
165
|
+
5. Use `pracht inspect routes --json` rather than reading `src/routes.ts`
|
|
166
|
+
manually — the resolved graph already accounts for groups and inheritance.
|
|
167
|
+
|
|
168
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typed-routes
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Add or maintain pracht typed routes, typed links, route-object navigation,
|
|
6
|
+
and generated href helpers. Use when asked to "add typed routes", "fix typed
|
|
7
|
+
links", "replace hard-coded hrefs", "run typegen", or make navigation route-id
|
|
8
|
+
based instead of string based.
|
|
9
|
+
allowed-tools:
|
|
10
|
+
- Bash
|
|
11
|
+
- Read
|
|
12
|
+
- Write
|
|
13
|
+
- Edit
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
- AskUserQuestion
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Pracht Typed Routes
|
|
20
|
+
|
|
21
|
+
Use this workflow to keep route ids, params, links, and navigation type-safe.
|
|
22
|
+
|
|
23
|
+
## Step 1: Inspect the resolved graph
|
|
24
|
+
|
|
25
|
+
The resolved app graph is the source of truth — not a manual glob of `src/`.
|
|
26
|
+
|
|
27
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
28
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
|
|
29
|
+
`generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
|
|
30
|
+
config with the pracht plugin registered.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pracht inspect routes --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Check every route has a stable id. Explicit `id` fields are preferred for routes
|
|
37
|
+
that app code links to, because fallback ids change when paths change.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
route("/products/:id", () => import("./routes/products/[id].tsx"), {
|
|
41
|
+
id: "product",
|
|
42
|
+
render: "ssr",
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Any route without an explicit `id` — manifest apps included, not just
|
|
47
|
+
pages-router apps — gets a fallback id derived from the route path (`/` →
|
|
48
|
+
`index`, `/blog/:slug` → `blog-slug`, `/*` → `splat`).
|
|
49
|
+
|
|
50
|
+
## Step 2: Generate route types and helpers
|
|
51
|
+
|
|
52
|
+
Run:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pracht typegen
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This writes:
|
|
59
|
+
|
|
60
|
+
- `src/pracht.d.ts` — module augmentation for route ids, params, loader data
|
|
61
|
+
types, and API route request/response types (consumed by `apiFetch()`).
|
|
62
|
+
- `src/pracht-routes.ts` — runtime `href()` helper backed by the same route map.
|
|
63
|
+
- `src/pracht-capabilities.d.ts` — only when the app registers capabilities:
|
|
64
|
+
input/output types from the capability schemas, so `invokeCapability()`,
|
|
65
|
+
`callCapability()`, and `<Form capability>`'s `onCapabilityResult` infer
|
|
66
|
+
types from the capability name.
|
|
67
|
+
|
|
68
|
+
Earlier versions wrote the declaration to `src/pracht-routes.d.ts`; typegen
|
|
69
|
+
removes that stale file automatically (TypeScript silently ignored it next to
|
|
70
|
+
the same-named `.ts` helper).
|
|
71
|
+
|
|
72
|
+
Do not hand-edit generated files. If they are stale, update the route graph and
|
|
73
|
+
run typegen again — or rely on `pracht dev`, which refreshes them when route
|
|
74
|
+
files are added, removed, or renamed and when the route manifest or an imported
|
|
75
|
+
definition module changes. The dev banner prompts for the initial typegen run
|
|
76
|
+
when `src/pracht.d.ts` does not exist. In CI, prefer:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pracht typegen --check
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Step 3: Replace string navigation where it matters
|
|
83
|
+
|
|
84
|
+
### Components
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { Link, useNavigate } from "@pracht/core";
|
|
88
|
+
|
|
89
|
+
export function ProductLink({ id }: { id: string }) {
|
|
90
|
+
const navigate = useNavigate();
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<Link route="product" params={{ id }} search={{ ref: "home" }}>
|
|
95
|
+
View product
|
|
96
|
+
</Link>
|
|
97
|
+
<button onClick={() => void navigate({ route: "product", params: { id } })}>
|
|
98
|
+
Open product
|
|
99
|
+
</button>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`<Link>` renders a normal `<a>` and the client router intercepts it like any
|
|
106
|
+
same-origin anchor. It also accepts navigation-behavior props:
|
|
107
|
+
`prefetch="none" | "hover" | "intent" | "viewport" | "render"` (per-link
|
|
108
|
+
prefetch strategy, default `"intent"`), `preserveScroll` (keep the scroll
|
|
109
|
+
position), and `viewTransition` (animate the navigation with the View Transitions API
|
|
110
|
+
where supported). There is also an imperative `prefetch()` export and a
|
|
111
|
+
`useNavigation()` hook for pending navigation/submission state.
|
|
112
|
+
|
|
113
|
+
### Outside components
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { href } from "./pracht-routes";
|
|
117
|
+
|
|
118
|
+
const productUrl = href("product", {
|
|
119
|
+
params: { id: "123" },
|
|
120
|
+
search: { tab: "details" },
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use `href()` in loaders that return URLs, sitemap helpers, menu config, test
|
|
125
|
+
fixtures, and other non-component code.
|
|
126
|
+
|
|
127
|
+
### Loader data
|
|
128
|
+
|
|
129
|
+
After typegen, `useRouteData(routeId)` returns that route's loader data with
|
|
130
|
+
no generic — route ids autocomplete and the type follows the route's loader
|
|
131
|
+
(or its separate loader file from the manifest):
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { useRouteData } from "@pracht/core";
|
|
135
|
+
|
|
136
|
+
export function Component() {
|
|
137
|
+
const data = useRouteData("product");
|
|
138
|
+
return <h1>{data.product.name}</h1>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Prefer this over `useRouteData<typeof loader>()` when typegen runs; keep the
|
|
143
|
+
generic form for projects that do not generate route types. Routes without a
|
|
144
|
+
loader type their data as `undefined`. The id must be the active route — dev
|
|
145
|
+
mode warns on mismatches.
|
|
146
|
+
|
|
147
|
+
### API routes
|
|
148
|
+
|
|
149
|
+
After typegen, `apiFetch()` type-checks API calls end to end — paths,
|
|
150
|
+
methods, params, bodies and queries (for `defineApi()` routes), and response
|
|
151
|
+
types:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { apiFetch } from "@pracht/core";
|
|
155
|
+
|
|
156
|
+
const item = await apiFetch("/api/items/:id", { params: { id: "42" } });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
See docs/API_VALIDATION.md for `defineApi()` and validation error handling.
|
|
160
|
+
Typegen discovers API route files without importing them, so it is safe for
|
|
161
|
+
route modules that initialize runtime-only services at module scope.
|
|
162
|
+
|
|
163
|
+
Query and params values cross the wire as strings — write schemas that accept
|
|
164
|
+
string input (`z.coerce.number()`, not `z.number()`); `apiFetch()` rejects
|
|
165
|
+
query and params keys without a string representation at compile time when the
|
|
166
|
+
schema exposes a concrete input type. Handlers that need a custom status keep
|
|
167
|
+
typed payloads with `json(value, { status })`.
|
|
168
|
+
|
|
169
|
+
## Step 4: Param and search rules
|
|
170
|
+
|
|
171
|
+
Generated param types accept `RouteParamInput = string | number | boolean`
|
|
172
|
+
(values are stringified into the path), so:
|
|
173
|
+
|
|
174
|
+
- `:id` requires `params: { id: RouteParamInput }` — a `string` is typical,
|
|
175
|
+
but `number`/`boolean` also typecheck.
|
|
176
|
+
- `*` requires `params: { "*": RouteParamInput }`.
|
|
177
|
+
- `:path*` requires `params: { path: RouteParamInput }`.
|
|
178
|
+
- Routes with no dynamic segments should omit `params`.
|
|
179
|
+
- Missing and extra params should fail at typecheck time.
|
|
180
|
+
- `search` currently accepts `string`, `URLSearchParams`, or an object of
|
|
181
|
+
primitive values/arrays; route-specific search schemas can be added later.
|
|
182
|
+
|
|
183
|
+
## Step 5: Verify
|
|
184
|
+
|
|
185
|
+
Run at least:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pracht typegen --check
|
|
189
|
+
pnpm typecheck
|
|
190
|
+
pracht verify --json
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If navigation changed, add or update Playwright coverage for both the rendered
|
|
194
|
+
anchor `href` and client-side navigation without a full page reload.
|
|
195
|
+
|
|
196
|
+
## Rules
|
|
197
|
+
|
|
198
|
+
1. Always start from `pracht inspect routes --json` or `pracht typegen`; do not
|
|
199
|
+
infer the full route map from files by hand.
|
|
200
|
+
2. Prefer adding explicit ids before converting links for important routes.
|
|
201
|
+
3. Never edit `src/pracht.d.ts` or `src/pracht-routes.ts` manually.
|
|
202
|
+
4. Keep plain `<a href="...">` where a URL is genuinely external, opaque, or
|
|
203
|
+
user-provided.
|
|
204
|
+
5. After adding/removing/renaming routes, run `pracht typegen` and include the
|
|
205
|
+
generated file changes in the same commit.
|
|
206
|
+
|
|
207
|
+
$ARGUMENTS
|