create-pracht 0.3.0 → 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,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,203 @@
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
+
64
+ Earlier versions wrote the declaration to `src/pracht-routes.d.ts`; typegen
65
+ removes that stale file automatically (TypeScript silently ignored it next to
66
+ the same-named `.ts` helper).
67
+
68
+ Do not hand-edit generated files. If they are stale, update the route graph and
69
+ run typegen again — or rely on `pracht dev`, which refreshes them when route
70
+ files are added, removed, or renamed and when the route manifest or an imported
71
+ definition module changes. The dev banner prompts for the initial typegen run
72
+ when `src/pracht.d.ts` does not exist. In CI, prefer:
73
+
74
+ ```bash
75
+ pracht typegen --check
76
+ ```
77
+
78
+ ## Step 3: Replace string navigation where it matters
79
+
80
+ ### Components
81
+
82
+ ```tsx
83
+ import { Link, useNavigate } from "@pracht/core";
84
+
85
+ export function ProductLink({ id }: { id: string }) {
86
+ const navigate = useNavigate();
87
+
88
+ return (
89
+ <>
90
+ <Link route="product" params={{ id }} search={{ ref: "home" }}>
91
+ View product
92
+ </Link>
93
+ <button onClick={() => void navigate({ route: "product", params: { id } })}>
94
+ Open product
95
+ </button>
96
+ </>
97
+ );
98
+ }
99
+ ```
100
+
101
+ `<Link>` renders a normal `<a>` and the client router intercepts it like any
102
+ same-origin anchor. It also accepts navigation-behavior props:
103
+ `prefetch="none" | "hover" | "intent" | "viewport" | "render"` (per-link
104
+ prefetch strategy, default `"intent"`), `preserveScroll` (keep the scroll
105
+ position), and `viewTransition` (animate the navigation with the View Transitions API
106
+ where supported). There is also an imperative `prefetch()` export and a
107
+ `useNavigation()` hook for pending navigation/submission state.
108
+
109
+ ### Outside components
110
+
111
+ ```ts
112
+ import { href } from "./pracht-routes";
113
+
114
+ const productUrl = href("product", {
115
+ params: { id: "123" },
116
+ search: { tab: "details" },
117
+ });
118
+ ```
119
+
120
+ Use `href()` in loaders that return URLs, sitemap helpers, menu config, test
121
+ fixtures, and other non-component code.
122
+
123
+ ### Loader data
124
+
125
+ After typegen, `useRouteData(routeId)` returns that route's loader data with
126
+ no generic — route ids autocomplete and the type follows the route's loader
127
+ (or its separate loader file from the manifest):
128
+
129
+ ```tsx
130
+ import { useRouteData } from "@pracht/core";
131
+
132
+ export function Component() {
133
+ const data = useRouteData("product");
134
+ return <h1>{data.product.name}</h1>;
135
+ }
136
+ ```
137
+
138
+ Prefer this over `useRouteData<typeof loader>()` when typegen runs; keep the
139
+ generic form for projects that do not generate route types. Routes without a
140
+ loader type their data as `undefined`. The id must be the active route — dev
141
+ mode warns on mismatches.
142
+
143
+ ### API routes
144
+
145
+ After typegen, `apiFetch()` type-checks API calls end to end — paths,
146
+ methods, params, bodies and queries (for `defineApi()` routes), and response
147
+ types:
148
+
149
+ ```ts
150
+ import { apiFetch } from "@pracht/core";
151
+
152
+ const item = await apiFetch("/api/items/:id", { params: { id: "42" } });
153
+ ```
154
+
155
+ See docs/API_VALIDATION.md for `defineApi()` and validation error handling.
156
+ Typegen discovers API route files without importing them, so it is safe for
157
+ route modules that initialize runtime-only services at module scope.
158
+
159
+ Query and params values cross the wire as strings — write schemas that accept
160
+ string input (`z.coerce.number()`, not `z.number()`); `apiFetch()` rejects
161
+ query and params keys without a string representation at compile time when the
162
+ schema exposes a concrete input type. Handlers that need a custom status keep
163
+ typed payloads with `json(value, { status })`.
164
+
165
+ ## Step 4: Param and search rules
166
+
167
+ Generated param types accept `RouteParamInput = string | number | boolean`
168
+ (values are stringified into the path), so:
169
+
170
+ - `:id` requires `params: { id: RouteParamInput }` — a `string` is typical,
171
+ but `number`/`boolean` also typecheck.
172
+ - `*` requires `params: { "*": RouteParamInput }`.
173
+ - `:path*` requires `params: { path: RouteParamInput }`.
174
+ - Routes with no dynamic segments should omit `params`.
175
+ - Missing and extra params should fail at typecheck time.
176
+ - `search` currently accepts `string`, `URLSearchParams`, or an object of
177
+ primitive values/arrays; route-specific search schemas can be added later.
178
+
179
+ ## Step 5: Verify
180
+
181
+ Run at least:
182
+
183
+ ```bash
184
+ pracht typegen --check
185
+ pnpm typecheck
186
+ pracht verify --json
187
+ ```
188
+
189
+ If navigation changed, add or update Playwright coverage for both the rendered
190
+ anchor `href` and client-side navigation without a full page reload.
191
+
192
+ ## Rules
193
+
194
+ 1. Always start from `pracht inspect routes --json` or `pracht typegen`; do not
195
+ infer the full route map from files by hand.
196
+ 2. Prefer adding explicit ids before converting links for important routes.
197
+ 3. Never edit `src/pracht.d.ts` or `src/pracht-routes.ts` manually.
198
+ 4. Keep plain `<a href="...">` where a URL is genuinely external, opaque, or
199
+ user-provided.
200
+ 5. After adding/removing/renaming routes, run `pracht typegen` and include the
201
+ generated file changes in the same commit.
202
+
203
+ $ARGUMENTS
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: upgrade-pracht
3
+ version: 1.0.0
4
+ description: |
5
+ Upgrade the @pracht/* packages in an app safely: inventory installed
6
+ versions, read the changelogs between installed and target, map breaking
7
+ changes to actual usage in the codebase, apply the upgrade, and walk the
8
+ verification ladder (doctor, typegen, verify, build, tests).
9
+ Use when asked to "upgrade pracht", "update @pracht packages", "bump the
10
+ framework", "what changed in the new pracht version", or "is this pracht
11
+ upgrade safe".
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Edit
16
+ - Grep
17
+ - Glob
18
+ - AskUserQuestion
19
+ ---
20
+
21
+ # Pracht Upgrade
22
+
23
+ Upgrade `@pracht/*` dependencies with the changelog read *before* the install,
24
+ not after the build breaks.
25
+
26
+ ## Step 1: Inventory
27
+
28
+ List every installed pracht package and its resolved version:
29
+
30
+ ```bash
31
+ pnpm list --depth 1 --json | grep -A2 '@pracht/' # or read package.json + lockfile
32
+ ```
33
+
34
+ The family: `@pracht/core`, `@pracht/cli`, `@pracht/vite-plugin`,
35
+ `@pracht/adapter-node`, `@pracht/adapter-cloudflare`, `@pracht/adapter-vercel`,
36
+ `@pracht/preact-ssr-precompile`. Get the latest published versions with
37
+ `npm view <pkg> version`.
38
+
39
+ ## Step 2: Understand the versioning model
40
+
41
+ Pracht packages are **independently versioned** (the repo's changesets config
42
+ has empty `fixed`/`linked` groups) — `@pracht/core` can be at 0.9.x while
43
+ `@pracht/cli` is at 1.6.x. There is no "one framework version". Two
44
+ consequences:
45
+
46
+ 1. **Internal dependencies are pinned exact.** Published packages depend on
47
+ their siblings at exact versions (e.g. `@pracht/vite-plugin@0.5.0` depends
48
+ on `@pracht/core@0.9.0`, not a range). Upgrade the whole family in one
49
+ move; upgrading only one package can drag in a second copy of
50
+ `@pracht/core` and split the runtime.
51
+ 2. **Most packages are 0.x**, so under semver a *minor* bump may be breaking —
52
+ treat `### Minor Changes` entries on 0.x packages with the same care as
53
+ majors.
54
+
55
+ After any upgrade, confirm a single core resolution:
56
+
57
+ ```bash
58
+ pnpm why @pracht/core # exactly one version may appear
59
+ ```
60
+
61
+ ## Step 3: Read the changelogs between installed and target
62
+
63
+ Only `@pracht/cli` ships `CHANGELOG.md` in its npm tarball
64
+ (`node_modules/@pracht/cli/CHANGELOG.md`); the other packages publish `dist/`
65
+ only. Fetch their changelogs from the repo instead:
66
+
67
+ ```
68
+ https://raw.githubusercontent.com/JoviDeCroock/pracht/main/packages/<dir>/CHANGELOG.md
69
+ ```
70
+
71
+ | Package | Repo directory |
72
+ | ------- | -------------- |
73
+ | `@pracht/core` | `packages/framework` |
74
+ | `@pracht/cli` | `packages/cli` |
75
+ | `@pracht/vite-plugin` | `packages/vite-plugin` |
76
+ | `@pracht/adapter-node` / `-cloudflare` / `-vercel` | `packages/adapter-*` |
77
+ | `@pracht/preact-ssr-precompile` | `packages/preact-ssr-precompile` |
78
+
79
+ Changelogs are changesets-generated: `## X.Y.Z` sections containing
80
+ `### Major Changes` / `### Minor Changes` / `### Patch Changes`. Read every
81
+ section between the installed and target version of every installed package.
82
+
83
+ ## Step 4: Map changes onto this app
84
+
85
+ Classify each entry as **breaking** / **feature** / **fix**. For each breaking
86
+ (or 0.x minor) entry, grep the app for the APIs, exports, config options, and
87
+ generated-file shapes it names, and record: affected files, the migration the
88
+ changelog prescribes, and whether it can be applied mechanically. Also
89
+ re-check peer ranges after a major target bump — `@pracht/vite-plugin`
90
+ requires `vite` (^8), `@pracht/adapter-cloudflare` requires `vite` and
91
+ `wrangler` (^4.81), `@pracht/core` requires `preact` (^10) and
92
+ `preact-render-to-string` (^6).
93
+
94
+ Present the plan as a table:
95
+
96
+ | Package | Installed → Target | Breaking entries | App impact | Migration |
97
+ | ------- | ------------------ | ---------------- | ---------- | --------- |
98
+
99
+ ## Step 5: Confirm, then apply
100
+
101
+ Use `AskUserQuestion` before touching anything when breaking migrations are
102
+ required: confirm the target versions and which migrations to apply. Then:
103
+
104
+ ```bash
105
+ pnpm up '@pracht/core@<v>' '@pracht/cli@<v>' '@pracht/vite-plugin@<v>' <adapters...>
106
+ ```
107
+
108
+ Upgrade every installed `@pracht/*` package in the same command. Apply the
109
+ agreed code migrations with minimal diffs, one changelog entry at a time.
110
+
111
+ ## Step 6: Verification ladder
112
+
113
+ Run in order; stop and fix at the first failure:
114
+
115
+ ```bash
116
+ pracht doctor --json # wiring still valid
117
+ pracht typegen --check # generated route types up to date?
118
+ pracht typegen # regenerate if --check failed or routes changed
119
+ pracht verify --json # framework-aware checks
120
+ pracht build # full production build (budgets included)
121
+ pnpm test # the app's own suite
122
+ ```
123
+
124
+ `pracht doctor`, `verify`, and `typegen --check` exit non-zero on failure, so
125
+ they gate CI cleanly.
126
+
127
+ ## Step 7: Rollback note
128
+
129
+ If the ladder cannot be made green, roll back rather than shipping a
130
+ half-upgrade:
131
+
132
+ ```bash
133
+ git restore package.json pnpm-lock.yaml && pnpm install
134
+ git checkout -- <migrated files> # or revert the upgrade commit
135
+ ```
136
+
137
+ Because internal deps are exact-pinned, a *partial* rollback (one package
138
+ back, the rest forward) recreates the duplicate-core problem from Step 2 —
139
+ roll the whole family back together.
140
+
141
+ ## Rules
142
+
143
+ 1. Never mix `@pracht/*` versions from different release waves — upgrade and
144
+ roll back the family as a unit, and verify with `pnpm why @pracht/core`.
145
+ 2. Read changelogs before installing, not after something breaks.
146
+ 3. Never apply a breaking-change migration without explicit user confirmation
147
+ via `AskUserQuestion`.
148
+ 4. Treat 0.x minor bumps as potentially breaking.
149
+ 5. Do not hand-edit lockfiles; let the package manager resolve.
150
+
151
+ $ARGUMENTS