pracht-debug
Framework-aware debugging for pracht: route matching, loader and API errors, rendering and hydration, middleware, HMR, and build failures — root cause first. Use for "debug this", "fix this bug", "why
SKILL.mdskills/pracht-debug/SKILL.md
GitHubPracht Debug
Iron law: no fixes without root-cause investigation first. Read the relevant source before diagnosing, start from the most likely cause for the symptom rather than a full audit, and when you find the root cause explain why it breaks. After fixing, prove the fix — run the test, check the dev server. Never say "this should fix it."
Instruments
Reach for these before deep manual inspection:
| Tool | Use |
|---|---|
pracht verify (--changed to scope to git-changed files) |
Fast confidence check |
pracht doctor |
Broader broken wiring or missing files |
pracht docs <topic> / pracht docs --search <query> |
Docs bundled with the installed CLI — check documented behavior before calling it a bug. Older CLIs lack the command; read https://pracht.resynapse.dev/llms.txt instead |
pracht inspect routes|api|build --json |
The resolved graph — never reconstruct it from source |
GET /_pracht (JSON at /_pracht.json) |
Same graph from a running dev server, no CLI needed |
Server-Timing on dev SSR responses |
mw / loader / render durations in ms — which phase is slow |
pracht_* WebMCP page tools in the open tab |
Tab-scoped view from an agent-driven browser: pracht_route, pracht_loader_data, pracht_islands, pracht_last_error, pracht_page_tools |
pracht inspect needs the pracht plugin in the vite config; inspect build
needs a prior pracht build. Under a Vite deploy base, prefix /_pracht with
that base (links from the devtools and dev-404 pages already do). When the
pracht MCP server is registered (pracht dev-mcp), prefer the
inspect_routes/inspect_api/doctor/verify MCP tools — same payloads,
structured results.
When you are driving a browser against pracht dev with a WebMCP-compatible
test harness or host, ask the tab before reading logs: every dev document registers read-only
pracht_* page tools with document.modelContext. pracht_route gives the
matched route, files, render/hydration mode, and middleware; pracht_loader_data
the data the page holds now ({ path: "a.0.b" } narrows it; islands/none routes
answer with the route-state request to make instead); pracht_islands each
island's file, strategy, props, and hydration status; pracht_last_error the
structured server error behind an overlay or ErrorBoundary plus recent client
errors; pracht_page_tools the app's active WebMCP tools on the route. All
resolve to { ok, data } / { ok: false, error } envelopes and follow client
navigation.
Checklist
Work in order; stop at the root cause.
1. Route matching
pracht inspect routes --json(orcurl http://localhost:5173/_pracht.json) for the resolved wiring;pracht doctorif a route may be missing, miswired, or pointing at a missing module.- Is the route in
src/routes.tswith the right path? The manifest uses relative paths like"./routes/home.tsx"— check for typos. - Dynamic segments:
route("/users/:id", ...)in the manifest,[id].tsin filenames. - Wrong page renders: manifest routes match in declaration order, first match
wins. Look for the dev-server warning
Route "..." can never match: the earlier route "..." matches every URL it doesand move the specific route up. - If needed, grep the route path across the manifest and check
matchAppRoute().
2. Typed routes and links
- A
<Link route="...">,href("..."), or route-objectuseNavigate()that fails to typecheck usually means stale generated files:pracht typegen --check. - Confirm the route id exists in
pracht inspect routes --json. Fallback ids get renamed by path changes. src/pracht.d.tscarries the inferred params.:id,*, and:path*are required; extra params should fail at typecheck time.- Runtime
Unknown pracht route id "...": dev appendsDid you mean "..."?plus the registered ids (production tree-shakes that and throws bare). Check the typo first, then thatpracht typegenran and the component renders inside the pracht route tree. - For unexpected URLs, reproduce with
href(routeId, options)and compare against the route's resolved path and params.
3. Loader / API errors
- On slow pages, read
Server-Timingbefore reading code. - Loaders must return serializable data — no functions, no circular refs. A
Datearrives as a string unlesspracht({ client: { richData: true } })is set; with it, an unsupported value fails with an error naming its path. - API handlers must return
Responseobjects, and a default export must branch onrequest.method. - Look for unhandled rejections or thrown errors.
LoaderArgsdestructuring must match what the framework provides:{ request, params, context, signal, url, route }.
4. Rendering
- Blank page — check whether the route is
render: "spa"(no SSR content expected) rather than"ssr". - Hydration mismatch — dev shows a fixed red banner listing each mismatched
component (via Preact's
options.__mhook). Compare server HTML against client output. Usual causes: date/time differences, browser-only APIs during SSR (window,document,localStorage), conditional rendering on client state — or, on Preact 10, a suspending boundary resolving to zero or multiple DOM nodes. Preact 11 supports those boundary shapes, so Pracht skips that legacy diagnostic there. Another cause is two copies of@pracht/corein the SSR module graph. The tell for that last one: in the server-rendered HTML of every page,useLocation()returns/,useParams()returns{}, anduseRouteData()returnsundefined, while the hydrated client is correct — provider and hooks hold differentcreateContext()objects. The plugin prevents it by keeping@pracht/*inssr.noExternal; listing a@pracht/*package inssr.externaloverrides that and brings the split back. - Missing shell — an unregistered shell name throws at manifest resolution
(
Unknown shell "..." for route "...". Did you mean "..."? Registered shells: ...) and appears in the dev error overlay as soon as the manifest loads. CheckdefineApp({ shells })and the route/group assignment. - 404 — usually step 1, but a 404 on a URL you do expect means the loader
threw
notFound(), not that matching failed. Inpracht dev, unmatched navigations render a dev-only 404 listing every registered route and its render mode (also printed at dev-server startup, also inpracht inspect routes). Apps declaringdefineApp({ notFound })render their own 404 page in dev and production alike, so that table is not shown — readpracht inspect routesdirectly.
5. Middleware
- It must be registered in
defineApp({ middleware })and applied viamiddleware: ["name"]on the route or group. An unregistered name (route, group, orapi.middleware) throws at manifest resolution:Unknown middleware "..." for route "...". Did you mean "..."? Registered middleware: ... - Middleware is wrap-around and server-side only, wrapping loaders and API
handlers. It must always return a
Response, either fromawait next()or by short-circuiting. Common failures:
| Bug | Error |
|---|---|
Forgetting return next() |
Middleware "..." did not return a Response |
Calling next() twice |
Middleware "..." called next() multiple times |
Mutating a non-object context |
Silent — mutations don't propagate; always pass an object |
6. API routes
- They live in
src/api/, auto-discovered, no manifest entry.pracht inspect api --jsonfor the inventory. - Path maps to URL:
src/api/health.ts→/api/health,src/api/users/[id].ts→/api/users/:id. - Each file exports named method handlers (
GET,POST, …) or one default handler that branches onrequest.method. A missing method handler is a 405 when there is no default.
7. Vite plugin and HMR
- Is
pracht()invite.config.ts? Virtual modules arevirtual:pracht/client(hydration),virtual:pracht/server(SSR),virtual:pracht/islands-client(islands hydration). - HMR only watches
src/routes/,src/shells/,src/middleware/,src/api/,src/server/,src/islands/. - What each change does: editing
src/routes.ts, or a module it imports routes from, applies the new routes in place, reprints the dev banner, and reloads the page (an adapter that runs its own dev server, like Cloudflare, restarts it instead). Route, shell, and island components use Preact Fast Refresh; route/shell edits and client-reachable loader dependencies also re-fetch active route data, with rapid saves coalesced so the newest result settles last and a failed latest refresh falling back to a reload. Adding or removing a routeloaderor route/shellheadexport reloads so generated client hints stay current, and editing a route/shell exporting documentheaders()reloads because CSP and other response headers cannot be patched. Compiled formats get refresh instrumentation after their companion Vite plugin runs, but Markdown, MDX, and configured additional formats reload conservatively because a transform may synthesizeheaders()from metadata; default.tsrxpreserves state when its source is headerless. Everything else invalidates the server module.
8. Build and deployment
pracht buildruns the client and server builds, then prerenders SSG/ISG routes. On serverful adapters a failed path is warned about and skipped, but the build fails if every attempted prerender returns non-200 — use the reported underlying error to fix the shared loader/render dependency; partial output stays valid. Static exports fail on any bad path.pracht previewbuilds and serves production output locally (Node runsdist/server/server.js; Cloudflare delegates towrangler dev).pracht inspect build --jsonreports the resolved adapter target plus the client/CSS/JS manifests from the last build.- Client assets land in
dist/client/, the server bundle indist/server/. The ISG manifest isdist/server/isg-manifest.json; on Cloudflare the build also copies it todist/client/_pracht/isg.jsonfor the worker to read via the assets binding. - Confirm the adapter in
pracht({ adapter: … })matches the deployment target.
Framework internals
handlePrachtRequest()dispatches: API routes → middleware → loader → render → HTML assembly.- Route-state JSON is returned when the
x-pracht-route-state-requestheader is present (client-side navigation). - Hydration state is injected as
window.__PRACHT_STATE__. initClientRouter()intercepts link clicks and fetches that route state.
$ARGUMENTS