create-pracht 0.2.6 → 0.4.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.
@@ -0,0 +1,519 @@
1
+ ---
2
+ name: migrate-nextjs
3
+ version: 1.1.0
4
+ description: |
5
+ Migrate a Next.js application to Pracht. Converts App Router pages, layouts,
6
+ middleware, API routes, data fetching, and metadata to pracht equivalents.
7
+ Handles React→Preact, className→class, server components→loaders, and
8
+ manifest wiring.
9
+ Use when asked to "migrate from next", "convert next.js app", "port from
10
+ next to pracht", "nextjs migration", or "switch from next".
11
+ allowed-tools:
12
+ - Bash
13
+ - Read
14
+ - Write
15
+ - Edit
16
+ - Grep
17
+ - Glob
18
+ - AskUserQuestion
19
+ ---
20
+
21
+ # Migrate Next.js to Pracht
22
+
23
+ Systematically migrate a Next.js application (App Router or Pages Router) to pracht — a full-stack Preact framework built on Vite.
24
+
25
+ ## Step 0: Assess the source project
26
+
27
+ Before touching any code, understand what you're migrating:
28
+
29
+ 1. Read `next.config.js` / `next.config.mjs` / `next.config.ts` for custom config.
30
+ 2. Read `package.json` for React/Next versions and dependencies.
31
+ 3. Scan the directory structure:
32
+ - `app/` → App Router (Next 13+)
33
+ - `pages/` → Pages Router (legacy)
34
+ - `middleware.ts` → edge middleware
35
+ - `app/api/` or `pages/api/` → API routes
36
+ 4. Identify rendering patterns in use:
37
+ - `"use client"` directives → client components
38
+ - `async` page/layout components → server components with data fetching
39
+ - `generateStaticParams` → static generation
40
+ - `generateMetadata` / `metadata` export → head management
41
+ - Server Actions (`"use server"`) → mutations
42
+ 5. Note third-party integrations (auth, CMS, DB, analytics).
43
+
44
+ Ask the user to confirm the migration scope if the project is large (>20 routes).
45
+
46
+ If the pracht MCP server is registered (docs/MCP.md), use the `generate_route`/`generate_shell`/`generate_middleware`/`generate_api` MCP tools for scaffolding and `inspect_routes`/`inspect_api`/`doctor`/`verify` to check migration progress, instead of Bash. (`pracht inspect` needs the pracht plugin in the vite config; `inspect_build` needs a prior `pracht build`.)
47
+
48
+ ## Fast Path: Pages Router Projects
49
+
50
+ If the source Next.js project uses the **pages router** (`pages/` directory), pracht's `pagesDir` plugin option provides a near-drop-in migration:
51
+
52
+ 1. Set `pracht({ pagesDir: "/src/pages" })` in `vite.config.ts`
53
+ 2. Copy `pages/` to `src/pages/`
54
+ 3. Convert `_app.tsx` to pracht shell format (`Shell` export + `children` prop)
55
+ 4. Convert `getServerSideProps`/`getStaticProps` to `loader` exports
56
+ 5. Add `export const RENDER_MODE = "ssg"` to static pages, `"ssr"` for dynamic (default is `"ssr"`)
57
+ 6. Run dev server, iterate on errors
58
+ 7. Optionally run `generateRoutesFile` to eject to explicit manifest
59
+
60
+ For pages router projects, you can **skip manual manifest wiring entirely** (Phase 7 below).
61
+
62
+ ## Concept Mapping
63
+
64
+ | Next.js | Pracht | Notes |
65
+ | ------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------- |
66
+ | `pages/` directory | `pagesDir` plugin option | Auto-discovers routes from file system |
67
+ | `app/page.tsx` | `src/routes/*.tsx` + `route()` in manifest | File is a module; wiring is explicit |
68
+ | `app/layout.tsx` | `src/shells/*.tsx` + `shells` in `defineApp` | Shells are named, not directory-nested |
69
+ | `app/loading.tsx` | `Loading` export on the shell | Rendered as SSR placeholder for SPA routes until the client router takes over |
70
+ | `app/error.tsx` | `ErrorBoundary` export in route module | Same concept, different wiring |
71
+ | `app/not-found.tsx` | `notFound:` in `defineApp` (or `pages/404.tsx` in pagesDir mode) | Not a route — never matches a URL, so it cannot shadow static assets |
72
+ | `middleware.ts` | `src/middleware/*.ts` + `middleware` in `defineApp` | Named, applied per route/group |
73
+ | `app/api/*/route.ts` | `src/api/*.ts` with `GET`/`POST` exports | Auto-discovered, no manifest entry |
74
+ | `generateStaticParams` | `getStaticPaths()` export | Returns `RouteParams[]` of param objects |
75
+ | `generateMetadata` | `head()` export | Returns `{ title, meta }` |
76
+ | Server Components | `loader()` export | Data fetching moves to loader; component is always a Preact component |
77
+ | `"use server"` actions | API routes + `<Form>` / `fetch` | Mutations move to `src/api/*`; return `Response` objects |
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
+ | `revalidatePath` / `res.revalidate()` | `webhookRevalidate()` + `POST /__pracht/revalidate` | On-demand ISG regeneration; combinable with `timeRevalidate(seconds)` |
80
+ | `useRouter()` (next/navigation) | `useNavigate()` from pracht | Accepts paths or typed route targets after `pracht typegen` |
81
+ | `useSearchParams()` | `useLocation()` from pracht | Returns `{ pathname, search }`; loaders also receive `url` with searchParams |
82
+ | `useParams()` | `useParams()` from pracht | Direct equivalent; also available as `params` in loader args |
83
+ | `next/link` `<Link>` | `<Link route="...">` or plain `<a>` | Prefer typed `<Link>` for known app routes after `pracht typegen`; plain anchors still work |
84
+ | `next/link` `prefetch={false}` | `<Link prefetch="none">` | Pracht prefetches on hover/focus by default; also `"viewport"`, `"render"` |
85
+ | `useLinkStatus()` / pending UI | `useNavigation()` | `{ state, location, formData }` — powers progress bars and optimistic UI |
86
+ | `next/image` | Standard `<img>` | Use `vite-imagetools` plugin if optimization needed |
87
+ | `next/head` or Metadata API | `head()` export on route/shell | Per-route and per-shell head merging |
88
+ | `className` | `class` | Preact uses `class` attribute |
89
+ | `React.useState` etc. | `import { useState } from "preact/hooks"` | Preact hooks API is compatible |
90
+ | `React.useEffect` | `import { useEffect } from "preact/hooks"` | Same API |
91
+ | `import React from "react"` | Remove — no import needed | Pracht's Vite plugin handles JSX automatically |
92
+
93
+ ## Migration Procedure
94
+
95
+ ### Phase 1: Project setup
96
+
97
+ 1. Initialize the pracht project structure:
98
+ ```
99
+ src/
100
+ routes.ts # Route manifest
101
+ routes/ # Route modules
102
+ shells/ # Layout shells
103
+ middleware/ # Server-side middleware
104
+ api/ # API routes
105
+ ```
106
+ 2. Create `vite.config.ts`:
107
+
108
+ ```ts
109
+ import { defineConfig } from "vite";
110
+ import { pracht } from "@pracht/vite-plugin";
111
+
112
+ export default defineConfig({
113
+ plugins: [pracht()],
114
+ });
115
+ ```
116
+
117
+ 3. Update `package.json`:
118
+ - Replace `react`, `react-dom` → `preact`
119
+ - Replace `next` → `@pracht/core` (framework runtime), `@pracht/cli` (provides the `pracht` bin), `@pracht/vite-plugin`, and `@pracht/adapter-node` (or target adapter). There is no package named `pracht`.
120
+ - Update scripts: `dev` → `pracht dev`, `build` → `pracht build`, `start` → `node dist/server/server.js` (Node.js) or a platform-specific deploy command; add `preview` → `pracht preview` to serve the production build locally
121
+ 4. Remove Next.js config files: `next.config.*`, `next-env.d.ts`, `.next/`
122
+ 5. If `tsconfig.json` has `"jsx": "preserve"`, change to `"jsx": "react-jsx"` and add `"jsxImportSource": "preact"`.
123
+
124
+ ### Phase 2: Convert layouts → shells
125
+
126
+ For each `layout.tsx`:
127
+
128
+ **Next.js:**
129
+
130
+ ```tsx
131
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
132
+ return (
133
+ <html>
134
+ <body className="root">{children}</body>
135
+ </html>
136
+ );
137
+ }
138
+ ```
139
+
140
+ **Pracht:**
141
+
142
+ ```tsx
143
+ import type { ShellProps } from "@pracht/core";
144
+
145
+ export function Shell({ children }: ShellProps) {
146
+ return (
147
+ <div class="root">
148
+ <main>{children}</main>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ export function head() {
154
+ return { title: "My App" };
155
+ }
156
+ ```
157
+
158
+ Key differences:
159
+
160
+ - Pracht shells do NOT render `<html>`, `<head>`, or `<body>` — the framework owns the HTML document.
161
+ - Use `class` not `className`.
162
+ - Register in `defineApp({ shells: { main: "./shells/main.tsx" } })`.
163
+
164
+ ### Phase 3: Convert pages → route modules
165
+
166
+ For each `page.tsx`:
167
+
168
+ **Next.js (Server Component with data):**
169
+
170
+ ```tsx
171
+ async function getData() {
172
+ const res = await fetch("https://api.example.com/data");
173
+ return res.json();
174
+ }
175
+
176
+ export default async function Page() {
177
+ const data = await getData();
178
+ return <div className="page">{data.title}</div>;
179
+ }
180
+
181
+ export async function generateMetadata() {
182
+ const data = await getData();
183
+ return { title: data.title };
184
+ }
185
+ ```
186
+
187
+ **Pracht:**
188
+
189
+ ```tsx
190
+ import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
191
+
192
+ export async function loader(_args: LoaderArgs) {
193
+ const res = await fetch("https://api.example.com/data");
194
+ return res.json();
195
+ }
196
+
197
+ export function head({ data }: { data: Awaited<ReturnType<typeof loader>> }) {
198
+ return { title: data.title };
199
+ }
200
+
201
+ export default function Page({ data }: RouteComponentProps<typeof loader>) {
202
+ return <div class="page">{data.title}</div>;
203
+ }
204
+ ```
205
+
206
+ Key transforms:
207
+
208
+ - Server-side data fetching → `loader()` export
209
+ - `generateMetadata` → `head()` export
210
+ - Keep `export default function Page` as the page component
211
+ - `className` → `class`
212
+ - No `async` components — data comes via props from loader
213
+
214
+ ### Phase 4: Convert client components
215
+
216
+ **Next.js:**
217
+
218
+ ```tsx
219
+ "use client";
220
+ import { useState } from "react";
221
+
222
+ export default function Counter() {
223
+ const [count, setCount] = useState(0);
224
+ return <button onClick={() => setCount(count + 1)}>{count}</button>;
225
+ }
226
+ ```
227
+
228
+ **Pracht:**
229
+
230
+ ```tsx
231
+ import { useState } from "preact/hooks";
232
+
233
+ export function Counter() {
234
+ const [count, setCount] = useState(0);
235
+ return <button onClick={() => setCount(count + 1)}>{count}</button>;
236
+ }
237
+ ```
238
+
239
+ Key transforms:
240
+
241
+ - Remove `"use client"` directive — not needed in pracht
242
+ - `import { ... } from "react"` → `import { ... } from "preact/hooks"` or `import { ... } from "preact/compat"`
243
+ - `import { ... } from "react-dom"` → `import { ... } from "preact/compat"`
244
+
245
+ **Islands note:** if the source app is mostly server components with only a handful of `"use client"` components, don't silently regress those pages to full-page hydration. Set `hydration: "islands"` on the route (or `export const HYDRATION = "islands"` in pages mode) and move the interactive components to `src/islands/` — the rest of the page renders as inert HTML and only the islands ship JavaScript. See `docs/ISLANDS.md`.
246
+
247
+ ### Phase 5: Convert API routes
248
+
249
+ **Next.js (`app/api/users/route.ts`):**
250
+
251
+ ```ts
252
+ import { NextRequest, NextResponse } from "next/server";
253
+
254
+ export async function GET(request: NextRequest) {
255
+ const users = await getUsers();
256
+ return NextResponse.json(users);
257
+ }
258
+ ```
259
+
260
+ **Pracht (`src/api/users.ts`):**
261
+
262
+ ```ts
263
+ import type { ApiRouteArgs } from "@pracht/core";
264
+
265
+ export async function GET({ request }: ApiRouteArgs) {
266
+ const users = await getUsers();
267
+ return Response.json(users);
268
+ }
269
+ ```
270
+
271
+ Key transforms:
272
+
273
+ - `NextRequest` → standard `Request` (via `ApiRouteArgs`)
274
+ - `NextResponse.json()` → `Response.json()` (Web standard)
275
+ - Dynamic segments: `app/api/users/[id]/route.ts` → `src/api/users/[id].ts`
276
+ - No manifest wiring needed — auto-discovered
277
+
278
+ ### Phase 6: Convert middleware
279
+
280
+ **Next.js (`middleware.ts`):**
281
+
282
+ ```ts
283
+ import { NextResponse } from "next/server";
284
+ import type { NextRequest } from "next/server";
285
+
286
+ export function middleware(request: NextRequest) {
287
+ const session = request.cookies.get("session");
288
+ if (!session) return NextResponse.redirect(new URL("/login", request.url));
289
+ return NextResponse.next();
290
+ }
291
+
292
+ export const config = { matcher: ["/dashboard/:path*"] };
293
+ ```
294
+
295
+ **Pracht (`src/middleware/auth.ts`):**
296
+
297
+ ```ts
298
+ import { redirect, type MiddlewareFn } from "@pracht/core";
299
+
300
+ export const middleware: MiddlewareFn = async ({ request }, next) => {
301
+ const session = request.headers.get("cookie")?.includes("session");
302
+ if (!session) return redirect("/login", { request });
303
+ return next();
304
+ };
305
+ ```
306
+
307
+ Then apply it in the manifest:
308
+
309
+ ```ts
310
+ group({ middleware: ["auth"] }, [
311
+ route("/dashboard", () => import("./routes/dashboard.tsx"), { render: "ssr" }),
312
+ ]);
313
+ ```
314
+
315
+ Key transforms:
316
+
317
+ - Path matching moves from `config.matcher` to manifest group/route assignment
318
+ - `NextResponse.redirect()` → `return redirect("/path", { request })`
319
+ - `NextResponse.next()` → `return next()`
320
+ - Pracht middleware is **wrap-around** (Hono/Koa/Astro shape), so you can
321
+ also `await next()` and observe the response — useful for tracing.
322
+
323
+ ### Phase 7: Wire the route manifest
324
+
325
+ **Note:** For pages router projects using `pagesDir`, this phase is automatic. Skip to Phase 8.
326
+
327
+ Instead of hand-writing every entry, prefer `pracht generate route --path ... --render ...` (with `--shell`/`--middleware`/`--loader` as needed) per page: it creates a wired skeleton **and** updates `src/routes.ts` for you — then port the Next.js component/loader bodies into the generated files. Hand-write the manifest only for shapes the generator cannot express.
328
+
329
+ Build `src/routes.ts` mapping every migrated page. Module references accept `() => import("./path")` (enables IDE navigation) or plain `"./path"` strings — both work:
330
+
331
+ ```ts
332
+ import { defineApp, group, route } from "@pracht/core";
333
+
334
+ export const app = defineApp({
335
+ shells: {
336
+ main: () => import("./shells/main.tsx"),
337
+ },
338
+ middleware: {
339
+ auth: () => import("./middleware/auth.ts"),
340
+ },
341
+ routes: [
342
+ group({ shell: "main" }, [
343
+ route("/", () => import("./routes/home.tsx"), { render: "ssg" }),
344
+ route("/about", () => import("./routes/about.tsx"), { render: "ssg" }),
345
+ route("/dashboard", () => import("./routes/dashboard.tsx"), {
346
+ render: "ssr",
347
+ middleware: ["auth"],
348
+ }),
349
+ route("/blog/:slug", () => import("./routes/blog-post.tsx"), { render: "isg" }),
350
+ ]),
351
+ ],
352
+ notFound: {
353
+ component: () => import("./routes/not-found.tsx"),
354
+ shell: "main",
355
+ },
356
+ });
357
+ ```
358
+
359
+ Choose render modes based on the Next.js original:
360
+
361
+ - Static pages (no data fetching, or `generateStaticParams`) → `"ssg"`
362
+ - Dynamic pages (`cookies()`, `headers()`, per-request data) → `"ssr"`
363
+ - ISR pages (`revalidate` option) → `"isg"` with `timeRevalidate(seconds)`
364
+ - On-demand ISR (`revalidatePath` / `res.revalidate()`) → add `webhookRevalidate()` (alone or as `[timeRevalidate(seconds), webhookRevalidate()]`) and trigger via `POST /__pracht/revalidate`
365
+ - Client-only pages → `"spa"`
366
+
367
+ ### Phase 8: Handle common patterns
368
+
369
+ #### `next/link` → typed `<Link>` or plain `<a>`
370
+
371
+ After manifest wiring is in place, run `pracht typegen` and prefer route-id based links for known app routes:
372
+
373
+ ```tsx
374
+ // Next.js
375
+ import Link from "next/link";
376
+ <Link href={`/products/${id}`}>Product</Link>
377
+
378
+ // Pracht
379
+ import { Link } from "@pracht/core";
380
+ <Link route="product" params={{ id }}>Product</Link>
381
+ ```
382
+
383
+ Plain anchors still work for simple, external, or user-provided URLs because the client router intercepts same-origin `<a>` clicks:
384
+
385
+ ```tsx
386
+ <a href="/about">About</a>
387
+ ```
388
+
389
+ `<Link>` also accepts navigation-behavior props: `prefetch` (`"none" | "intent" | "viewport" | "render"`, default `"intent"` on hover/focus — the equivalent of `next/link`'s `prefetch` tuning), `preserveScroll` (skip the scroll-to-top reset), and `viewTransition` (wrap the navigation in `document.startViewTransition()` where supported). Scroll restoration on back/forward works out of the box, like Next.js.
390
+
391
+ #### `next/image` → `<img>`
392
+
393
+ ```tsx
394
+ // Next.js
395
+ import Image from "next/image";
396
+ <Image src="/photo.jpg" width={500} height={300} alt="Photo" />
397
+
398
+ // Pracht
399
+ <img src="/photo.jpg" width={500} height={300} alt="Photo" />
400
+ ```
401
+
402
+ #### `useRouter` → navigation
403
+
404
+ ```tsx
405
+ // Next.js
406
+ import { useRouter } from "next/navigation";
407
+ const router = useRouter();
408
+ router.push("/dashboard");
409
+
410
+ // Pracht
411
+ import { useNavigate } from "@pracht/core";
412
+ const navigate = useNavigate();
413
+ navigate("/dashboard");
414
+
415
+ // After `pracht typegen`, prefer route ids for known routes
416
+ navigate({ route: "dashboard" });
417
+ ```
418
+
419
+ #### Server Actions → API routes
420
+
421
+ ```tsx
422
+ // Next.js
423
+ "use server";
424
+ async function createPost(formData: FormData) {
425
+ await db.insert({ title: formData.get("title") });
426
+ revalidatePath("/posts");
427
+ }
428
+
429
+ // Pracht — API route handler
430
+ import type { ApiRouteArgs } from "@pracht/core";
431
+
432
+ export async function POST({ request }: ApiRouteArgs) {
433
+ const form = await request.formData();
434
+ await db.insert({ title: form.get("title") });
435
+ // revalidatePath("/posts") equivalent: regenerate the ISG page on demand
436
+ await fetch(new URL("/__pracht/revalidate", request.url), {
437
+ method: "POST",
438
+ headers: {
439
+ authorization: `Bearer ${process.env.PRACHT_REVALIDATE_TOKEN}`,
440
+ "content-type": "application/json",
441
+ },
442
+ body: JSON.stringify({ paths: ["/posts"] }),
443
+ });
444
+ return new Response(null, {
445
+ status: 303,
446
+ headers: { location: "/posts" },
447
+ });
448
+ }
449
+ ```
450
+
451
+ For the revalidation call to take effect, the `/posts` route must be `render: "isg"` and opt in with `revalidate: webhookRevalidate()` (or `[timeRevalidate(seconds), webhookRevalidate()]`) in the manifest — import both from `@pracht/core` — and `PRACHT_REVALIDATE_TOKEN` must be set in the runtime environment. If `/posts` is a plain SSR route, skip the revalidation call; the redirect re-renders it fresh anyway.
452
+
453
+ #### `cookies()` / `headers()` → loader args
454
+
455
+ ```tsx
456
+ // Next.js
457
+ import { cookies, headers } from "next/headers";
458
+ const session = cookies().get("session");
459
+ const ua = headers().get("user-agent");
460
+
461
+ // Pracht — available in loader args
462
+ export async function loader({ request }: LoaderArgs) {
463
+ const cookies = request.headers.get("cookie");
464
+ const ua = request.headers.get("user-agent");
465
+ return {
466
+ /* ... */
467
+ };
468
+ }
469
+ ```
470
+
471
+ ### Phase 9: Clean up
472
+
473
+ 1. Remove all `"use client"` and `"use server"` directives.
474
+ 2. Remove all `next/*` imports (`next/link`, `next/image`, `next/navigation`, `next/headers`).
475
+ 3. Search for remaining `className` → replace with `class`.
476
+ 4. Search for remaining `react` imports → replace with `preact` equivalents.
477
+ 5. Remove `next.config.*`, `next-env.d.ts`, `.next/` directory.
478
+ 6. Run `pracht typegen` if route ids/paths changed or if you converted links/navigation to typed route ids.
479
+ 7. Run the dev server (`pracht dev`) and fix any remaining issues.
480
+
481
+ ## Dependency Mapping
482
+
483
+ | Next.js package | Pracht equivalent |
484
+ | --------------- | ---------------------------------------------------------------------------- |
485
+ | `next` | `@pracht/core` + `@pracht/cli` + `@pracht/vite-plugin` + `@pracht/adapter-node` (or target adapter) |
486
+ | `react` | `preact` |
487
+ | `react-dom` | `preact` |
488
+ | `@next/font` | CSS `@font-face` or `fontsource` packages |
489
+ | `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
490
+ | `next-auth` | Direct integration in middleware/loaders |
491
+ | `next/og` | `@vercel/og` or custom solution |
492
+
493
+ ## React Library Compatibility
494
+
495
+ Many React libraries work with Preact via `preact/compat`. Add aliases in `vite.config.ts` if needed:
496
+
497
+ ```ts
498
+ resolve: {
499
+ alias: {
500
+ "react": "preact/compat",
501
+ "react-dom": "preact/compat",
502
+ "react/jsx-runtime": "preact/jsx-runtime",
503
+ }
504
+ }
505
+ ```
506
+
507
+ Note: The pracht Vite plugin sets these aliases automatically. Only add manual aliases if a dependency doesn't resolve correctly.
508
+
509
+ ## Rules
510
+
511
+ 1. Always read the Next.js source before converting — understand what each file does.
512
+ 2. Migrate in phases: setup → shells → routes → API → middleware → manifest → cleanup.
513
+ 3. Prefer the simplest pracht equivalent. Don't over-engineer the migration.
514
+ 4. Identify React libraries that need `preact/compat` aliasing and flag them.
515
+ 5. After migration, run `pracht dev` to verify. Fix errors iteratively.
516
+ 6. If a Next.js feature has no pracht equivalent, explain the gap and suggest alternatives.
517
+ 7. Use Preact idioms: `class` not `className`, no `React` import needed, `preact/hooks` for hooks.
518
+
519
+ $ARGUMENTS
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: pracht-debug
3
+ version: 1.3.0
4
+ description: |
5
+ Pracht framework-aware debugging. Systematically investigates route matching,
6
+ loader/API route errors, rendering issues, middleware, API routes, HMR, and build
7
+ problems. Uses pracht's architecture knowledge to find root causes fast.
8
+ Use when asked to "debug this", "fix this bug", "why is this broken",
9
+ "blank page", "hydration mismatch", or "404 on my route".
10
+ Proactively suggest when the user reports errors or unexpected behavior
11
+ in a pracht application.
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Write
16
+ - Edit
17
+ - Grep
18
+ - Glob
19
+ - AskUserQuestion
20
+ ---
21
+
22
+ # Pracht Debug
23
+
24
+ Framework-aware debugging for pracht applications — a full-stack Preact framework built on Vite.
25
+
26
+ The user will describe a symptom (error, unexpected behavior, blank page, etc.). Investigate systematically using the checklist below, stopping when you find the root cause.
27
+
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
+ 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
+ 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.
32
+
33
+ ## Iron Law
34
+
35
+ **NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.**
36
+
37
+ ## Debugging Checklist
38
+
39
+ Work through these in order, stopping when you find the root cause:
40
+
41
+ ### 1. Route matching
42
+
43
+ - Run `pracht verify --changed` first if you want a cheap changed-file confidence check.
44
+ - Run `pracht doctor` if the route might be missing, miswired, or pointing at a missing module across the project.
45
+ - For machine-readable route wiring, run `pracht inspect routes --json`. With a running dev server, `curl http://localhost:5173/_pracht.json` returns the same graph.
46
+ - Read `src/routes.ts` — is the route defined? Is the path correct?
47
+ - Check for typos in file paths (the manifest uses relative paths like `"./routes/home.tsx"`).
48
+ - For dynamic segments, verify bracket syntax: `route("/users/:id", ...)` in manifest, `[id].ts` in filenames.
49
+ - Grep for the route path across the manifest and check `matchAppRoute()` logic if needed.
50
+
51
+ ### 2. Typed route/link issues
52
+
53
+ - If `<Link route="...">`, `href("...")`, or route-object `useNavigate()` fails to typecheck, run `pracht typegen --check` to detect stale generated files.
54
+ - Run `pracht inspect routes --json` and confirm the route id exists. If it is a fallback id, remember path changes can rename it.
55
+ - Check generated `src/pracht.d.ts` for inferred params. `:id`, `*`, and `:path*` params are required; extra params should fail at typecheck time.
56
+ - If runtime navigation throws `Unknown pracht route id "..."`, in dev the error includes a `Did you mean "..."?` suggestion and the list of registered route ids (production builds tree-shake this and throw the bare error) — check for a typo first, then ensure `pracht typegen` was run and the component is rendered inside the pracht route tree.
57
+ - For unexpected URLs, reproduce with `href(routeId, options)` and compare against the route's resolved path and params.
58
+
59
+ ### 3. Loader / API route errors
60
+
61
+ - For slow pages, read the dev `Server-Timing` response header (`mw`/`loader`/`render` in ms) to see which phase dominates before reading code.
62
+ - Read the route module's `loader` function or the matching API route handler.
63
+ - Check that `loader` returns serializable data (no functions, no circular refs).
64
+ - Check that API route handlers return `Response` objects and branch on `request.method` when using a default export.
65
+ - Look for unhandled promise rejections or thrown errors.
66
+ - Verify `LoaderArgs` destructuring matches what the framework provides: `{ request, params, context, signal, url, route }`.
67
+
68
+ ### 4. Rendering issues
69
+
70
+ - **Blank page**: Check if the route has `render: "spa"` (no SSR content expected) vs `"ssr"`.
71
+ - **Hydration mismatch**: In dev, pracht surfaces a fixed-position red banner at the top of the page listing each mismatched component (via Preact's `options.__m` hook). Compare server-rendered HTML vs client component output. Common causes:
72
+ - Date/time rendering differences
73
+ - Browser-only APIs used during SSR (`window`, `document`, `localStorage`)
74
+ - Conditional rendering based on client state
75
+ - **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
+ - **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
+
78
+ ### 5. Middleware issues
79
+
80
+ - Verify middleware is registered in `defineApp({ middleware: { ... } })`. An
81
+ unregistered name (on a route, group, or `api.middleware`) throws at manifest
82
+ resolution — `Unknown middleware "..." for route "...". Did you mean "..."? Registered middleware: ...`
83
+ - Verify middleware is applied to the route/group: `middleware: ["name"]`.
84
+ - Middleware is wrap-around: it must always return a `Response`, either by
85
+ calling `await next()` (to continue down the chain) or short-circuiting.
86
+ - Common bugs:
87
+ - Forgetting `return next()` → `Middleware "..." did not return a Response`
88
+ - Calling `next()` twice → `Middleware "..." called next() multiple times`
89
+ - Mutating a non-object `context` → mutations don't propagate; always pass
90
+ an object as the request context.
91
+ - Middleware runs server-side only, wrapping loaders and API handlers.
92
+
93
+ ### 6. API route issues
94
+
95
+ - API routes live in `src/api/` and are auto-discovered (no manifest entry needed).
96
+ - For machine-readable API inventory, run `pracht inspect api --json`.
97
+ - File path maps to URL: `src/api/health.ts` → `/api/health`, `src/api/users/[id].ts` → `/api/users/:id`.
98
+ - Each file exports named HTTP method handlers (`GET`, `POST`, etc.) or one default handler.
99
+ - Missing method handler → 405 response when there is no default handler.
100
+ - Default handlers receive the same route args and can branch on `request.method`.
101
+ - Handlers must return `Response` objects.
102
+
103
+ ### 7. Vite plugin / HMR issues
104
+
105
+ - Check `vite.config.ts` — is `pracht()` plugin included?
106
+ - Virtual modules: `virtual:pracht/client` (hydration), `virtual:pracht/server` (SSR), `virtual:pracht/islands-client` (islands hydration).
107
+ - HMR: changes to `src/routes.ts` restart the dev server (`server.restart()`, not a browser-side full reload); changes to route/shell/middleware/API/server/islands files invalidate the server module.
108
+ - If HMR seems broken, check that the file is in one of the watched directories (`src/routes/`, `src/shells/`, `src/middleware/`, `src/api/`, `src/server/`, `src/islands/`).
109
+
110
+ ### 8. Build / deployment issues
111
+
112
+ - `pracht build` runs client + server builds, then prerenders SSG/ISG routes.
113
+ - `pracht preview` builds and serves the production output locally (Node runs `dist/server/server.js`, Cloudflare delegates to `wrangler dev`).
114
+ - `pracht inspect build --json` reports the resolved adapter target plus client/CSS/JS manifests from the latest build output (requires a prior `pracht build`).
115
+ - Check `dist/client/` for client assets and `dist/server/` for server bundle.
116
+ - ISG manifest: `dist/server/isg-manifest.json`. On Cloudflare the build also copies it to `dist/client/_pracht/isg.json` for the worker runtime to read via the assets binding.
117
+ - Adapter mismatch: ensure `pracht({ adapter: nodeAdapter() })` or `cloudflareAdapter()` matches deployment target.
118
+
119
+ ## Key Files
120
+
121
+ | File | Purpose |
122
+ | --------------------- | ----------------------------------------------------- |
123
+ | `src/routes.ts` | App manifest — all route/shell/middleware definitions |
124
+ | `vite.config.ts` | Vite config with `pracht()` plugin |
125
+ | `src/routes/*.tsx` | Route modules (loader, Component) |
126
+ | `src/shells/*.tsx` | Shell layout components |
127
+ | `src/middleware/*.ts` | Server-side middleware |
128
+ | `src/api/*.ts` | API route handlers |
129
+
130
+ ## Framework Internals
131
+
132
+ - `handlePrachtRequest()` dispatches: API routes → middleware → loader → render → HTML assembly
133
+ - Route state JSON: returned when `x-pracht-route-state-request` header is present (client-side navigation)
134
+ - Hydration state: injected as `window.__PRACHT_STATE__` in the HTML
135
+ - Client router: `initClientRouter()` intercepts link clicks and fetches route state JSON
136
+
137
+ ## Rules
138
+
139
+ 1. Always read the relevant source files before diagnosing.
140
+ 2. Start with the most likely cause based on the symptom, not a full audit.
141
+ 3. When you find the root cause, explain _why_ it breaks and fix it.
142
+ 4. If wiring looks suspicious, run `pracht verify` first, then `pracht doctor` if you need the full-project view. If running the dev server or tests would help, do so (`pracht dev`, `pnpm test`, `pnpm e2e`).
143
+ 5. After fixing, verify the fix works (run relevant test or check dev server output).
144
+ 6. Never say "this should fix it." Verify and prove it.
145
+
146
+ $ARGUMENTS