create-pracht 0.6.0 → 0.6.2
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/package.json +1 -1
- package/skills/add-auth/SKILL.md +63 -143
- package/skills/add-capabilities/SKILL.md +409 -0
- package/skills/add-content/SKILL.md +242 -0
- package/skills/add-db/SKILL.md +93 -202
- package/skills/add-i18n/SKILL.md +178 -217
- package/skills/add-images/SKILL.md +203 -0
- package/skills/add-observability/SKILL.md +118 -15
- package/skills/add-openapi/SKILL.md +209 -0
- package/skills/audit-a11y/SKILL.md +8 -9
- package/skills/audit-agent-surface/SKILL.md +335 -0
- package/skills/audit-auth/SKILL.md +16 -11
- package/skills/audit-bundles/SKILL.md +56 -12
- package/skills/audit-csrf/SKILL.md +9 -10
- package/skills/audit-deps/SKILL.md +8 -8
- package/skills/audit-headers/SKILL.md +9 -10
- package/skills/audit-islands/SKILL.md +9 -10
- package/skills/audit-loaders/SKILL.md +23 -8
- package/skills/audit-redirects/SKILL.md +9 -10
- package/skills/audit-secrets/SKILL.md +6 -6
- package/skills/audit-seo/SKILL.md +8 -8
- package/skills/audit-shells/SKILL.md +8 -9
- package/skills/configure-isg/SKILL.md +9 -10
- package/skills/migrate-nextjs/SKILL.md +200 -415
- package/skills/pracht-debug/SKILL.md +165 -120
- package/skills/pracht-deploy/SKILL.md +248 -329
- package/skills/pracht-scaffold/SKILL.md +123 -146
- package/skills/pracht-test-api/SKILL.md +10 -10
- package/skills/pre-deploy/SKILL.md +166 -195
- package/skills/scaffold-e2e/SKILL.md +11 -12
- package/skills/scaffold-tests/SKILL.md +10 -12
- package/skills/tune-render-mode/SKILL.md +7 -8
- package/skills/typed-routes/SKILL.md +15 -11
- package/skills/upgrade-pracht/SKILL.md +12 -10
- package/src/index.js +43 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: migrate-nextjs
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.3.0
|
|
4
4
|
description: |
|
|
5
|
-
Migrate a Next.js
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
next to pracht", "nextjs migration", or "switch from next".
|
|
5
|
+
Migrate a Next.js app to pracht: App or Pages Router pages, layouts, middleware,
|
|
6
|
+
API routes, data fetching, and metadata — plus React→Preact, `className`→`class`,
|
|
7
|
+
server components→loaders, and manifest wiring.
|
|
8
|
+
Use for "migrate from next", "convert next.js app", "port from next to pracht",
|
|
9
|
+
"nextjs migration", "switch from next".
|
|
11
10
|
allowed-tools:
|
|
12
11
|
- Bash
|
|
13
12
|
- Read
|
|
@@ -20,136 +19,105 @@ allowed-tools:
|
|
|
20
19
|
|
|
21
20
|
# Migrate Next.js to Pracht
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
## Concept Mapping
|
|
22
|
+
Migrate in phases: setup → shells → routes → client components → API →
|
|
23
|
+
middleware → manifest → patterns → cleanup. Read each Next.js source file
|
|
24
|
+
before converting it; never infer from the filename. Prefer the simplest
|
|
25
|
+
pracht equivalent, and when a Next.js feature has no equivalent, say so and
|
|
26
|
+
propose an alternative instead of inventing one.
|
|
27
|
+
|
|
28
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), use
|
|
29
|
+
`generate_route`/`generate_shell`/`generate_middleware`/`generate_api` to
|
|
30
|
+
scaffold and `inspect_routes`/`inspect_api`/`doctor`/`verify` to check
|
|
31
|
+
progress, instead of Bash. `pracht inspect` needs the pracht plugin in the
|
|
32
|
+
vite config; `inspect_build` needs a prior `pracht build`.
|
|
33
|
+
|
|
34
|
+
## Step 0: Assess the source
|
|
35
|
+
|
|
36
|
+
Read `next.config.*` and `package.json` (React/Next versions, deps), then map
|
|
37
|
+
the tree: `app/` (App Router), `pages/` (Pages Router), `middleware.ts`,
|
|
38
|
+
`app/api/` or `pages/api/`. Note which patterns are in use — `"use client"`,
|
|
39
|
+
`async` server components, `generateStaticParams`, `generateMetadata`/`metadata`,
|
|
40
|
+
`"use server"` actions — and the third-party integrations (auth, CMS, DB,
|
|
41
|
+
analytics). Confirm scope with the user if the app has more than ~20 routes.
|
|
42
|
+
|
|
43
|
+
## Fast path: Pages Router
|
|
44
|
+
|
|
45
|
+
`pagesDir` makes a pages-router source near-drop-in — **Phase 7 is then
|
|
46
|
+
automatic**:
|
|
47
|
+
|
|
48
|
+
1. `pracht({ pagesDir: "/src/pages" })` in `vite.config.ts`; copy `pages/` to
|
|
49
|
+
`src/pages/`.
|
|
50
|
+
2. `_app.tsx` → pracht shell shape (`Shell` export taking `children`).
|
|
51
|
+
3. `getServerSideProps`/`getStaticProps` → `loader` export.
|
|
52
|
+
4. `export const RENDER_MODE = "ssg"` on static pages (`"ssr"` is the
|
|
53
|
+
default). For time-revalidated pages export `RENDER_MODE = "isg"` plus a
|
|
54
|
+
positive integer `REVALIDATE` in seconds; webhook policies require ejecting
|
|
55
|
+
to a manifest.
|
|
56
|
+
5. Run the dev server, iterate, and optionally eject later with
|
|
57
|
+
`generateRoutesFile`.
|
|
58
|
+
|
|
59
|
+
## Concept mapping
|
|
63
60
|
|
|
64
61
|
| Next.js | Pracht | Notes |
|
|
65
62
|
| ------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
66
|
-
| `pages/` directory | `pagesDir` plugin option | Auto-discovers routes from file system
|
|
63
|
+
| `pages/` directory | `pagesDir` plugin option | Auto-discovers routes from the file system |
|
|
67
64
|
| `app/page.tsx` | `src/routes/*.tsx` + `route()` in manifest | File is a module; wiring is explicit |
|
|
68
65
|
| `app/layout.tsx` | `src/shells/*.tsx` + `shells` in `defineApp` | Shells are named, not directory-nested |
|
|
69
|
-
| `app/loading.tsx` | `Loading` export on the shell |
|
|
66
|
+
| `app/loading.tsx` | `Loading` export on the shell | SSR placeholder for SPA routes until the client router takes over |
|
|
70
67
|
| `app/error.tsx` | `ErrorBoundary` export in route module | Same concept, different wiring |
|
|
71
68
|
| `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
69
|
| `middleware.ts` | `src/middleware/*.ts` + `middleware` in `defineApp` | Named, applied per route/group |
|
|
73
70
|
| `app/api/*/route.ts` | `src/api/*.ts` with `GET`/`POST` exports | Auto-discovered, no manifest entry |
|
|
74
71
|
| `generateStaticParams` | `getStaticPaths()` export | Returns `RouteParams[]` of param objects |
|
|
75
72
|
| `generateMetadata` | `head()` export | Returns `{ title, meta }` |
|
|
76
|
-
| Server Components | `loader()` export | Data fetching moves to loader; component is
|
|
73
|
+
| Server Components | `loader()` export | Data fetching moves to the loader; the component is a Preact component |
|
|
77
74
|
| `"use server"` actions | API routes + `<Form>` / `fetch` | Mutations move to `src/api/*`; return `Response` objects |
|
|
78
|
-
| `"use client"` (few,
|
|
79
|
-
| `revalidatePath` / `res.revalidate()` | `webhookRevalidate()` + `POST /__pracht/revalidate` | On-demand ISG
|
|
80
|
-
| `useRouter()` (next/navigation) | `useNavigate()`
|
|
81
|
-
| `useSearchParams()` | `useSearchParams()`
|
|
82
|
-
| `useParams()` | `useParams()`
|
|
83
|
-
| `next/link` `<Link>` | `<Link route="...">` or plain `<a>`
|
|
84
|
-
| `next/link` `prefetch={false}` | `<Link prefetch="none">` |
|
|
85
|
-
| `useLinkStatus()` / pending UI | `useNavigation()` | `{ state, location, formData }` —
|
|
86
|
-
| `next/image` | `<Image>` from `@pracht/image` | Responsive srcsets
|
|
75
|
+
| `"use client"` (few, mostly-server app) | `hydration: "islands"` + `src/islands/` | Only islands ship JS; see Phase 4 |
|
|
76
|
+
| `revalidatePath` / `res.revalidate()` | `webhookRevalidate()` + `POST /__pracht/revalidate` | On-demand ISG; combinable with `timeRevalidate(seconds)` |
|
|
77
|
+
| `useRouter()` (next/navigation) | `useNavigate()` | Takes a path, or `{ route: "id" }` after `pracht typegen` |
|
|
78
|
+
| `useSearchParams()` | `useSearchParams()` | Reactive read-only params; SSG gets the browser query after hydration, loaders use `url.searchParams` |
|
|
79
|
+
| `useParams()` | `useParams()` | Direct equivalent; also `params` in loader args |
|
|
80
|
+
| `next/link` `<Link>` | `<Link route="..." params={{…}}>` or plain `<a>` | Prefer typed `<Link>` after `pracht typegen`; the router intercepts same-origin anchors |
|
|
81
|
+
| `next/link` `prefetch={false}` | `<Link prefetch="none">` | Default `"intent"` (hover/focus); also `"viewport"`, `"render"` |
|
|
82
|
+
| `useLinkStatus()` / pending UI | `useNavigation()` | `{ state, location, formData }` — progress bars, optimistic UI |
|
|
83
|
+
| `next/image` | `<Image>` from `@pracht/image` | Responsive srcsets; Node, Cloudflare, Vercel, or passthrough loaders |
|
|
87
84
|
| `next/head` or Metadata API | `head()` export on route/shell | Per-route and per-shell head merging |
|
|
88
|
-
| `next/script` `<Script>` | `<Script>` from `@pracht/core` |
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `import React from "react"` | Remove
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
3. Update `package.json`:
|
|
119
|
-
- Replace `react`, `react-dom` → `preact`
|
|
120
|
-
- 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`.
|
|
121
|
-
- If the app imports `next/image`, add `@pracht/image`; add `sharp` only for the built-in Node optimization endpoint or build-time `?pracht` imports (static imports / blur placeholders).
|
|
122
|
-
- 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
|
|
123
|
-
4. Remove Next.js config files: `next.config.*`, `next-env.d.ts`, `.next/`
|
|
124
|
-
5. If `tsconfig.json` has `"jsx": "preserve"`, change to `"jsx": "react-jsx"` and add `"jsxImportSource": "preact"`.
|
|
125
|
-
|
|
126
|
-
### Phase 2: Convert layouts → shells
|
|
127
|
-
|
|
128
|
-
For each `layout.tsx`:
|
|
129
|
-
|
|
130
|
-
**Next.js:**
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
134
|
-
return (
|
|
135
|
-
<html>
|
|
136
|
-
<body className="root">{children}</body>
|
|
137
|
-
</html>
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**Pracht:**
|
|
85
|
+
| `next/script` `<Script>` | `<Script>` from `@pracht/core` | `beforeHydration` (≈ `beforeInteractive`), `afterHydration` (≈ `afterInteractive`, default), `idle` (≈ `lazyOnload`), `visible` |
|
|
86
|
+
| `cookies()` / `headers()` | `request.headers` in loader/middleware/API args | No separate API — read the standard `Request` |
|
|
87
|
+
| `className` | `class` | Preact uses the `class` attribute |
|
|
88
|
+
| `react` / `react-dom` imports | `preact/hooks`, `preact/compat` | Same hook APIs |
|
|
89
|
+
| `import React from "react"` | Remove | The Vite plugin handles JSX |
|
|
90
|
+
|
|
91
|
+
Scroll restoration on back/forward works out of the box. `<Link>` also takes
|
|
92
|
+
`preserveScroll` (skip the scroll-to-top reset) and `viewTransition` (wrap the
|
|
93
|
+
navigation in `document.startViewTransition()` where supported).
|
|
94
|
+
|
|
95
|
+
## Phase 1: Project setup
|
|
96
|
+
|
|
97
|
+
Create `src/routes.ts` (manifest), `src/routes/`, `src/shells/`,
|
|
98
|
+
`src/middleware/`, `src/api/`, and a `vite.config.ts` whose `plugins` array
|
|
99
|
+
contains `pracht()` from `@pracht/vite-plugin`. Then:
|
|
100
|
+
|
|
101
|
+
- Dependencies: drop `react`/`react-dom` for `preact`; drop `next` for
|
|
102
|
+
`@pracht/core` (runtime), `@pracht/cli` (the `pracht` bin),
|
|
103
|
+
`@pracht/vite-plugin`, and a target adapter such as `@pracht/adapter-node`.
|
|
104
|
+
There is no package named `pracht`. Add `@pracht/image` if the app used
|
|
105
|
+
`next/image`, and `sharp` only for the built-in Node optimization endpoint
|
|
106
|
+
or build-time `?pracht` imports.
|
|
107
|
+
- Scripts: `dev` → `pracht dev`, `build` → `pracht build`, `start` →
|
|
108
|
+
`node dist/server/server.js` (Node) or the platform deploy command; add
|
|
109
|
+
`preview` → `pracht preview`.
|
|
110
|
+
- Delete `next.config.*`, `next-env.d.ts`, `.next/`.
|
|
111
|
+
- In `tsconfig.json`, `"jsx": "preserve"` → `"jsx": "react-jsx"` with
|
|
112
|
+
`"jsxImportSource": "preact"`.
|
|
113
|
+
|
|
114
|
+
## Phase 2: Layouts → shells
|
|
143
115
|
|
|
144
116
|
```tsx
|
|
145
117
|
import type { ShellProps } from "@pracht/core";
|
|
146
118
|
|
|
147
119
|
export function Shell({ children }: ShellProps) {
|
|
148
|
-
return
|
|
149
|
-
<div class="root">
|
|
150
|
-
<main>{children}</main>
|
|
151
|
-
</div>
|
|
152
|
-
);
|
|
120
|
+
return <div class="root"><main>{children}</main></div>;
|
|
153
121
|
}
|
|
154
122
|
|
|
155
123
|
export function head() {
|
|
@@ -157,36 +125,15 @@ export function head() {
|
|
|
157
125
|
}
|
|
158
126
|
```
|
|
159
127
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- Use `class` not `className`.
|
|
164
|
-
- Register in `defineApp({ shells: { main: "./shells/main.tsx" } })`.
|
|
165
|
-
|
|
166
|
-
### Phase 3: Convert pages → route modules
|
|
167
|
-
|
|
168
|
-
For each `page.tsx`:
|
|
169
|
-
|
|
170
|
-
**Next.js (Server Component with data):**
|
|
171
|
-
|
|
172
|
-
```tsx
|
|
173
|
-
async function getData() {
|
|
174
|
-
const res = await fetch("https://api.example.com/data");
|
|
175
|
-
return res.json();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export default async function Page() {
|
|
179
|
-
const data = await getData();
|
|
180
|
-
return <div className="page">{data.title}</div>;
|
|
181
|
-
}
|
|
128
|
+
Shells must NOT render `<html>`, `<head>`, or `<body>` — the framework owns
|
|
129
|
+
the document, so move anything from `RootLayout`'s document tags into `head()`.
|
|
130
|
+
Register as `defineApp({ shells: { main: "./shells/main.tsx" } })`.
|
|
182
131
|
|
|
183
|
-
|
|
184
|
-
const data = await getData();
|
|
185
|
-
return { title: data.title };
|
|
186
|
-
}
|
|
187
|
-
```
|
|
132
|
+
## Phase 3: Pages → route modules
|
|
188
133
|
|
|
189
|
-
|
|
134
|
+
An `async` page that fetches and exports `generateMetadata` becomes three
|
|
135
|
+
exports — the fetch moves to `loader`, the metadata to `head`, the JSX stays
|
|
136
|
+
in the default export:
|
|
190
137
|
|
|
191
138
|
```tsx
|
|
192
139
|
import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
|
|
@@ -205,96 +152,40 @@ export default function Page({ data }: RouteComponentProps<typeof loader>) {
|
|
|
205
152
|
}
|
|
206
153
|
```
|
|
207
154
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
- Server-side data fetching → `loader()` export
|
|
211
|
-
- `generateMetadata` → `head()` export
|
|
212
|
-
- Keep `export default function Page` as the page component
|
|
213
|
-
- `className` → `class`
|
|
214
|
-
- No `async` components — data comes via props from loader
|
|
215
|
-
|
|
216
|
-
### Phase 4: Convert client components
|
|
217
|
-
|
|
218
|
-
**Next.js:**
|
|
219
|
-
|
|
220
|
-
```tsx
|
|
221
|
-
"use client";
|
|
222
|
-
import { useState } from "react";
|
|
223
|
-
|
|
224
|
-
export default function Counter() {
|
|
225
|
-
const [count, setCount] = useState(0);
|
|
226
|
-
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
**Pracht:**
|
|
231
|
-
|
|
232
|
-
```tsx
|
|
233
|
-
import { useState } from "preact/hooks";
|
|
234
|
-
|
|
235
|
-
export function Counter() {
|
|
236
|
-
const [count, setCount] = useState(0);
|
|
237
|
-
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Key transforms:
|
|
155
|
+
Components are never `async` — data arrives as the `data` prop.
|
|
242
156
|
|
|
243
|
-
|
|
244
|
-
- `import { ... } from "react"` → `import { ... } from "preact/hooks"` or `import { ... } from "preact/compat"`
|
|
245
|
-
- `import { ... } from "react-dom"` → `import { ... } from "preact/compat"`
|
|
157
|
+
## Phase 4: Client components
|
|
246
158
|
|
|
247
|
-
|
|
159
|
+
Drop the `"use client"` directive (pracht has no such concept), and repoint
|
|
160
|
+
imports: `react` → `preact/hooks` for hooks, `react`/`react-dom` →
|
|
161
|
+
`preact/compat` for everything else. The component body is otherwise unchanged.
|
|
248
162
|
|
|
249
|
-
|
|
163
|
+
**Islands:** if the source is mostly server components with a handful of
|
|
164
|
+
`"use client"` leaves, do not silently regress those pages to full-page
|
|
165
|
+
hydration. Set `hydration: "islands"` on the route (or
|
|
166
|
+
`export const HYDRATION = "islands"` in pages mode) and move the interactive
|
|
167
|
+
components into `src/islands/` — the rest renders as inert HTML. See
|
|
168
|
+
`docs/ISLANDS.md`.
|
|
250
169
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
255
|
-
|
|
256
|
-
export async function GET(request: NextRequest) {
|
|
257
|
-
const users = await getUsers();
|
|
258
|
-
return NextResponse.json(users);
|
|
259
|
-
}
|
|
260
|
-
```
|
|
170
|
+
## Phase 5: API routes
|
|
261
171
|
|
|
262
|
-
|
|
172
|
+
`app/api/users/route.ts` → `src/api/users.ts`, dynamic segments included
|
|
173
|
+
(`app/api/users/[id]/route.ts` → `src/api/users/[id].ts`). Handlers take
|
|
174
|
+
`ApiRouteArgs` and use web standards throughout:
|
|
263
175
|
|
|
264
176
|
```ts
|
|
265
177
|
import type { ApiRouteArgs } from "@pracht/core";
|
|
266
178
|
|
|
267
179
|
export async function GET({ request }: ApiRouteArgs) {
|
|
268
|
-
|
|
269
|
-
return Response.json(users);
|
|
180
|
+
return Response.json(await getUsers());
|
|
270
181
|
}
|
|
271
182
|
```
|
|
272
183
|
|
|
273
|
-
|
|
184
|
+
`NextRequest` → the standard `Request` from `ApiRouteArgs`;
|
|
185
|
+
`NextResponse.json()` → `Response.json()`. No manifest wiring — API routes are
|
|
186
|
+
auto-discovered.
|
|
274
187
|
|
|
275
|
-
|
|
276
|
-
- `NextResponse.json()` → `Response.json()` (Web standard)
|
|
277
|
-
- Dynamic segments: `app/api/users/[id]/route.ts` → `src/api/users/[id].ts`
|
|
278
|
-
- No manifest wiring needed — auto-discovered
|
|
279
|
-
|
|
280
|
-
### Phase 6: Convert middleware
|
|
281
|
-
|
|
282
|
-
**Next.js (`middleware.ts`):**
|
|
283
|
-
|
|
284
|
-
```ts
|
|
285
|
-
import { NextResponse } from "next/server";
|
|
286
|
-
import type { NextRequest } from "next/server";
|
|
287
|
-
|
|
288
|
-
export function middleware(request: NextRequest) {
|
|
289
|
-
const session = request.cookies.get("session");
|
|
290
|
-
if (!session) return NextResponse.redirect(new URL("/login", request.url));
|
|
291
|
-
return NextResponse.next();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export const config = { matcher: ["/dashboard/:path*"] };
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**Pracht (`src/middleware/auth.ts`):**
|
|
188
|
+
## Phase 6: Middleware
|
|
298
189
|
|
|
299
190
|
```ts
|
|
300
191
|
import { redirect, type MiddlewareFn } from "@pracht/core";
|
|
@@ -306,44 +197,30 @@ export const middleware: MiddlewareFn = async ({ request }, next) => {
|
|
|
306
197
|
};
|
|
307
198
|
```
|
|
308
199
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
group({ middleware: ["auth"] }, [
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Key transforms:
|
|
318
|
-
|
|
319
|
-
- Path matching moves from `config.matcher` to manifest group/route assignment
|
|
320
|
-
- `NextResponse.redirect()` → `return redirect("/path", { request })`
|
|
321
|
-
- `NextResponse.next()` → `return next()`
|
|
322
|
-
- Pracht middleware is **wrap-around** (Hono/Koa/Astro shape), so you can
|
|
323
|
-
also `await next()` and observe the response — useful for tracing.
|
|
200
|
+
`NextResponse.redirect()` → `return redirect("/path", { request })`;
|
|
201
|
+
`NextResponse.next()` → `return next()`. Path matching moves out of
|
|
202
|
+
`config.matcher` and into manifest assignment:
|
|
203
|
+
`group({ middleware: ["auth"] }, [route("/dashboard", …)])`. Pracht middleware
|
|
204
|
+
is wrap-around (Hono/Koa/Astro shape), so you can `await next()` and observe
|
|
205
|
+
the response — useful for tracing.
|
|
324
206
|
|
|
325
|
-
|
|
207
|
+
## Phase 7: Route manifest
|
|
326
208
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
209
|
+
Skip this phase for `pagesDir` projects. Prefer
|
|
210
|
+
`pracht generate route --path ... --render ...` (plus `--shell`/`--middleware`/
|
|
211
|
+
`--loader`) per page — it creates a wired skeleton **and** updates
|
|
212
|
+
`src/routes.ts` — then port the Next.js bodies into the generated files.
|
|
213
|
+
Hand-write manifest entries only for shapes the generator cannot express.
|
|
332
214
|
|
|
333
215
|
```ts
|
|
334
216
|
import { defineApp, group, route } from "@pracht/core";
|
|
335
217
|
|
|
336
218
|
export const app = defineApp({
|
|
337
|
-
shells: {
|
|
338
|
-
|
|
339
|
-
},
|
|
340
|
-
middleware: {
|
|
341
|
-
auth: () => import("./middleware/auth.ts"),
|
|
342
|
-
},
|
|
219
|
+
shells: { main: () => import("./shells/main.tsx") },
|
|
220
|
+
middleware: { auth: () => import("./middleware/auth.ts") },
|
|
343
221
|
routes: [
|
|
344
222
|
group({ shell: "main" }, [
|
|
345
223
|
route("/", () => import("./routes/home.tsx"), { render: "ssg" }),
|
|
346
|
-
route("/about", () => import("./routes/about.tsx"), { render: "ssg" }),
|
|
347
224
|
route("/dashboard", () => import("./routes/dashboard.tsx"), {
|
|
348
225
|
render: "ssr",
|
|
349
226
|
middleware: ["auth"],
|
|
@@ -351,113 +228,63 @@ export const app = defineApp({
|
|
|
351
228
|
route("/blog/:slug", () => import("./routes/blog-post.tsx"), { render: "isg" }),
|
|
352
229
|
]),
|
|
353
230
|
],
|
|
354
|
-
notFound: {
|
|
355
|
-
component: () => import("./routes/not-found.tsx"),
|
|
356
|
-
shell: "main",
|
|
357
|
-
},
|
|
231
|
+
notFound: { component: () => import("./routes/not-found.tsx"), shell: "main" },
|
|
358
232
|
});
|
|
359
233
|
```
|
|
360
234
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
235
|
+
Module references accept `() => import("./path")` (better IDE navigation) or a
|
|
236
|
+
plain `"./path"` string. Pick render modes from the Next.js original:
|
|
237
|
+
|
|
238
|
+
| Next.js original | Render mode |
|
|
239
|
+
| --------------------------------------------- | -------------------------------------------------------- |
|
|
240
|
+
| No data fetching, or `generateStaticParams` | `"ssg"` |
|
|
241
|
+
| `cookies()`, `headers()`, per-request data | `"ssr"` |
|
|
242
|
+
| `revalidate` option | `"isg"` + `timeRevalidate(seconds)` |
|
|
243
|
+
| `revalidatePath` / `res.revalidate()` | `"isg"` + `webhookRevalidate()`, triggered by `POST /__pracht/revalidate` |
|
|
244
|
+
| Client-only | `"spa"` |
|
|
245
|
+
|
|
246
|
+
## Phase 8: Remaining patterns
|
|
247
|
+
|
|
248
|
+
**Links and navigation.** After the manifest exists, run `pracht typegen` and
|
|
249
|
+
switch known app routes to route ids: `<Link route="product" params={{ id }}>`
|
|
250
|
+
and `navigate({ route: "dashboard" })`. Plain `<a href="/about">` and
|
|
251
|
+
`navigate("/dashboard")` keep working for simple, external, or user-provided
|
|
252
|
+
URLs.
|
|
253
|
+
|
|
254
|
+
**Images.** `<Image>` from `@pracht/image` takes the same
|
|
255
|
+
`width`/`height`/`fill`/`sizes`/`quality` and priority intent — preserve them.
|
|
256
|
+
Pick the loader for the deployment target:
|
|
257
|
+
|
|
258
|
+
| Target | Loader |
|
|
259
|
+
| ------------------ | -------------------------------------------------------------------------------- |
|
|
260
|
+
| Node | `createImageHandler()` from `@pracht/image/node` + `sharp`; set its `localOrigin` to the same trusted value as `nodeAdapter({ canonicalOrigin })` |
|
|
261
|
+
| Cloudflare Workers | `cloudflareLoader` — never bundle the Node handler, `sharp` does not run in Workers |
|
|
262
|
+
| Vercel Edge | `vercelLoader`, with Vercel's allowed image sizes aligned to the pracht breakpoints |
|
|
263
|
+
| Static hosts | `passthroughLoader` |
|
|
264
|
+
|
|
265
|
+
Static imports and blur placeholders migrate too: `import photo from
|
|
266
|
+
"./photo.jpg"` → `"./photo.jpg?pracht"`, add `prachtImage()` (from
|
|
267
|
+
`@pracht/image/vite`) to the Vite plugins, reference the `@pracht/image/client`
|
|
268
|
+
types once in a `.d.ts`, and keep `<Image src={photo} placeholder="blur" />`
|
|
269
|
+
as-is — the import supplies `width`/`height`/`blurDataURL` exactly like Next's
|
|
270
|
+
static imports, though pracht's blur is CSS-only (no fade, no inline handlers).
|
|
271
|
+
Where `next/image` produced files during a static export, use
|
|
272
|
+
`?pracht&pracht-static`: it emits cached responsive WebP variants and bypasses
|
|
273
|
+
the runtime loader while keeping plain hydration-free `<img>` markup. For
|
|
274
|
+
relative images in Markdown, `defineMarkdownCollection()` from
|
|
275
|
+
`@pracht/markdown` applies the same pipeline to ``. Leave
|
|
276
|
+
`public/` and remote URLs unchanged, and use an absolute Vite `base` for static
|
|
277
|
+
variants. See `docs/IMAGES.md`.
|
|
278
|
+
|
|
279
|
+
**Server Actions.** A `"use server"` mutation becomes an API route; the
|
|
280
|
+
`revalidatePath` half becomes an authenticated webhook call:
|
|
406
281
|
|
|
407
|
-
|
|
408
|
-
`sharp`, and set its `localOrigin` to the same trusted value as
|
|
409
|
-
`nodeAdapter({ canonicalOrigin })`.
|
|
410
|
-
- Cloudflare Workers: configure `cloudflareLoader`; do not bundle the Node
|
|
411
|
-
handler because `sharp` does not run in Workers.
|
|
412
|
-
- Vercel Edge: configure `vercelLoader` and keep Vercel's allowed image sizes
|
|
413
|
-
aligned with the Pracht breakpoints.
|
|
414
|
-
- Static hosts: configure `passthroughLoader`.
|
|
415
|
-
|
|
416
|
-
Preserve the original `width`, `height`, `fill`, `sizes`, `quality`, and
|
|
417
|
-
priority intent. See `docs/IMAGES.md` for the endpoint and loader wiring.
|
|
418
|
-
|
|
419
|
-
Static imports and blur placeholders migrate too: replace
|
|
420
|
-
`import photo from "./photo.jpg"` with `import photo from "./photo.jpg?pracht"`,
|
|
421
|
-
add `prachtImage()` (from `@pracht/image/vite`) to the Vite plugins, reference
|
|
422
|
-
the `@pracht/image/client` types once in a `.d.ts`, and keep
|
|
423
|
-
`<Image src={photo} placeholder="blur" />` as-is — the import supplies
|
|
424
|
-
`width`/`height`/`blurDataURL` exactly like Next's static imports. Pracht's
|
|
425
|
-
blur is CSS-only (no fade animation, no inline event handlers).
|
|
426
|
-
|
|
427
|
-
#### `useRouter` → navigation
|
|
428
|
-
|
|
429
|
-
```tsx
|
|
430
|
-
// Next.js
|
|
431
|
-
import { useRouter } from "next/navigation";
|
|
432
|
-
const router = useRouter();
|
|
433
|
-
router.push("/dashboard");
|
|
434
|
-
|
|
435
|
-
// Pracht
|
|
436
|
-
import { useNavigate } from "@pracht/core";
|
|
437
|
-
const navigate = useNavigate();
|
|
438
|
-
navigate("/dashboard");
|
|
439
|
-
|
|
440
|
-
// After `pracht typegen`, prefer route ids for known routes
|
|
441
|
-
navigate({ route: "dashboard" });
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
#### Server Actions → API routes
|
|
445
|
-
|
|
446
|
-
```tsx
|
|
447
|
-
// Next.js
|
|
448
|
-
"use server";
|
|
449
|
-
async function createPost(formData: FormData) {
|
|
450
|
-
await db.insert({ title: formData.get("title") });
|
|
451
|
-
revalidatePath("/posts");
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Pracht — API route handler
|
|
282
|
+
```ts
|
|
455
283
|
import { withBase, type ApiRouteArgs } from "@pracht/core";
|
|
456
284
|
|
|
457
285
|
export async function POST({ request }: ApiRouteArgs) {
|
|
458
286
|
const form = await request.formData();
|
|
459
287
|
await db.insert({ title: form.get("title") });
|
|
460
|
-
// revalidatePath("/posts") equivalent: regenerate the ISG page on demand
|
|
461
288
|
await fetch(new URL(withBase("/__pracht/revalidate"), request.url), {
|
|
462
289
|
method: "POST",
|
|
463
290
|
headers: {
|
|
@@ -466,81 +293,39 @@ export async function POST({ request }: ApiRouteArgs) {
|
|
|
466
293
|
},
|
|
467
294
|
body: JSON.stringify({ paths: ["/posts"] }),
|
|
468
295
|
});
|
|
469
|
-
return new Response(null, {
|
|
470
|
-
status: 303,
|
|
471
|
-
headers: { location: withBase("/posts") },
|
|
472
|
-
});
|
|
296
|
+
return new Response(null, { status: 303, headers: { location: withBase("/posts") } });
|
|
473
297
|
}
|
|
474
298
|
```
|
|
475
299
|
|
|
476
|
-
|
|
300
|
+
That call only does something if `/posts` is `render: "isg"` and opts in with
|
|
301
|
+
`revalidate: webhookRevalidate()` (or `[timeRevalidate(seconds),
|
|
302
|
+
webhookRevalidate()]`, both imported from `@pracht/core`), and
|
|
303
|
+
`PRACHT_REVALIDATE_TOKEN` is set in the runtime environment. If `/posts` is a
|
|
304
|
+
plain SSR route, drop the call — the redirect re-renders it fresh.
|
|
477
305
|
|
|
478
|
-
|
|
306
|
+
## Phase 9: Clean up and verify
|
|
479
307
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const ua = headers().get("user-agent");
|
|
485
|
-
|
|
486
|
-
// Pracht — available in loader args
|
|
487
|
-
export async function loader({ request }: LoaderArgs) {
|
|
488
|
-
const cookies = request.headers.get("cookie");
|
|
489
|
-
const ua = request.headers.get("user-agent");
|
|
490
|
-
return {
|
|
491
|
-
/* ... */
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
### Phase 9: Clean up
|
|
497
|
-
|
|
498
|
-
1. Remove all `"use client"` and `"use server"` directives.
|
|
499
|
-
2. Remove all `next/*` imports (`next/link`, `next/image`, `next/navigation`, `next/headers`).
|
|
500
|
-
3. Search for remaining `className` → replace with `class`.
|
|
501
|
-
4. Search for remaining `react` imports → replace with `preact` equivalents.
|
|
502
|
-
5. Remove `next.config.*`, `next-env.d.ts`, `.next/` directory.
|
|
503
|
-
6. Run `pracht typegen` if route ids/paths changed or if you converted links/navigation to typed route ids.
|
|
504
|
-
7. Run the dev server (`pracht dev`) and fix any remaining issues.
|
|
308
|
+
Sweep for leftovers: `"use client"`/`"use server"` directives, `next/*`
|
|
309
|
+
imports, `className`, `react` imports, and `next.config.*`/`next-env.d.ts`/
|
|
310
|
+
`.next/`. Run `pracht typegen` if route ids or paths changed, then `pracht dev`
|
|
311
|
+
and fix errors iteratively.
|
|
505
312
|
|
|
506
|
-
## Dependency
|
|
313
|
+
## Dependency mapping
|
|
507
314
|
|
|
508
315
|
| Next.js package | Pracht equivalent |
|
|
509
316
|
| --------------- | ---------------------------------------------------------------------------- |
|
|
510
|
-
| `next` | `@pracht/core` + `@pracht/cli` + `@pracht/vite-plugin` +
|
|
511
|
-
| `next/image` | `@pracht/image`
|
|
512
|
-
| `react`
|
|
513
|
-
| `
|
|
514
|
-
| `next/font/local` | `defineFont()` from `@pracht/core` — register via `head() { return { fonts: [font] } }`, use `font.className`/`font.style` in components |
|
|
317
|
+
| `next` | `@pracht/core` + `@pracht/cli` + `@pracht/vite-plugin` + a target adapter |
|
|
318
|
+
| `next/image` | `@pracht/image` |
|
|
319
|
+
| `react`, `react-dom` | `preact` |
|
|
320
|
+
| `next/font/local` | `defineFont()` from `@pracht/core` — register via `head() { return { fonts: [font] } }`, use `font.className`/`font.style` |
|
|
515
321
|
| `next/font/google` | Download the woff2 files into `public/fonts/` (e.g. via google-webfonts-helper), then `defineFont()` — pracht never fetches fonts at build time |
|
|
516
|
-
| `@next/mdx` | `@mdx-js/rollup` (Vite plugin)
|
|
517
|
-
| `next-auth` | Direct integration in middleware/loaders
|
|
518
|
-
| `next/og` | `@vercel/og` or custom solution
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
```ts
|
|
525
|
-
resolve: {
|
|
526
|
-
alias: {
|
|
527
|
-
"react": "preact/compat",
|
|
528
|
-
"react-dom": "preact/compat",
|
|
529
|
-
"react/jsx-runtime": "preact/jsx-runtime",
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Note: The pracht Vite plugin sets these aliases automatically. Only add manual aliases if a dependency doesn't resolve correctly.
|
|
535
|
-
|
|
536
|
-
## Rules
|
|
537
|
-
|
|
538
|
-
1. Always read the Next.js source before converting — understand what each file does.
|
|
539
|
-
2. Migrate in phases: setup → shells → routes → API → middleware → manifest → cleanup.
|
|
540
|
-
3. Prefer the simplest pracht equivalent. Don't over-engineer the migration.
|
|
541
|
-
4. Identify React libraries that need `preact/compat` aliasing and flag them.
|
|
542
|
-
5. After migration, run `pracht dev` to verify. Fix errors iteratively.
|
|
543
|
-
6. If a Next.js feature has no pracht equivalent, explain the gap and suggest alternatives.
|
|
544
|
-
7. Use Preact idioms: `class` not `className`, no `React` import needed, `preact/hooks` for hooks.
|
|
322
|
+
| `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
|
|
323
|
+
| `next-auth` | Direct integration in middleware/loaders |
|
|
324
|
+
| `next/og` | `@vercel/og` or a custom solution |
|
|
325
|
+
|
|
326
|
+
Most React libraries work through `preact/compat`, and the pracht Vite plugin
|
|
327
|
+
already aliases `react`/`react-dom`/`react/jsx-runtime` for you. Add manual
|
|
328
|
+
`resolve.alias` entries only when a dependency still fails to resolve — and
|
|
329
|
+
flag those libraries to the user.
|
|
545
330
|
|
|
546
331
|
$ARGUMENTS
|