jskelet 0.6.2 → 0.6.3
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/AGENTS.md +136 -136
- package/CHANGELOG.md +620 -596
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -309
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +661 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1443 -1423
- package/docs/07-yapilandirma.md +12 -6
- package/docs/08-build.md +429 -428
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +338 -338
- package/docs/12-panel-ve-oturum.md +478 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -328
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +669 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1453 -1431
- package/docs/en/07-configuration.md +1219 -1214
- package/docs/en/08-build.md +447 -446
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +340 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +488 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +17 -1
- package/src/config/index.js +13 -0
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +230 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -462
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -0
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1122
- package/src/server/image-optimizer.js +500 -407
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -66
- package/src/server/logs/pipeline.js +165 -158
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -100
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +356 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1058
- package/src/server/redis.js +588 -569
- package/src/server/render.js +4 -4
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +15 -1
- package/types/config/index.d.ts +8 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/redis.d.ts +2 -1
package/docs/en/11-migration.md
CHANGED
|
@@ -1,398 +1,398 @@
|
|
|
1
|
-
# 11 — Migrating from Next.js
|
|
2
|
-
|
|
3
|
-
This document explains how to move a project using the Next.js App Router over
|
|
4
|
-
to JSkelet: a table of concept and API equivalents, an explicit list of what
|
|
5
|
-
cannot be migrated, and a step-by-step plan. JSkelet's surface was deliberately
|
|
6
|
-
modeled on the subset of Next that people actually use — concepts like the
|
|
7
|
-
`next.config` syntax, the Metadata API, `notFound()`, `revalidate` and `cache()`
|
|
8
|
-
will feel familiar. The *reasons* behind the differences are in
|
|
9
|
-
[02-architecture.md](./02-architecture.md).
|
|
10
|
-
|
|
11
|
-
## `jskelet migrate` (codemod)
|
|
12
|
-
|
|
13
|
-
Run the codemod against an App Router tree. Babel (`@babel/parser`,
|
|
14
|
-
`@babel/types`) ships with JSkelet — no extra install.
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npx jskelet migrate scan ../my-next-app
|
|
18
|
-
npx jskelet migrate apply ../my-next-app --out . --write
|
|
19
|
-
npx jskelet migrate config ../my-next-app --write
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
| Command | What it does |
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| `migrate` / `migrate scan` | Inventory pages, layouts, `"use client"` modules, blockers (nested layouts, Server Actions, Suspense). |
|
|
25
|
-
| `migrate apply` | **Automatic convert:** `page.*` → feature controller + `.jsk`; presentational components → `views/components/*.js`; clients → island `mount()` stubs. Default is dry-run; pass `--write`. Never overwrites (conflicts get a `.migrate` suffix). |
|
|
26
|
-
| `migrate config` | Draft `jskelet.config.mjs` from `next.config` (`headers` / `redirects` / `rewrites`, `images.widths`, `NEXT_PUBLIC_*` → `clientEnv`). |
|
|
27
|
-
|
|
28
|
-
Flags: `--out <dir>`, `--only pages,components,islands`, `--json`, `--strict` (exit 1 on partial/skipped).
|
|
29
|
-
|
|
30
|
-
**Converted automatically:** `className`, `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}`, `next/image` → `<Image />`, `next/link` → `<Link />`, `dangerouslySetInnerHTML`, `revalidate`, simple controller prelude (`await` data + `notFound()`).
|
|
31
|
-
|
|
32
|
-
**Not converted (reported):** React hooks, Server Actions, nested layout flattening, Streaming/Suspense, client-side routing. Confidence per file is `ok` / `partial` / `skipped`.
|
|
33
|
-
|
|
34
|
-
## Equivalence table
|
|
35
|
-
|
|
36
|
-
### Configuration
|
|
37
|
-
|
|
38
|
-
| Next.js | JSkelet | Note |
|
|
39
|
-
| --- | --- | --- |
|
|
40
|
-
| `next.config.mjs` | `jskelet.config.mjs` | Same spirit, smaller surface ([07](./07-configuration.md)) |
|
|
41
|
-
| `headers()` | `headers()` | Same shape: `{ source, headers: [{ key, value }] }` |
|
|
42
|
-
| `redirects()` | `redirects()` | `permanent` → 308, otherwise 307; can be overridden with `statusCode` |
|
|
43
|
-
| `trailingSlash` | `trailingSlash` | `true` → canonical URLs end with `/` (308); default `false` does not strip ([07](./07-configuration.md)) |
|
|
44
|
-
| `rewrites()` | `rewrites()` | There are `beforeFiles` / `afterFiles` phases; no `fallback` |
|
|
45
|
-
| `compress: true` | Automatic | brotli + gzip via `node:zlib` |
|
|
46
|
-
| `images.deviceSizes` | `images.widths` | Build-time webp generation ([08](./08-build.md)) |
|
|
47
|
-
| `NEXT_PUBLIC_*` | `clientEnv: [...]` | Which key is exposed is clear from the config, not from the name |
|
|
48
|
-
| `experimental.*` | — | None |
|
|
49
|
-
|
|
50
|
-
### Routing and rendering
|
|
51
|
-
|
|
52
|
-
| Next.js | JSkelet | Note |
|
|
53
|
-
| --- | --- | --- |
|
|
54
|
-
| `app/page.js` (file-based routing) | `app.get(...)` inside `routes/*.mjs` | The order is written explicitly ([03](./03-routing.md)) |
|
|
55
|
-
| `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express pattern syntax |
|
|
56
|
-
| `params`, `searchParams` | `ctx.params`, `ctx.query` | The controller's single argument |
|
|
57
|
-
| `layout.js` | `views/layout.jsk` + `hooks.layoutContext()` | A single layout; no nested layouts |
|
|
58
|
-
| Server component (RSC) | Controller + `.jsk` template + `views/components/**` | A function returns an HTML string |
|
|
59
|
-
| Client component (`"use client"`) | Island (`data-island` + `mount`) | The whole page is not hydrated ([05](./05-islands.md)) |
|
|
60
|
-
| `notFound()` | `notFound()` | Same name, same control flow |
|
|
61
|
-
| `redirect()` | `redirect()` (307) | For permanent, `permanentRedirect()` (308) |
|
|
62
|
-
| `not-found.js` | `hooks.notFound()` | Returns a page definition |
|
|
63
|
-
| `error.js` | Express error handler | The framework returns minimal HTML for a 500 |
|
|
64
|
-
| `loading.js` / Suspense | — | The server HTML is complete; no skeleton needed |
|
|
65
|
-
| Streaming SSR | — | The response is a single chunk |
|
|
66
|
-
| `generateMetadata()` | Controller `metadata` + `hooks.metadata()` | Same field names ([04](./04-rendering.md)) |
|
|
67
|
-
| `opengraph-image.tsx` / `ImageResponse` | `ogHandler` + `ImageResponse` / `sendOgImage` | SVG or card fields → PNG (`sharp`); [04](./04-rendering.md) |
|
|
68
|
-
| `generateStaticParams()` | `hooks.prewarmPaths()` | Warming at startup time, not build time |
|
|
69
|
-
| Route Handlers (`route.js`) | A plain Express handler | `app.get/post(...)` |
|
|
70
|
-
| Middleware (`middleware.ts`) | Express middleware + config `rewrites`/`headers`/`redirects` | `app.use(...)` |
|
|
71
|
-
|
|
72
|
-
### Data and cache
|
|
73
|
-
|
|
74
|
-
| Next.js | JSkelet | Note |
|
|
75
|
-
| --- | --- | --- |
|
|
76
|
-
| `export const revalidate = 60` | `route(controller, { revalidate: 60 })` | Or `cache().html` ([06](./06-caching.md)) |
|
|
77
|
-
| ISR (prerender written to disk) | In-memory TTL cache + stale-while-revalidate | Nothing is written to disk |
|
|
78
|
-
| `fetch(..., { next: { revalidate } })` | — | The cache is at page level |
|
|
79
|
-
| `unstable_cache` | — | No cross-request data cache; there is a page cache |
|
|
80
|
-
| React `cache()` | `cache()` | Same behavior: in-request memoization |
|
|
81
|
-
| `revalidatePath()` | `invalidateHtmlCache("/news/:slug")` | A path, a pattern or a regexp; stales rather than deletes by default |
|
|
82
|
-
| `revalidateTag()` | `clearDataCache("news:")` | No tags to declare: the dependency is observed during the render ([06](./06-caching.md)) |
|
|
83
|
-
| `cookies()`, `headers()` | `ctx.req.headers`, `ctx.req.cookies`* | Direct access to the Express object |
|
|
84
|
-
| `dynamic = "force-dynamic"` | Not passing `revalidate` | Which means the cache is off |
|
|
85
|
-
|
|
86
|
-
\* Express 5 does not parse cookies on its own; add `cookie-parser` or read the
|
|
87
|
-
header manually.
|
|
88
|
-
|
|
89
|
-
### Components and helpers
|
|
90
|
-
|
|
91
|
-
| Next.js | JSkelet | Note |
|
|
92
|
-
| --- | --- | --- |
|
|
93
|
-
| `next/link` | `link({ href, text })` — `jskelet/tags` | `title` automatic, `rel`/`target` automatic for external links |
|
|
94
|
-
| `next/link` prefetching | `navigation: { prefetch, prerender }` | Speculation Rules; no client runtime ([07](./07-configuration.md)) |
|
|
95
|
-
| `next/image` | `image({ src, alt, priority })` — `jskelet/tags` | `srcset` from the build manifest |
|
|
96
|
-
| `next/og` `ImageResponse` | `ImageResponse` / `ogHandler` — `jskelet` | No JSX; SVG or `title`/`description` card |
|
|
97
|
-
| `next/font/google` | `fonts: [{ family, weights }]` | Self-hosted woff2, committed |
|
|
98
|
-
| `@phosphor-icons/react` | `icon({ name, weight })` — `jskelet/tags` | Build-time SVG sprite |
|
|
99
|
-
| `react-dom` preconnect/preload | `preconnect: [...]` + `headHints()` | ([04](./04-rendering.md)) |
|
|
100
|
-
| `clsx` | `cx()` — `jskelet/html` | — |
|
|
101
|
-
| `cn()` (clsx + tailwind-merge) | `cn()` — `jskelet/html` | Same behavior |
|
|
102
|
-
| JSX automatic escaping | `esc()` — `jskelet/html` | **You have to call it yourself** |
|
|
103
|
-
| React Context | `createStore()` — `jskelet/client` | Minimal pub/sub |
|
|
104
|
-
| `useState` / `useEffect` | Plain JS inside the island's `mount()` | — |
|
|
105
|
-
| `useSyncExternalStore` | `store.subscribe()` | — |
|
|
106
|
-
| `<Script>` | A `<script>` in the layout, or an island | — |
|
|
107
|
-
|
|
108
|
-
### What has no equivalent
|
|
109
|
-
|
|
110
|
-
Account for these from the start in your migration plan:
|
|
111
|
-
|
|
112
|
-
- **React itself.** Components turn into functions that return HTML strings. No
|
|
113
|
-
JSX, no hooks, no virtual DOM.
|
|
114
|
-
- **TypeScript.** Framework source is plain JS + JSDoc and publishes `.d.ts` for
|
|
115
|
-
consumers. Client entries and islands may be `.ts` / `.mts` (esbuild strips
|
|
116
|
-
types; the manifest key stays `*.js`). Server routes, hooks and
|
|
117
|
-
`jskelet.config.mjs` remain Node ESM JavaScript — use `checkJs: true` in
|
|
118
|
-
`jsconfig.json` for editor checking there.
|
|
119
|
-
- **Nested layouts.** There is a single layout; you share common sections with
|
|
120
|
-
EJS `include` or component functions.
|
|
121
|
-
- **Streaming / Suspense / partial prerendering.** The response is produced as a
|
|
122
|
-
single chunk.
|
|
123
|
-
- **Client-side routing.** Navigation is a real page load. Because the server
|
|
124
|
-
HTML comes from the cache it is very fast in practice, but there are no SPA
|
|
125
|
-
transitions. What closes the gap is the `navigation` section:
|
|
126
|
-
prefetch/prerender prepares the document before the click, and
|
|
127
|
-
`viewTransition` smooths the transition
|
|
128
|
-
([07](./07-configuration.md)).
|
|
129
|
-
- **Server Actions.** Form submissions are ordinary `app.post(...)` handlers.
|
|
130
|
-
- **Automatic image optimization (at request time).** Local `public/` images
|
|
131
|
-
are still optimized at build time. Remote `http(s)` images can be proxied at
|
|
132
|
-
runtime when `images.remote.allowHosts` is set (`/_jskelet/image` → webp);
|
|
133
|
-
without that config they are emitted as-is.
|
|
134
|
-
|
|
135
|
-
## A side-by-side example
|
|
136
|
-
|
|
137
|
-
**Next.js (App Router):**
|
|
138
|
-
|
|
139
|
-
```jsx
|
|
140
|
-
// app/news/[slug]/page.jsx
|
|
141
|
-
import { notFound } from "next/navigation";
|
|
142
|
-
import Image from "next/image";
|
|
143
|
-
import { getArticle } from "@/lib/api";
|
|
144
|
-
|
|
145
|
-
export const revalidate = 300;
|
|
146
|
-
|
|
147
|
-
export async function generateMetadata({ params }) {
|
|
148
|
-
const article = await getArticle(params.slug);
|
|
149
|
-
return {
|
|
150
|
-
title: article?.title,
|
|
151
|
-
description: article?.summary,
|
|
152
|
-
alternates: { canonical: `/news/${params.slug}` },
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export default async function Page({ params }) {
|
|
157
|
-
const article = await getArticle(params.slug);
|
|
158
|
-
if (!article) notFound();
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<article className="wrapper">
|
|
162
|
-
<h1 className="text-3xl font-bold">{article.title}</h1>
|
|
163
|
-
<Image src={article.cover} alt={article.title} priority width={1200} height={630} />
|
|
164
|
-
<div dangerouslySetInnerHTML={{ __html: article.body }} />
|
|
165
|
-
</article>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**JSkelet:**
|
|
171
|
-
|
|
172
|
-
```js
|
|
173
|
-
// routes/50-news.mjs
|
|
174
|
-
import { getArticle } from "@/lib/api.js";
|
|
175
|
-
|
|
176
|
-
export default function register(app, { route, notFound }) {
|
|
177
|
-
app.get(
|
|
178
|
-
"/news/:slug",
|
|
179
|
-
route(
|
|
180
|
-
async ({ params }) => {
|
|
181
|
-
const article = await getArticle(params.slug);
|
|
182
|
-
if (!article) notFound();
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
view: "pages/article",
|
|
186
|
-
data: { article },
|
|
187
|
-
metadata: {
|
|
188
|
-
title: article.title,
|
|
189
|
-
description: article.summary,
|
|
190
|
-
canonical: `/news/${params.slug}`,
|
|
191
|
-
openGraph: { type: "article", image: article.cover },
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
},
|
|
195
|
-
{ revalidate: 300 },
|
|
196
|
-
),
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
```jsk
|
|
202
|
-
{# views/pages/article.jsk #}
|
|
203
|
-
<article class="wrapper">
|
|
204
|
-
<h1 class="text-3xl font-bold">{{ article.title }}</h1>
|
|
205
|
-
<Image :src="article.cover" :alt="article.title" priority :width="1200" :height="630" />
|
|
206
|
-
<div>{{{ article.body }}}</div>
|
|
207
|
-
</article>
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Wrapping `getArticle` with `cache()` makes sure that if
|
|
211
|
-
`hooks.layoutContext()` asks for the same article in the same render, only a
|
|
212
|
-
single upstream request is made ([06-caching.md](./06-caching.md)).
|
|
213
|
-
|
|
214
|
-
## Step-by-step plan
|
|
215
|
-
|
|
216
|
-
### 1. Set up the skeleton (half a day)
|
|
217
|
-
|
|
218
|
-
Run `npx jskelet init` in a new directory and watch `jskelet dev` come up. Leave
|
|
219
|
-
the existing Next project as it is; let the migration run in parallel. Optionally
|
|
220
|
-
run `jskelet migrate scan <next-root>` first to list pages and blockers.
|
|
221
|
-
|
|
222
|
-
Carry over the `paths` aliases from your `jsconfig.json` — prefixes like `@/`
|
|
223
|
-
work the same way both on the server and in the bundle
|
|
224
|
-
([02-architecture.md](./02-architecture.md)).
|
|
225
|
-
|
|
226
|
-
### 2. Translate `next.config.mjs` (1-2 hours)
|
|
227
|
-
|
|
228
|
-
`jskelet migrate config <next-root> --write` drafts most of this. Then review:
|
|
229
|
-
|
|
230
|
-
The `headers()`, `redirects()` and `rewrites()` sections are copied almost
|
|
231
|
-
verbatim. Check the pattern syntax: JSkelet supports the `:slug`, `:path*`,
|
|
232
|
-
`/a-:b` and `/:path*.svg` forms; more complex `path-to-regexp` expressions are
|
|
233
|
-
not supported and produce a warning ([07-configuration.md](./07-configuration.md)).
|
|
234
|
-
|
|
235
|
-
Move your `NEXT_PUBLIC_*` variables into the `clientEnv` list and simplify their
|
|
236
|
-
names (the prefix no longer carries meaning).
|
|
237
|
-
|
|
238
|
-
### 3. Move the data layer (the easiest step)
|
|
239
|
-
|
|
240
|
-
The API client and data functions under `lib/` usually do not depend on React;
|
|
241
|
-
they are copied as-is. Make two changes:
|
|
242
|
-
|
|
243
|
-
- Use `import { cache } from "jskelet"` instead of React's `cache()`.
|
|
244
|
-
- Call `reportUpstreamFailure({ status, path })` on failed upstream responses.
|
|
245
|
-
This prevents pages produced with missing data from being written to the cache
|
|
246
|
-
([06-caching.md](./06-caching.md)).
|
|
247
|
-
|
|
248
|
-
### 4. Set up the layout (half a day)
|
|
249
|
-
|
|
250
|
-
Translate `app/layout.jsx` into `views/layout.jsk` (or let `migrate apply` draft
|
|
251
|
-
it). Copying the framework's default layout (`jskelet/layout` → `.jsk`) and
|
|
252
|
-
editing it is the fastest path.
|
|
253
|
-
|
|
254
|
-
If you fetch data inside `layout.jsx` (navigation, site settings), move it into
|
|
255
|
-
`hooks.layoutContext()`: it runs in parallel with the body render, and every
|
|
256
|
-
field it returns becomes a layout local.
|
|
257
|
-
|
|
258
|
-
Put your global metadata defaults (`titleTemplate`, `siteUrl`, `description`)
|
|
259
|
-
into `hooks.metadata()`.
|
|
260
|
-
|
|
261
|
-
### 5. Translate the components (the longest step)
|
|
262
|
-
|
|
263
|
-
`jskelet migrate apply --only components --write` converts presentational
|
|
264
|
-
components that are props + JSX with no hooks. Everything else you finish by
|
|
265
|
-
hand:
|
|
266
|
-
|
|
267
|
-
Every React component turns into a function:
|
|
268
|
-
|
|
269
|
-
```jsx
|
|
270
|
-
// Before
|
|
271
|
-
export function Badge({ label, tone = "neutral", className }) {
|
|
272
|
-
return <span className={cn("rounded px-2 py-1", TONES[tone], className)}>{label}</span>;
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
```js
|
|
277
|
-
// After — views/components/badge.js
|
|
278
|
-
import { attrs, cn, esc } from "jskelet/html";
|
|
279
|
-
|
|
280
|
-
export function badge({ label, tone = "neutral", class: className }) {
|
|
281
|
-
return `<span${attrs({ class: cn("rounded px-2 py-1", TONES[tone], className) })}>${esc(label)}</span>`;
|
|
282
|
-
}
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Things to watch out for:
|
|
286
|
-
|
|
287
|
-
- **Escaping is now on you.** JSX escaped automatically; here you must call
|
|
288
|
-
`esc()` when printing external data.
|
|
289
|
-
- **`className` → `class`.** Since `class` is a reserved word in JS, rename it in
|
|
290
|
-
the props as `class: className`.
|
|
291
|
-
- **An `html` field instead of children.** Nested content is passed as a string.
|
|
292
|
-
- Every function you place under `views/components/**` as a named export can be
|
|
293
|
-
used in templates without importing it
|
|
294
|
-
([04-rendering.md](./04-rendering.md)).
|
|
295
|
-
|
|
296
|
-
Keep components small and pure; leave data fetching in the controller.
|
|
297
|
-
|
|
298
|
-
### 6. Migrate the pages (hours per page)
|
|
299
|
-
|
|
300
|
-
`jskelet migrate apply --only pages --write` splits each `page.*` into a
|
|
301
|
-
feature controller plus a `.jsk` template. Review `partial` / `skipped` rows,
|
|
302
|
-
then finish the TODO markers. File them with the order in mind:
|
|
303
|
-
|
|
304
|
-
```
|
|
305
|
-
routes/
|
|
306
|
-
├── 00-health.mjs health check
|
|
307
|
-
├── 10-pages.mjs static paths: /, /about
|
|
308
|
-
├── 50-news.mjs /news/:slug
|
|
309
|
-
└── 99-catch-all.mjs /:slug (if any, last of all)
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
The places where you used `generateStaticParams()` turn into
|
|
313
|
-
`hooks.prewarmPaths()`. If you have a function that produces the sitemap, use
|
|
314
|
-
the same one.
|
|
315
|
-
|
|
316
|
-
Move your `export const revalidate` values either into `route()`'s second
|
|
317
|
-
argument or, to manage them from a single place, into `cache().html` patterns.
|
|
318
|
-
|
|
319
|
-
### 7. Turn client components into islands (hours per page)
|
|
320
|
-
|
|
321
|
-
Every `"use client"` component becomes an island. The process:
|
|
322
|
-
|
|
323
|
-
1. Move the component's **static** output into the server template. Everything
|
|
324
|
-
visible on first render must be in the HTML.
|
|
325
|
-
2. Write the remaining behavior inside `mount(element, props)`: `useState`
|
|
326
|
-
becomes a local variable, `useEffect` a direct call, event handlers
|
|
327
|
-
`on()`/`onClick()`.
|
|
328
|
-
3. Pass props as JSON with `data-island-props`.
|
|
329
|
-
4. Add it to the `registerAll()` map in the entry.
|
|
330
|
-
5. Choose the binding strategy: the default (visibility), `data-island-eager`
|
|
331
|
-
(global behavior) or `data-island-idle` (heavy and non-critical).
|
|
332
|
-
|
|
333
|
-
For components using Context, `createStore()` is the closest equivalent
|
|
334
|
-
([05-islands.md](./05-islands.md)).
|
|
335
|
-
|
|
336
|
-
**This is where the biggest win of this step lies:** the hydrated area is not
|
|
337
|
-
the whole page, only the parts that are genuinely interactive.
|
|
338
|
-
|
|
339
|
-
### 8. Move the CSS (1-2 hours)
|
|
340
|
-
|
|
341
|
-
If your Tailwind configuration is already in v4 format, `styles/globals.css`
|
|
342
|
-
stays almost the same. The one critical addition is the `@source` directives:
|
|
343
|
-
|
|
344
|
-
```css
|
|
345
|
-
@import "tailwindcss" source(none);
|
|
346
|
-
|
|
347
|
-
@source "../views";
|
|
348
|
-
@source "../client";
|
|
349
|
-
@source "../routes";
|
|
350
|
-
@source "../lib";
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Without these, the classes used in templates (especially variants like
|
|
354
|
-
`data-[state=open]:…`) are silently dropped
|
|
355
|
-
([08-build.md](./08-build.md)).
|
|
356
|
-
|
|
357
|
-
If you use `next/font`, add `fonts: [{ family, weights }]` and write the
|
|
358
|
-
`@font-face` blocks by hand; the generated files sit under `public/fonts/` with
|
|
359
|
-
stable names.
|
|
360
|
-
|
|
361
|
-
### 9. Verify and measure
|
|
362
|
-
|
|
363
|
-
- Browse with `jskelet dev` and confirm there are no errors in the dev overlay.
|
|
364
|
-
- On the report page (`/__jskelet/dev/report`), look at each page's Web Vitals
|
|
365
|
-
measurements, SSR size and island status ([09-dev-tools.md](./09-dev-tools.md)).
|
|
366
|
-
- Clear the missing-icon warnings.
|
|
367
|
-
- Compare the sizes in the `jskelet build` output with your old Next bundle.
|
|
368
|
-
- Check that the `X-JSkelet-Cache` header returns `HIT` on the pages you expect.
|
|
369
|
-
|
|
370
|
-
### 10. Go live
|
|
371
|
-
|
|
372
|
-
Go through the checklist in [10-deployment.md](./10-deployment.md). Keeping the
|
|
373
|
-
old Next setup alongside for a while and shifting traffic gradually is useful,
|
|
374
|
-
especially for verifying that the redirect rules are correct.
|
|
375
|
-
|
|
376
|
-
## Common mistakes during migration
|
|
377
|
-
|
|
378
|
-
- **Forgetting `esc()`.** Writing `${value}` out of JSX habit means XSS. In
|
|
379
|
-
`.jsk` templates use `{{ }}` (escaped) vs `{{{ }}}` (raw); in components call
|
|
380
|
-
`esc()` yourself.
|
|
381
|
-
- **Opening a new directory without adding `@source`.** The classes are silently
|
|
382
|
-
dropped.
|
|
383
|
-
- **Putting the catch-all route in the wrong order.** `/:slug` always goes last.
|
|
384
|
-
- **Making the whole page an island.** The win comes from the server HTML being
|
|
385
|
-
complete; bind the island only to the genuinely interactive part.
|
|
386
|
-
- **Forgetting to pass `revalidate`.** The cache stays off, every request is
|
|
387
|
-
rendered, and `X-JSkelet-Cache: MISS` is returned.
|
|
388
|
-
- **Not calling `reportUpstreamFailure()`.** When the upstream goes down, the
|
|
389
|
-
page produced with missing data is served for the entire TTL.
|
|
390
|
-
- **Putting a secret key in `clientEnv`.** The values sit in the bundle as plain
|
|
391
|
-
text.
|
|
392
|
-
|
|
393
|
-
## What's next
|
|
394
|
-
|
|
395
|
-
- The reasons behind the architectural decisions:
|
|
396
|
-
[02-architecture.md](./02-architecture.md)
|
|
397
|
-
- Details of the island model: [05-islands.md](./05-islands.md)
|
|
398
|
-
- Configuration reference: [07-configuration.md](./07-configuration.md)
|
|
1
|
+
# 11 — Migrating from Next.js
|
|
2
|
+
|
|
3
|
+
This document explains how to move a project using the Next.js App Router over
|
|
4
|
+
to JSkelet: a table of concept and API equivalents, an explicit list of what
|
|
5
|
+
cannot be migrated, and a step-by-step plan. JSkelet's surface was deliberately
|
|
6
|
+
modeled on the subset of Next that people actually use — concepts like the
|
|
7
|
+
`next.config` syntax, the Metadata API, `notFound()`, `revalidate` and `cache()`
|
|
8
|
+
will feel familiar. The *reasons* behind the differences are in
|
|
9
|
+
[02-architecture.md](./02-architecture.md).
|
|
10
|
+
|
|
11
|
+
## `jskelet migrate` (codemod)
|
|
12
|
+
|
|
13
|
+
Run the codemod against an App Router tree. Babel (`@babel/parser`,
|
|
14
|
+
`@babel/types`) ships with JSkelet — no extra install.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx jskelet migrate scan ../my-next-app
|
|
18
|
+
npx jskelet migrate apply ../my-next-app --out . --write
|
|
19
|
+
npx jskelet migrate config ../my-next-app --write
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Command | What it does |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `migrate` / `migrate scan` | Inventory pages, layouts, `"use client"` modules, blockers (nested layouts, Server Actions, Suspense). |
|
|
25
|
+
| `migrate apply` | **Automatic convert:** `page.*` → feature controller + `.jsk`; presentational components → `views/components/*.js`; clients → island `mount()` stubs. Default is dry-run; pass `--write`. Never overwrites (conflicts get a `.migrate` suffix). |
|
|
26
|
+
| `migrate config` | Draft `jskelet.config.mjs` from `next.config` (`headers` / `redirects` / `rewrites`, `images.widths`, `NEXT_PUBLIC_*` → `clientEnv`). |
|
|
27
|
+
|
|
28
|
+
Flags: `--out <dir>`, `--only pages,components,islands`, `--json`, `--strict` (exit 1 on partial/skipped).
|
|
29
|
+
|
|
30
|
+
**Converted automatically:** `className`, `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}`, `next/image` → `<Image />`, `next/link` → `<Link />`, `dangerouslySetInnerHTML`, `revalidate`, simple controller prelude (`await` data + `notFound()`).
|
|
31
|
+
|
|
32
|
+
**Not converted (reported):** React hooks, Server Actions, nested layout flattening, Streaming/Suspense, client-side routing. Confidence per file is `ok` / `partial` / `skipped`.
|
|
33
|
+
|
|
34
|
+
## Equivalence table
|
|
35
|
+
|
|
36
|
+
### Configuration
|
|
37
|
+
|
|
38
|
+
| Next.js | JSkelet | Note |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `next.config.mjs` | `jskelet.config.mjs` | Same spirit, smaller surface ([07](./07-configuration.md)) |
|
|
41
|
+
| `headers()` | `headers()` | Same shape: `{ source, headers: [{ key, value }] }` |
|
|
42
|
+
| `redirects()` | `redirects()` | `permanent` → 308, otherwise 307; can be overridden with `statusCode` |
|
|
43
|
+
| `trailingSlash` | `trailingSlash` | `true` → canonical URLs end with `/` (308); default `false` does not strip ([07](./07-configuration.md)) |
|
|
44
|
+
| `rewrites()` | `rewrites()` | There are `beforeFiles` / `afterFiles` phases; no `fallback` |
|
|
45
|
+
| `compress: true` | Automatic | brotli + gzip via `node:zlib` |
|
|
46
|
+
| `images.deviceSizes` | `images.widths` | Build-time webp generation ([08](./08-build.md)) |
|
|
47
|
+
| `NEXT_PUBLIC_*` | `clientEnv: [...]` | Which key is exposed is clear from the config, not from the name |
|
|
48
|
+
| `experimental.*` | — | None |
|
|
49
|
+
|
|
50
|
+
### Routing and rendering
|
|
51
|
+
|
|
52
|
+
| Next.js | JSkelet | Note |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `app/page.js` (file-based routing) | `app.get(...)` inside `routes/*.mjs` | The order is written explicitly ([03](./03-routing.md)) |
|
|
55
|
+
| `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express pattern syntax |
|
|
56
|
+
| `params`, `searchParams` | `ctx.params`, `ctx.query` | The controller's single argument |
|
|
57
|
+
| `layout.js` | `views/layout.jsk` + `hooks.layoutContext()` | A single layout; no nested layouts |
|
|
58
|
+
| Server component (RSC) | Controller + `.jsk` template + `views/components/**` | A function returns an HTML string |
|
|
59
|
+
| Client component (`"use client"`) | Island (`data-island` + `mount`) | The whole page is not hydrated ([05](./05-islands.md)) |
|
|
60
|
+
| `notFound()` | `notFound()` | Same name, same control flow |
|
|
61
|
+
| `redirect()` | `redirect()` (307) | For permanent, `permanentRedirect()` (308) |
|
|
62
|
+
| `not-found.js` | `hooks.notFound()` | Returns a page definition |
|
|
63
|
+
| `error.js` | Express error handler | The framework returns minimal HTML for a 500 |
|
|
64
|
+
| `loading.js` / Suspense | — | The server HTML is complete; no skeleton needed |
|
|
65
|
+
| Streaming SSR | — | The response is a single chunk |
|
|
66
|
+
| `generateMetadata()` | Controller `metadata` + `hooks.metadata()` | Same field names ([04](./04-rendering.md)) |
|
|
67
|
+
| `opengraph-image.tsx` / `ImageResponse` | `ogHandler` + `ImageResponse` / `sendOgImage` | SVG or card fields → PNG (`sharp`); [04](./04-rendering.md) |
|
|
68
|
+
| `generateStaticParams()` | `hooks.prewarmPaths()` | Warming at startup time, not build time |
|
|
69
|
+
| Route Handlers (`route.js`) | A plain Express handler | `app.get/post(...)` |
|
|
70
|
+
| Middleware (`middleware.ts`) | Express middleware + config `rewrites`/`headers`/`redirects` | `app.use(...)` |
|
|
71
|
+
|
|
72
|
+
### Data and cache
|
|
73
|
+
|
|
74
|
+
| Next.js | JSkelet | Note |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `export const revalidate = 60` | `route(controller, { revalidate: 60 })` | Or `cache().html` ([06](./06-caching.md)) |
|
|
77
|
+
| ISR (prerender written to disk) | In-memory TTL cache + stale-while-revalidate | Nothing is written to disk |
|
|
78
|
+
| `fetch(..., { next: { revalidate } })` | — | The cache is at page level |
|
|
79
|
+
| `unstable_cache` | — | No cross-request data cache; there is a page cache |
|
|
80
|
+
| React `cache()` | `cache()` | Same behavior: in-request memoization |
|
|
81
|
+
| `revalidatePath()` | `invalidateHtmlCache("/news/:slug")` | A path, a pattern or a regexp; stales rather than deletes by default |
|
|
82
|
+
| `revalidateTag()` | `clearDataCache("news:")` | No tags to declare: the dependency is observed during the render ([06](./06-caching.md)) |
|
|
83
|
+
| `cookies()`, `headers()` | `ctx.req.headers`, `ctx.req.cookies`* | Direct access to the Express object |
|
|
84
|
+
| `dynamic = "force-dynamic"` | Not passing `revalidate` | Which means the cache is off |
|
|
85
|
+
|
|
86
|
+
\* Express 5 does not parse cookies on its own; add `cookie-parser` or read the
|
|
87
|
+
header manually.
|
|
88
|
+
|
|
89
|
+
### Components and helpers
|
|
90
|
+
|
|
91
|
+
| Next.js | JSkelet | Note |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `next/link` | `link({ href, text })` — `jskelet/tags` | `title` automatic, `rel`/`target` automatic for external links |
|
|
94
|
+
| `next/link` prefetching | `navigation: { prefetch, prerender }` | Speculation Rules; no client runtime ([07](./07-configuration.md)) |
|
|
95
|
+
| `next/image` | `image({ src, alt, priority })` — `jskelet/tags` | `srcset` from the build manifest |
|
|
96
|
+
| `next/og` `ImageResponse` | `ImageResponse` / `ogHandler` — `jskelet` | No JSX; SVG or `title`/`description` card |
|
|
97
|
+
| `next/font/google` | `fonts: [{ family, weights }]` | Self-hosted woff2, committed |
|
|
98
|
+
| `@phosphor-icons/react` | `icon({ name, weight })` — `jskelet/tags` | Build-time SVG sprite |
|
|
99
|
+
| `react-dom` preconnect/preload | `preconnect: [...]` + `headHints()` | ([04](./04-rendering.md)) |
|
|
100
|
+
| `clsx` | `cx()` — `jskelet/html` | — |
|
|
101
|
+
| `cn()` (clsx + tailwind-merge) | `cn()` — `jskelet/html` | Same behavior |
|
|
102
|
+
| JSX automatic escaping | `esc()` — `jskelet/html` | **You have to call it yourself** |
|
|
103
|
+
| React Context | `createStore()` — `jskelet/client` | Minimal pub/sub |
|
|
104
|
+
| `useState` / `useEffect` | Plain JS inside the island's `mount()` | — |
|
|
105
|
+
| `useSyncExternalStore` | `store.subscribe()` | — |
|
|
106
|
+
| `<Script>` | A `<script>` in the layout, or an island | — |
|
|
107
|
+
|
|
108
|
+
### What has no equivalent
|
|
109
|
+
|
|
110
|
+
Account for these from the start in your migration plan:
|
|
111
|
+
|
|
112
|
+
- **React itself.** Components turn into functions that return HTML strings. No
|
|
113
|
+
JSX, no hooks, no virtual DOM.
|
|
114
|
+
- **TypeScript.** Framework source is plain JS + JSDoc and publishes `.d.ts` for
|
|
115
|
+
consumers. Client entries and islands may be `.ts` / `.mts` (esbuild strips
|
|
116
|
+
types; the manifest key stays `*.js`). Server routes, hooks and
|
|
117
|
+
`jskelet.config.mjs` remain Node ESM JavaScript — use `checkJs: true` in
|
|
118
|
+
`jsconfig.json` for editor checking there.
|
|
119
|
+
- **Nested layouts.** There is a single layout; you share common sections with
|
|
120
|
+
EJS `include` or component functions.
|
|
121
|
+
- **Streaming / Suspense / partial prerendering.** The response is produced as a
|
|
122
|
+
single chunk.
|
|
123
|
+
- **Client-side routing.** Navigation is a real page load. Because the server
|
|
124
|
+
HTML comes from the cache it is very fast in practice, but there are no SPA
|
|
125
|
+
transitions. What closes the gap is the `navigation` section:
|
|
126
|
+
prefetch/prerender prepares the document before the click, and
|
|
127
|
+
`viewTransition` smooths the transition
|
|
128
|
+
([07](./07-configuration.md)).
|
|
129
|
+
- **Server Actions.** Form submissions are ordinary `app.post(...)` handlers.
|
|
130
|
+
- **Automatic image optimization (at request time).** Local `public/` images
|
|
131
|
+
are still optimized at build time. Remote `http(s)` images can be proxied at
|
|
132
|
+
runtime when `images.remote.allowHosts` is set (`/_jskelet/image` → webp);
|
|
133
|
+
without that config they are emitted as-is.
|
|
134
|
+
|
|
135
|
+
## A side-by-side example
|
|
136
|
+
|
|
137
|
+
**Next.js (App Router):**
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
// app/news/[slug]/page.jsx
|
|
141
|
+
import { notFound } from "next/navigation";
|
|
142
|
+
import Image from "next/image";
|
|
143
|
+
import { getArticle } from "@/lib/api";
|
|
144
|
+
|
|
145
|
+
export const revalidate = 300;
|
|
146
|
+
|
|
147
|
+
export async function generateMetadata({ params }) {
|
|
148
|
+
const article = await getArticle(params.slug);
|
|
149
|
+
return {
|
|
150
|
+
title: article?.title,
|
|
151
|
+
description: article?.summary,
|
|
152
|
+
alternates: { canonical: `/news/${params.slug}` },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default async function Page({ params }) {
|
|
157
|
+
const article = await getArticle(params.slug);
|
|
158
|
+
if (!article) notFound();
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<article className="wrapper">
|
|
162
|
+
<h1 className="text-3xl font-bold">{article.title}</h1>
|
|
163
|
+
<Image src={article.cover} alt={article.title} priority width={1200} height={630} />
|
|
164
|
+
<div dangerouslySetInnerHTML={{ __html: article.body }} />
|
|
165
|
+
</article>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**JSkelet:**
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
// routes/50-news.mjs
|
|
174
|
+
import { getArticle } from "@/lib/api.js";
|
|
175
|
+
|
|
176
|
+
export default function register(app, { route, notFound }) {
|
|
177
|
+
app.get(
|
|
178
|
+
"/news/:slug",
|
|
179
|
+
route(
|
|
180
|
+
async ({ params }) => {
|
|
181
|
+
const article = await getArticle(params.slug);
|
|
182
|
+
if (!article) notFound();
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
view: "pages/article",
|
|
186
|
+
data: { article },
|
|
187
|
+
metadata: {
|
|
188
|
+
title: article.title,
|
|
189
|
+
description: article.summary,
|
|
190
|
+
canonical: `/news/${params.slug}`,
|
|
191
|
+
openGraph: { type: "article", image: article.cover },
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
{ revalidate: 300 },
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```jsk
|
|
202
|
+
{# views/pages/article.jsk #}
|
|
203
|
+
<article class="wrapper">
|
|
204
|
+
<h1 class="text-3xl font-bold">{{ article.title }}</h1>
|
|
205
|
+
<Image :src="article.cover" :alt="article.title" priority :width="1200" :height="630" />
|
|
206
|
+
<div>{{{ article.body }}}</div>
|
|
207
|
+
</article>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Wrapping `getArticle` with `cache()` makes sure that if
|
|
211
|
+
`hooks.layoutContext()` asks for the same article in the same render, only a
|
|
212
|
+
single upstream request is made ([06-caching.md](./06-caching.md)).
|
|
213
|
+
|
|
214
|
+
## Step-by-step plan
|
|
215
|
+
|
|
216
|
+
### 1. Set up the skeleton (half a day)
|
|
217
|
+
|
|
218
|
+
Run `npx jskelet init` in a new directory and watch `jskelet dev` come up. Leave
|
|
219
|
+
the existing Next project as it is; let the migration run in parallel. Optionally
|
|
220
|
+
run `jskelet migrate scan <next-root>` first to list pages and blockers.
|
|
221
|
+
|
|
222
|
+
Carry over the `paths` aliases from your `jsconfig.json` — prefixes like `@/`
|
|
223
|
+
work the same way both on the server and in the bundle
|
|
224
|
+
([02-architecture.md](./02-architecture.md)).
|
|
225
|
+
|
|
226
|
+
### 2. Translate `next.config.mjs` (1-2 hours)
|
|
227
|
+
|
|
228
|
+
`jskelet migrate config <next-root> --write` drafts most of this. Then review:
|
|
229
|
+
|
|
230
|
+
The `headers()`, `redirects()` and `rewrites()` sections are copied almost
|
|
231
|
+
verbatim. Check the pattern syntax: JSkelet supports the `:slug`, `:path*`,
|
|
232
|
+
`/a-:b` and `/:path*.svg` forms; more complex `path-to-regexp` expressions are
|
|
233
|
+
not supported and produce a warning ([07-configuration.md](./07-configuration.md)).
|
|
234
|
+
|
|
235
|
+
Move your `NEXT_PUBLIC_*` variables into the `clientEnv` list and simplify their
|
|
236
|
+
names (the prefix no longer carries meaning).
|
|
237
|
+
|
|
238
|
+
### 3. Move the data layer (the easiest step)
|
|
239
|
+
|
|
240
|
+
The API client and data functions under `lib/` usually do not depend on React;
|
|
241
|
+
they are copied as-is. Make two changes:
|
|
242
|
+
|
|
243
|
+
- Use `import { cache } from "jskelet"` instead of React's `cache()`.
|
|
244
|
+
- Call `reportUpstreamFailure({ status, path })` on failed upstream responses.
|
|
245
|
+
This prevents pages produced with missing data from being written to the cache
|
|
246
|
+
([06-caching.md](./06-caching.md)).
|
|
247
|
+
|
|
248
|
+
### 4. Set up the layout (half a day)
|
|
249
|
+
|
|
250
|
+
Translate `app/layout.jsx` into `views/layout.jsk` (or let `migrate apply` draft
|
|
251
|
+
it). Copying the framework's default layout (`jskelet/layout` → `.jsk`) and
|
|
252
|
+
editing it is the fastest path.
|
|
253
|
+
|
|
254
|
+
If you fetch data inside `layout.jsx` (navigation, site settings), move it into
|
|
255
|
+
`hooks.layoutContext()`: it runs in parallel with the body render, and every
|
|
256
|
+
field it returns becomes a layout local.
|
|
257
|
+
|
|
258
|
+
Put your global metadata defaults (`titleTemplate`, `siteUrl`, `description`)
|
|
259
|
+
into `hooks.metadata()`.
|
|
260
|
+
|
|
261
|
+
### 5. Translate the components (the longest step)
|
|
262
|
+
|
|
263
|
+
`jskelet migrate apply --only components --write` converts presentational
|
|
264
|
+
components that are props + JSX with no hooks. Everything else you finish by
|
|
265
|
+
hand:
|
|
266
|
+
|
|
267
|
+
Every React component turns into a function:
|
|
268
|
+
|
|
269
|
+
```jsx
|
|
270
|
+
// Before
|
|
271
|
+
export function Badge({ label, tone = "neutral", className }) {
|
|
272
|
+
return <span className={cn("rounded px-2 py-1", TONES[tone], className)}>{label}</span>;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
// After — views/components/badge.js
|
|
278
|
+
import { attrs, cn, esc } from "jskelet/html";
|
|
279
|
+
|
|
280
|
+
export function badge({ label, tone = "neutral", class: className }) {
|
|
281
|
+
return `<span${attrs({ class: cn("rounded px-2 py-1", TONES[tone], className) })}>${esc(label)}</span>`;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Things to watch out for:
|
|
286
|
+
|
|
287
|
+
- **Escaping is now on you.** JSX escaped automatically; here you must call
|
|
288
|
+
`esc()` when printing external data.
|
|
289
|
+
- **`className` → `class`.** Since `class` is a reserved word in JS, rename it in
|
|
290
|
+
the props as `class: className`.
|
|
291
|
+
- **An `html` field instead of children.** Nested content is passed as a string.
|
|
292
|
+
- Every function you place under `views/components/**` as a named export can be
|
|
293
|
+
used in templates without importing it
|
|
294
|
+
([04-rendering.md](./04-rendering.md)).
|
|
295
|
+
|
|
296
|
+
Keep components small and pure; leave data fetching in the controller.
|
|
297
|
+
|
|
298
|
+
### 6. Migrate the pages (hours per page)
|
|
299
|
+
|
|
300
|
+
`jskelet migrate apply --only pages --write` splits each `page.*` into a
|
|
301
|
+
feature controller plus a `.jsk` template. Review `partial` / `skipped` rows,
|
|
302
|
+
then finish the TODO markers. File them with the order in mind:
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
routes/
|
|
306
|
+
├── 00-health.mjs health check
|
|
307
|
+
├── 10-pages.mjs static paths: /, /about
|
|
308
|
+
├── 50-news.mjs /news/:slug
|
|
309
|
+
└── 99-catch-all.mjs /:slug (if any, last of all)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The places where you used `generateStaticParams()` turn into
|
|
313
|
+
`hooks.prewarmPaths()`. If you have a function that produces the sitemap, use
|
|
314
|
+
the same one.
|
|
315
|
+
|
|
316
|
+
Move your `export const revalidate` values either into `route()`'s second
|
|
317
|
+
argument or, to manage them from a single place, into `cache().html` patterns.
|
|
318
|
+
|
|
319
|
+
### 7. Turn client components into islands (hours per page)
|
|
320
|
+
|
|
321
|
+
Every `"use client"` component becomes an island. The process:
|
|
322
|
+
|
|
323
|
+
1. Move the component's **static** output into the server template. Everything
|
|
324
|
+
visible on first render must be in the HTML.
|
|
325
|
+
2. Write the remaining behavior inside `mount(element, props)`: `useState`
|
|
326
|
+
becomes a local variable, `useEffect` a direct call, event handlers
|
|
327
|
+
`on()`/`onClick()`.
|
|
328
|
+
3. Pass props as JSON with `data-island-props`.
|
|
329
|
+
4. Add it to the `registerAll()` map in the entry.
|
|
330
|
+
5. Choose the binding strategy: the default (visibility), `data-island-eager`
|
|
331
|
+
(global behavior) or `data-island-idle` (heavy and non-critical).
|
|
332
|
+
|
|
333
|
+
For components using Context, `createStore()` is the closest equivalent
|
|
334
|
+
([05-islands.md](./05-islands.md)).
|
|
335
|
+
|
|
336
|
+
**This is where the biggest win of this step lies:** the hydrated area is not
|
|
337
|
+
the whole page, only the parts that are genuinely interactive.
|
|
338
|
+
|
|
339
|
+
### 8. Move the CSS (1-2 hours)
|
|
340
|
+
|
|
341
|
+
If your Tailwind configuration is already in v4 format, `styles/globals.css`
|
|
342
|
+
stays almost the same. The one critical addition is the `@source` directives:
|
|
343
|
+
|
|
344
|
+
```css
|
|
345
|
+
@import "tailwindcss" source(none);
|
|
346
|
+
|
|
347
|
+
@source "../views";
|
|
348
|
+
@source "../client";
|
|
349
|
+
@source "../routes";
|
|
350
|
+
@source "../lib";
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Without these, the classes used in templates (especially variants like
|
|
354
|
+
`data-[state=open]:…`) are silently dropped
|
|
355
|
+
([08-build.md](./08-build.md)).
|
|
356
|
+
|
|
357
|
+
If you use `next/font`, add `fonts: [{ family, weights }]` and write the
|
|
358
|
+
`@font-face` blocks by hand; the generated files sit under `public/fonts/` with
|
|
359
|
+
stable names.
|
|
360
|
+
|
|
361
|
+
### 9. Verify and measure
|
|
362
|
+
|
|
363
|
+
- Browse with `jskelet dev` and confirm there are no errors in the dev overlay.
|
|
364
|
+
- On the report page (`/__jskelet/dev/report`), look at each page's Web Vitals
|
|
365
|
+
measurements, SSR size and island status ([09-dev-tools.md](./09-dev-tools.md)).
|
|
366
|
+
- Clear the missing-icon warnings.
|
|
367
|
+
- Compare the sizes in the `jskelet build` output with your old Next bundle.
|
|
368
|
+
- Check that the `X-JSkelet-Cache` header returns `HIT` on the pages you expect.
|
|
369
|
+
|
|
370
|
+
### 10. Go live
|
|
371
|
+
|
|
372
|
+
Go through the checklist in [10-deployment.md](./10-deployment.md). Keeping the
|
|
373
|
+
old Next setup alongside for a while and shifting traffic gradually is useful,
|
|
374
|
+
especially for verifying that the redirect rules are correct.
|
|
375
|
+
|
|
376
|
+
## Common mistakes during migration
|
|
377
|
+
|
|
378
|
+
- **Forgetting `esc()`.** Writing `${value}` out of JSX habit means XSS. In
|
|
379
|
+
`.jsk` templates use `{{ }}` (escaped) vs `{{{ }}}` (raw); in components call
|
|
380
|
+
`esc()` yourself.
|
|
381
|
+
- **Opening a new directory without adding `@source`.** The classes are silently
|
|
382
|
+
dropped.
|
|
383
|
+
- **Putting the catch-all route in the wrong order.** `/:slug` always goes last.
|
|
384
|
+
- **Making the whole page an island.** The win comes from the server HTML being
|
|
385
|
+
complete; bind the island only to the genuinely interactive part.
|
|
386
|
+
- **Forgetting to pass `revalidate`.** The cache stays off, every request is
|
|
387
|
+
rendered, and `X-JSkelet-Cache: MISS` is returned.
|
|
388
|
+
- **Not calling `reportUpstreamFailure()`.** When the upstream goes down, the
|
|
389
|
+
page produced with missing data is served for the entire TTL.
|
|
390
|
+
- **Putting a secret key in `clientEnv`.** The values sit in the bundle as plain
|
|
391
|
+
text.
|
|
392
|
+
|
|
393
|
+
## What's next
|
|
394
|
+
|
|
395
|
+
- The reasons behind the architectural decisions:
|
|
396
|
+
[02-architecture.md](./02-architecture.md)
|
|
397
|
+
- Details of the island model: [05-islands.md](./05-islands.md)
|
|
398
|
+
- Configuration reference: [07-configuration.md](./07-configuration.md)
|