create-pracht 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,536 @@
1
+ ---
2
+ name: migrate-nextjs
3
+ version: 1.2.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` | `<Image>` from `@pracht/image` | Responsive srcsets plus Node, Cloudflare, Vercel, or passthrough loaders |
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
+ - If the app imports `next/image`, add `@pracht/image`; add `sharp` only for the built-in Node optimization endpoint.
121
+ - 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
122
+ 4. Remove Next.js config files: `next.config.*`, `next-env.d.ts`, `.next/`
123
+ 5. If `tsconfig.json` has `"jsx": "preserve"`, change to `"jsx": "react-jsx"` and add `"jsxImportSource": "preact"`.
124
+
125
+ ### Phase 2: Convert layouts → shells
126
+
127
+ For each `layout.tsx`:
128
+
129
+ **Next.js:**
130
+
131
+ ```tsx
132
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
133
+ return (
134
+ <html>
135
+ <body className="root">{children}</body>
136
+ </html>
137
+ );
138
+ }
139
+ ```
140
+
141
+ **Pracht:**
142
+
143
+ ```tsx
144
+ import type { ShellProps } from "@pracht/core";
145
+
146
+ export function Shell({ children }: ShellProps) {
147
+ return (
148
+ <div class="root">
149
+ <main>{children}</main>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ export function head() {
155
+ return { title: "My App" };
156
+ }
157
+ ```
158
+
159
+ Key differences:
160
+
161
+ - Pracht shells do NOT render `<html>`, `<head>`, or `<body>` — the framework owns the HTML document.
162
+ - Use `class` not `className`.
163
+ - Register in `defineApp({ shells: { main: "./shells/main.tsx" } })`.
164
+
165
+ ### Phase 3: Convert pages → route modules
166
+
167
+ For each `page.tsx`:
168
+
169
+ **Next.js (Server Component with data):**
170
+
171
+ ```tsx
172
+ async function getData() {
173
+ const res = await fetch("https://api.example.com/data");
174
+ return res.json();
175
+ }
176
+
177
+ export default async function Page() {
178
+ const data = await getData();
179
+ return <div className="page">{data.title}</div>;
180
+ }
181
+
182
+ export async function generateMetadata() {
183
+ const data = await getData();
184
+ return { title: data.title };
185
+ }
186
+ ```
187
+
188
+ **Pracht:**
189
+
190
+ ```tsx
191
+ import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
192
+
193
+ export async function loader(_args: LoaderArgs) {
194
+ const res = await fetch("https://api.example.com/data");
195
+ return res.json();
196
+ }
197
+
198
+ export function head({ data }: { data: Awaited<ReturnType<typeof loader>> }) {
199
+ return { title: data.title };
200
+ }
201
+
202
+ export default function Page({ data }: RouteComponentProps<typeof loader>) {
203
+ return <div class="page">{data.title}</div>;
204
+ }
205
+ ```
206
+
207
+ Key transforms:
208
+
209
+ - Server-side data fetching → `loader()` export
210
+ - `generateMetadata` → `head()` export
211
+ - Keep `export default function Page` as the page component
212
+ - `className` → `class`
213
+ - No `async` components — data comes via props from loader
214
+
215
+ ### Phase 4: Convert client components
216
+
217
+ **Next.js:**
218
+
219
+ ```tsx
220
+ "use client";
221
+ import { useState } from "react";
222
+
223
+ export default function Counter() {
224
+ const [count, setCount] = useState(0);
225
+ return <button onClick={() => setCount(count + 1)}>{count}</button>;
226
+ }
227
+ ```
228
+
229
+ **Pracht:**
230
+
231
+ ```tsx
232
+ import { useState } from "preact/hooks";
233
+
234
+ export function Counter() {
235
+ const [count, setCount] = useState(0);
236
+ return <button onClick={() => setCount(count + 1)}>{count}</button>;
237
+ }
238
+ ```
239
+
240
+ Key transforms:
241
+
242
+ - Remove `"use client"` directive — not needed in pracht
243
+ - `import { ... } from "react"` → `import { ... } from "preact/hooks"` or `import { ... } from "preact/compat"`
244
+ - `import { ... } from "react-dom"` → `import { ... } from "preact/compat"`
245
+
246
+ **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`.
247
+
248
+ ### Phase 5: Convert API routes
249
+
250
+ **Next.js (`app/api/users/route.ts`):**
251
+
252
+ ```ts
253
+ import { NextRequest, NextResponse } from "next/server";
254
+
255
+ export async function GET(request: NextRequest) {
256
+ const users = await getUsers();
257
+ return NextResponse.json(users);
258
+ }
259
+ ```
260
+
261
+ **Pracht (`src/api/users.ts`):**
262
+
263
+ ```ts
264
+ import type { ApiRouteArgs } from "@pracht/core";
265
+
266
+ export async function GET({ request }: ApiRouteArgs) {
267
+ const users = await getUsers();
268
+ return Response.json(users);
269
+ }
270
+ ```
271
+
272
+ Key transforms:
273
+
274
+ - `NextRequest` → standard `Request` (via `ApiRouteArgs`)
275
+ - `NextResponse.json()` → `Response.json()` (Web standard)
276
+ - Dynamic segments: `app/api/users/[id]/route.ts` → `src/api/users/[id].ts`
277
+ - No manifest wiring needed — auto-discovered
278
+
279
+ ### Phase 6: Convert middleware
280
+
281
+ **Next.js (`middleware.ts`):**
282
+
283
+ ```ts
284
+ import { NextResponse } from "next/server";
285
+ import type { NextRequest } from "next/server";
286
+
287
+ export function middleware(request: NextRequest) {
288
+ const session = request.cookies.get("session");
289
+ if (!session) return NextResponse.redirect(new URL("/login", request.url));
290
+ return NextResponse.next();
291
+ }
292
+
293
+ export const config = { matcher: ["/dashboard/:path*"] };
294
+ ```
295
+
296
+ **Pracht (`src/middleware/auth.ts`):**
297
+
298
+ ```ts
299
+ import { redirect, type MiddlewareFn } from "@pracht/core";
300
+
301
+ export const middleware: MiddlewareFn = async ({ request }, next) => {
302
+ const session = request.headers.get("cookie")?.includes("session");
303
+ if (!session) return redirect("/login", { request });
304
+ return next();
305
+ };
306
+ ```
307
+
308
+ Then apply it in the manifest:
309
+
310
+ ```ts
311
+ group({ middleware: ["auth"] }, [
312
+ route("/dashboard", () => import("./routes/dashboard.tsx"), { render: "ssr" }),
313
+ ]);
314
+ ```
315
+
316
+ Key transforms:
317
+
318
+ - Path matching moves from `config.matcher` to manifest group/route assignment
319
+ - `NextResponse.redirect()` → `return redirect("/path", { request })`
320
+ - `NextResponse.next()` → `return next()`
321
+ - Pracht middleware is **wrap-around** (Hono/Koa/Astro shape), so you can
322
+ also `await next()` and observe the response — useful for tracing.
323
+
324
+ ### Phase 7: Wire the route manifest
325
+
326
+ **Note:** For pages router projects using `pagesDir`, this phase is automatic. Skip to Phase 8.
327
+
328
+ 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.
329
+
330
+ Build `src/routes.ts` mapping every migrated page. Module references accept `() => import("./path")` (enables IDE navigation) or plain `"./path"` strings — both work:
331
+
332
+ ```ts
333
+ import { defineApp, group, route } from "@pracht/core";
334
+
335
+ export const app = defineApp({
336
+ shells: {
337
+ main: () => import("./shells/main.tsx"),
338
+ },
339
+ middleware: {
340
+ auth: () => import("./middleware/auth.ts"),
341
+ },
342
+ routes: [
343
+ group({ shell: "main" }, [
344
+ route("/", () => import("./routes/home.tsx"), { render: "ssg" }),
345
+ route("/about", () => import("./routes/about.tsx"), { render: "ssg" }),
346
+ route("/dashboard", () => import("./routes/dashboard.tsx"), {
347
+ render: "ssr",
348
+ middleware: ["auth"],
349
+ }),
350
+ route("/blog/:slug", () => import("./routes/blog-post.tsx"), { render: "isg" }),
351
+ ]),
352
+ ],
353
+ notFound: {
354
+ component: () => import("./routes/not-found.tsx"),
355
+ shell: "main",
356
+ },
357
+ });
358
+ ```
359
+
360
+ Choose render modes based on the Next.js original:
361
+
362
+ - Static pages (no data fetching, or `generateStaticParams`) → `"ssg"`
363
+ - Dynamic pages (`cookies()`, `headers()`, per-request data) → `"ssr"`
364
+ - ISR pages (`revalidate` option) → `"isg"` with `timeRevalidate(seconds)`
365
+ - On-demand ISR (`revalidatePath` / `res.revalidate()`) → add `webhookRevalidate()` (alone or as `[timeRevalidate(seconds), webhookRevalidate()]`) and trigger via `POST /__pracht/revalidate`
366
+ - Client-only pages → `"spa"`
367
+
368
+ ### Phase 8: Handle common patterns
369
+
370
+ #### `next/link` → typed `<Link>` or plain `<a>`
371
+
372
+ After manifest wiring is in place, run `pracht typegen` and prefer route-id based links for known app routes:
373
+
374
+ ```tsx
375
+ // Next.js
376
+ import Link from "next/link";
377
+ <Link href={`/products/${id}`}>Product</Link>
378
+
379
+ // Pracht
380
+ import { Link } from "@pracht/core";
381
+ <Link route="product" params={{ id }}>Product</Link>
382
+ ```
383
+
384
+ Plain anchors still work for simple, external, or user-provided URLs because the client router intercepts same-origin `<a>` clicks:
385
+
386
+ ```tsx
387
+ <a href="/about">About</a>
388
+ ```
389
+
390
+ `<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.
391
+
392
+ #### `next/image` → `@pracht/image`
393
+
394
+ ```tsx
395
+ // Next.js
396
+ import Image from "next/image";
397
+ <Image src="/photo.jpg" width={500} height={300} alt="Photo" />
398
+
399
+ // Pracht
400
+ import { Image } from "@pracht/image";
401
+ <Image src="/photo.jpg" width={500} height={300} alt="Photo" />
402
+ ```
403
+
404
+ Choose the loader for the deployment target:
405
+
406
+ - Node: mount `createImageHandler()` from `@pracht/image/node`, install
407
+ `sharp`, and set its `localOrigin` to the same trusted value as
408
+ `nodeAdapter({ canonicalOrigin })`.
409
+ - Cloudflare Workers: configure `cloudflareLoader`; do not bundle the Node
410
+ handler because `sharp` does not run in Workers.
411
+ - Vercel Edge: configure `vercelLoader` and keep Vercel's allowed image sizes
412
+ aligned with the Pracht breakpoints.
413
+ - Static hosts: configure `passthroughLoader`.
414
+
415
+ Preserve the original `width`, `height`, `fill`, `sizes`, `quality`, and
416
+ priority intent. See `docs/IMAGES.md` for the endpoint and loader wiring.
417
+
418
+ #### `useRouter` → navigation
419
+
420
+ ```tsx
421
+ // Next.js
422
+ import { useRouter } from "next/navigation";
423
+ const router = useRouter();
424
+ router.push("/dashboard");
425
+
426
+ // Pracht
427
+ import { useNavigate } from "@pracht/core";
428
+ const navigate = useNavigate();
429
+ navigate("/dashboard");
430
+
431
+ // After `pracht typegen`, prefer route ids for known routes
432
+ navigate({ route: "dashboard" });
433
+ ```
434
+
435
+ #### Server Actions → API routes
436
+
437
+ ```tsx
438
+ // Next.js
439
+ "use server";
440
+ async function createPost(formData: FormData) {
441
+ await db.insert({ title: formData.get("title") });
442
+ revalidatePath("/posts");
443
+ }
444
+
445
+ // Pracht — API route handler
446
+ import type { ApiRouteArgs } from "@pracht/core";
447
+
448
+ export async function POST({ request }: ApiRouteArgs) {
449
+ const form = await request.formData();
450
+ await db.insert({ title: form.get("title") });
451
+ // revalidatePath("/posts") equivalent: regenerate the ISG page on demand
452
+ await fetch(new URL("/__pracht/revalidate", request.url), {
453
+ method: "POST",
454
+ headers: {
455
+ authorization: `Bearer ${process.env.PRACHT_REVALIDATE_TOKEN}`,
456
+ "content-type": "application/json",
457
+ },
458
+ body: JSON.stringify({ paths: ["/posts"] }),
459
+ });
460
+ return new Response(null, {
461
+ status: 303,
462
+ headers: { location: "/posts" },
463
+ });
464
+ }
465
+ ```
466
+
467
+ 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.
468
+
469
+ #### `cookies()` / `headers()` → loader args
470
+
471
+ ```tsx
472
+ // Next.js
473
+ import { cookies, headers } from "next/headers";
474
+ const session = cookies().get("session");
475
+ const ua = headers().get("user-agent");
476
+
477
+ // Pracht — available in loader args
478
+ export async function loader({ request }: LoaderArgs) {
479
+ const cookies = request.headers.get("cookie");
480
+ const ua = request.headers.get("user-agent");
481
+ return {
482
+ /* ... */
483
+ };
484
+ }
485
+ ```
486
+
487
+ ### Phase 9: Clean up
488
+
489
+ 1. Remove all `"use client"` and `"use server"` directives.
490
+ 2. Remove all `next/*` imports (`next/link`, `next/image`, `next/navigation`, `next/headers`).
491
+ 3. Search for remaining `className` → replace with `class`.
492
+ 4. Search for remaining `react` imports → replace with `preact` equivalents.
493
+ 5. Remove `next.config.*`, `next-env.d.ts`, `.next/` directory.
494
+ 6. Run `pracht typegen` if route ids/paths changed or if you converted links/navigation to typed route ids.
495
+ 7. Run the dev server (`pracht dev`) and fix any remaining issues.
496
+
497
+ ## Dependency Mapping
498
+
499
+ | Next.js package | Pracht equivalent |
500
+ | --------------- | ---------------------------------------------------------------------------- |
501
+ | `next` | `@pracht/core` + `@pracht/cli` + `@pracht/vite-plugin` + `@pracht/adapter-node` (or target adapter) |
502
+ | `next/image` | `@pracht/image` |
503
+ | `react` | `preact` |
504
+ | `react-dom` | `preact` |
505
+ | `@next/font` | CSS `@font-face` or `fontsource` packages |
506
+ | `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
507
+ | `next-auth` | Direct integration in middleware/loaders |
508
+ | `next/og` | `@vercel/og` or custom solution |
509
+
510
+ ## React Library Compatibility
511
+
512
+ Many React libraries work with Preact via `preact/compat`. Add aliases in `vite.config.ts` if needed:
513
+
514
+ ```ts
515
+ resolve: {
516
+ alias: {
517
+ "react": "preact/compat",
518
+ "react-dom": "preact/compat",
519
+ "react/jsx-runtime": "preact/jsx-runtime",
520
+ }
521
+ }
522
+ ```
523
+
524
+ Note: The pracht Vite plugin sets these aliases automatically. Only add manual aliases if a dependency doesn't resolve correctly.
525
+
526
+ ## Rules
527
+
528
+ 1. Always read the Next.js source before converting — understand what each file does.
529
+ 2. Migrate in phases: setup → shells → routes → API → middleware → manifest → cleanup.
530
+ 3. Prefer the simplest pracht equivalent. Don't over-engineer the migration.
531
+ 4. Identify React libraries that need `preact/compat` aliasing and flag them.
532
+ 5. After migration, run `pracht dev` to verify. Fix errors iteratively.
533
+ 6. If a Next.js feature has no pracht equivalent, explain the gap and suggest alternatives.
534
+ 7. Use Preact idioms: `class` not `className`, no `React` import needed, `preact/hooks` for hooks.
535
+
536
+ $ARGUMENTS