@rsc-kit/mcp 0.15.0 → 0.16.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.
package/dist/report.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface ReportedRoute {
4
4
  type: string;
5
5
  reason: string | null;
6
6
  warning: string | null;
7
+ /** How it was stored, when that is worth knowing - "no client components, so ships no javascript". */
8
+ note?: string | null;
7
9
  clientJs: number | null;
8
10
  }
9
11
  export interface ReportedApiRoute {
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,mDAAmD;AAEnD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqCzC,mEAAmE;AACnE,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;AAEnD,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,YAAY,IAAY;QACtB,KAAK,CACH,yBAAyB,IAAI,iDAAiD;YAC5E,uEAAuE,CAC1E,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;IACxB,CAAC;CACF;AAED,qCAAqC;AACrC,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1B,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,mBAAmB,CAAC,CAAA;QAEjD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAQ;QAE/B,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAgB;YAC9D,0EAA0E;YAC1E,uEAAuE;YACvE,wDAAwD;YACxD,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK;YAC7B,IAAI,EAAE,IAAI;SACX,CAAA;IACH,CAAC;IAED,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC1B,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,MAAM,OAAO,GAA2B;IAC7C,MAAM,EAAE,iDAAiD;IACzD,KAAK,EAAE,oDAAoD;IAC3D,OAAO,EAAE,uFAAuF;IAChG,OAAO,EAAE,sBAAsB;IAC/B,KAAK,EAAE,kBAAkB;CAC1B,CAAA;AAED,8DAA8D;AAC9D,MAAM,UAAU,QAAQ,CAAC,MAAmB,EAAE,GAAW;IACvD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;IAC3E,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAE,CACpC,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;IAEjE,OAAO,CACL,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAC7F,CAAA;AACH,CAAC","sourcesContent":["// Reading what the build wrote down.\n//\n// Everything this server answers comes from one file: `build-report.json`, in\n// the build's own output directory. Nothing here runs a build, imports the\n// app, or re-derives the route tree — the build already decided all of this,\n// and a second implementation would be a second thing that can be wrong.\n//\n// The consequence to be honest about: an answer is only as fresh as the last\n// build. So every answer says when it was built, and a missing report says\n// \"run a build\" rather than \"there are no routes\".\n\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\n\nexport interface ReportedRoute {\n url: string\n component: string\n type: string\n reason: string | null\n warning: string | null\n clientJs: number | null\n}\n\nexport interface ReportedApiRoute {\n url: string\n name: string\n type: string\n reason: string | null\n}\n\nexport interface ReportedAction {\n id: string\n name: string\n file: string\n /** Built by createActionClient, so its middleware ran. */\n client: boolean\n /** A read (GET) rather than a mutation. */\n query: boolean\n}\n\nexport interface BuildReport {\n version: number\n routes: ReportedRoute[]\n apis: ReportedApiRoute[]\n /** Absent from reports written before actions were audited. */\n actions?: ReportedAction[]\n totals: { static: number; partial: number; dynamic: number; failed: number }\n}\n\n/** Where a build leaves its report, in the order worth looking. */\nconst LIKELY = ['.rsc', 'build', 'dist', '.output']\n\nexport class NoReport extends Error {\n constructor(root: string) {\n super(\n `No build report under ${root}. This server answers from what the last build ` +\n 'decided, so there has to have been one — run the build and ask again.',\n )\n this.name = 'NoReport'\n }\n}\n\n/** The report, and how old it is. */\nexport function loadReport(root: string): { report: BuildReport; builtAt: Date; from: string } {\n const base = resolve(root)\n\n for (const dir of LIKELY) {\n const file = join(base, dir, 'build-report.json')\n\n if (!existsSync(file)) continue\n\n return {\n report: JSON.parse(readFileSync(file, 'utf-8')) as BuildReport,\n // The file's own mtime rather than a timestamp inside it: a stamp written\n // into the file changes the file on every build even when nothing else\n // did, which defeats every cache keyed on its contents.\n builtAt: statSync(file).mtime,\n from: file,\n }\n }\n\n throw new NoReport(base)\n}\n\n/** What each classification means, in one line, for an answer that has to stand alone. */\nexport const MEANING: Record<string, string> = {\n frozen: 'stored whole at build time and served as a file',\n shell: 'a stored shell, with the rest rendered per request',\n blocked: 'REFUSED — nothing could paint before it read the request, so the build did not finish',\n dynamic: 'answered per request',\n error: 'failed to render',\n}\n\n/** A route by url, tolerating a trailing slash either way. */\nexport function routeFor(report: BuildReport, url: string): ReportedRoute | ReportedApiRoute | null {\n const wanted = (url.split('?')[0].replace(/\\/+$/, '') || '/').toLowerCase()\n const matches = (candidate: string) =>\n (candidate.replace(/\\/+$/, '') || '/').toLowerCase() === wanted\n\n return (\n report.routes.find((r) => matches(r.url)) ?? report.apis.find((a) => matches(a.url)) ?? null\n )\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,mDAAmD;AAEnD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAuCzC,mEAAmE;AACnE,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;AAEnD,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,YAAY,IAAY;QACtB,KAAK,CACH,yBAAyB,IAAI,iDAAiD;YAC5E,uEAAuE,CAC1E,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;IACxB,CAAC;CACF;AAED,qCAAqC;AACrC,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1B,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,mBAAmB,CAAC,CAAA;QAEjD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAQ;QAE/B,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAgB;YAC9D,0EAA0E;YAC1E,uEAAuE;YACvE,wDAAwD;YACxD,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK;YAC7B,IAAI,EAAE,IAAI;SACX,CAAA;IACH,CAAC;IAED,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC1B,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,MAAM,OAAO,GAA2B;IAC7C,MAAM,EAAE,iDAAiD;IACzD,KAAK,EAAE,oDAAoD;IAC3D,OAAO,EAAE,uFAAuF;IAChG,OAAO,EAAE,sBAAsB;IAC/B,KAAK,EAAE,kBAAkB;CAC1B,CAAA;AAED,8DAA8D;AAC9D,MAAM,UAAU,QAAQ,CAAC,MAAmB,EAAE,GAAW;IACvD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;IAC3E,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAE,CACpC,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;IAEjE,OAAO,CACL,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAC7F,CAAA;AACH,CAAC","sourcesContent":["// Reading what the build wrote down.\n//\n// Everything this server answers comes from one file: `build-report.json`, in\n// the build's own output directory. Nothing here runs a build, imports the\n// app, or re-derives the route tree — the build already decided all of this,\n// and a second implementation would be a second thing that can be wrong.\n//\n// The consequence to be honest about: an answer is only as fresh as the last\n// build. So every answer says when it was built, and a missing report says\n// \"run a build\" rather than \"there are no routes\".\n\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\n\nexport interface ReportedRoute {\n url: string\n component: string\n type: string\n reason: string | null\n warning: string | null\n /** How it was stored, when that is worth knowing - \"no client components, so ships no javascript\". */\n note?: string | null\n clientJs: number | null\n}\n\nexport interface ReportedApiRoute {\n url: string\n name: string\n type: string\n reason: string | null\n}\n\nexport interface ReportedAction {\n id: string\n name: string\n file: string\n /** Built by createActionClient, so its middleware ran. */\n client: boolean\n /** A read (GET) rather than a mutation. */\n query: boolean\n}\n\nexport interface BuildReport {\n version: number\n routes: ReportedRoute[]\n apis: ReportedApiRoute[]\n /** Absent from reports written before actions were audited. */\n actions?: ReportedAction[]\n totals: { static: number; partial: number; dynamic: number; failed: number }\n}\n\n/** Where a build leaves its report, in the order worth looking. */\nconst LIKELY = ['.rsc', 'build', 'dist', '.output']\n\nexport class NoReport extends Error {\n constructor(root: string) {\n super(\n `No build report under ${root}. This server answers from what the last build ` +\n 'decided, so there has to have been one — run the build and ask again.',\n )\n this.name = 'NoReport'\n }\n}\n\n/** The report, and how old it is. */\nexport function loadReport(root: string): { report: BuildReport; builtAt: Date; from: string } {\n const base = resolve(root)\n\n for (const dir of LIKELY) {\n const file = join(base, dir, 'build-report.json')\n\n if (!existsSync(file)) continue\n\n return {\n report: JSON.parse(readFileSync(file, 'utf-8')) as BuildReport,\n // The file's own mtime rather than a timestamp inside it: a stamp written\n // into the file changes the file on every build even when nothing else\n // did, which defeats every cache keyed on its contents.\n builtAt: statSync(file).mtime,\n from: file,\n }\n }\n\n throw new NoReport(base)\n}\n\n/** What each classification means, in one line, for an answer that has to stand alone. */\nexport const MEANING: Record<string, string> = {\n frozen: 'stored whole at build time and served as a file',\n shell: 'a stored shell, with the rest rendered per request',\n blocked: 'REFUSED — nothing could paint before it read the request, so the build did not finish',\n dynamic: 'answered per request',\n error: 'failed to render',\n}\n\n/** A route by url, tolerating a trailing slash either way. */\nexport function routeFor(report: BuildReport, url: string): ReportedRoute | ReportedApiRoute | null {\n const wanted = (url.split('?')[0].replace(/\\/+$/, '') || '/').toLowerCase()\n const matches = (candidate: string) =>\n (candidate.replace(/\\/+$/, '') || '/').toLowerCase() === wanted\n\n return (\n report.routes.find((r) => matches(r.url)) ?? report.apis.find((a) => matches(a.url)) ?? null\n )\n}\n"]}
@@ -0,0 +1,151 @@
1
+ # Coming from Next.js
2
+
3
+ > What carries over unchanged, what to rename, and what is different on purpose.
4
+
5
+ Most of a Next.js app directory moves over as it is. This page is the rest,
6
+ in the order a port meets it.
7
+
8
+ ## What is the same
9
+
10
+ The `app/` conventions are the same conventions: `layout.tsx`, `page.tsx`,
11
+ `loading.tsx`, `error.tsx`, `not-found.tsx`, `route.ts`, `[slug]`,
12
+ `[...path]`, `(group)`, `@slot` and `(.)intercept`. `"use client"` and
13
+ `"use server"` mean what they mean in React. `cookies()` and `headers()` are
14
+ async and have the same names. `redirect()`, `notFound()`,
15
+ `generateStaticParams()` and `export const metadata` all exist. Streaming
16
+ through `<Suspense>` works the same way, because it is React doing it.
17
+
18
+ Copy `src/app` across first and fix imports second. Most files need only the
19
+ imports.
20
+
21
+ ## Imports
22
+
23
+ | Next.js | here |
24
+ | --- | --- |
25
+ | `next/link` | `@rsc-kit/core/Link` — `href` is typed to your routes, and `search` to the page's schema |
26
+ | `useRouter().push(url)` | `visit(url)` from `@rsc-kit/core/router`; `replace: true` for `.replace()` |
27
+ | `useRouter().refresh()` | `refresh()` from `@rsc-kit/core/router` — or better, `revalidate()` from the action |
28
+ | `usePathname()` | `@rsc-kit/core/usePathname` |
29
+ | `useSearchParams()` | `@rsc-kit/core/useSearchParams`, or [nuqs](/guides/routing#search-params-as-state) with our adapter |
30
+ | `useParams()` | the page's `params` prop, passed down |
31
+ | `cookies()`, `headers()` from `next/headers` | the same names from `@rsc-kit/core/request` |
32
+ | `redirect()` from `next/navigation` | `@rsc-kit/core/redirect` |
33
+ | `notFound()` | `@rsc-kit/core/not-found` |
34
+ | `revalidatePath()`, `revalidateTag()` | `revalidate('tag')` from `@rsc-kit/core/revalidate` — see below, it is narrower |
35
+ | `Metadata` from `next` | `@rsc-kit/core/metadata` — `metadataBase`, `openGraph`, `twitter`, `icons` as you had them |
36
+ | `next/font` | [Fontsource](/guides/fonts): install the font, import its CSS |
37
+ | `next/image` | [unpic](/guides/images) for a CDN, `vite-imagetools` for files in the repo |
38
+ | `next/script` | [a `<script>` tag](/guides/third-party-scripts): React 19 hoists and dedupes `async` scripts itself |
39
+ | `NEXT_PUBLIC_*` | `VITE_*`, read through `import.meta.env`; everything else stays `process.env` on the server |
40
+ | `next.config.js` | `vite.config.ts` — Tailwind, aliases and plugins are Vite's |
41
+ | `next-safe-action` | `createActionClient()` — same shape, [below](#actions) |
42
+
43
+ ## Different on purpose
44
+
45
+ ### Nothing is dynamic by declaration
46
+
47
+ There is no `export const dynamic = 'force-dynamic'` and no `revalidate = 60`.
48
+ A page is frozen at build time unless it **reads the request** — `cookies()`,
49
+ `headers()`, `searchParams`, `await connection()` — and then it renders per
50
+ request, or as a shell with the reading part streamed in. The build prints
51
+ which, and why:
52
+
53
+ ```
54
+ ○ /about no js
55
+ ◐ /orders 85 kB
56
+ dynamic — called cookies()
57
+ ```
58
+
59
+ `await connection()` is the one explicit mark, for a page that must render per
60
+ visitor and does not happen to read anything. Time-based ISR does not exist:
61
+ a frozen page changes when you build, a dynamic one on every request, and
62
+ [edge caching](/guides/edge-caching) covers the middle.
63
+
64
+ ### Middleware is per directory, not one file at the edge
65
+
66
+ Next has one `middleware.ts` that runs on a matcher, on the edge runtime, with
67
+ a restricted API. Here a `middleware.ts` sits in the directory it protects,
68
+ runs on the server with the full API, and covers everything below it:
69
+
70
+ ```ts title="src/app/admin/middleware.ts"
71
+ export default async function middleware() {
72
+ if (!(await currentUser())?.isAdmin) redirect('/login');
73
+ }
74
+ ```
75
+
76
+ It does not run for actions — an action renders no route — which is why the
77
+ check for an action belongs in the action. See [Authorization](/guides/authorization).
78
+
79
+ ### Actions
80
+
81
+ `next-safe-action` users will find the same shape under a different name.
82
+ `createActionClient()` chains middleware, validates with any Standard Schema,
83
+ and **returns** failures as `{ validationErrors }` or `{ serverError }` rather
84
+ than throwing them across the wire:
85
+
86
+ ```ts
87
+ export const client = createActionClient().use(async ({ next }) => {
88
+ const user = await currentUser();
89
+ if (!user) throw new ServerAuthenticationError();
90
+ return next({ ctx: { user } });
91
+ });
92
+
93
+ export const createPost = client.input(schema).handler(async ({ input, ctx, fieldErrors }) => {
94
+ if (await slugTaken(input.slug)) return fieldErrors({ slug: 'Already taken' });
95
+ return save(input, ctx.user);
96
+ });
97
+ ```
98
+
99
+ `returnValidationErrors(schema, { email: { _errors: [...] } })` becomes
100
+ `return fieldErrors({ email: 'Account not found' })`. `useAction` from
101
+ next-safe-action is `useActionState`, or a `<Form action={createPost}>` that
102
+ reads the returned errors on its own. The build lists any action not built
103
+ from a client, because nothing checks who calls those.
104
+
105
+ ### Revalidation is targeted
106
+
107
+ `revalidatePath('/orders')` re-renders the page. `revalidate('orders')`
108
+ re-renders the [section](/guides/sections) registered under that name and
109
+ sends it back **with the action's own response** — one request, the rest of
110
+ the page untouched, a half-typed input elsewhere on it still typed. Wrap the
111
+ region in `section('orders', Orders)` and name it from the action.
112
+
113
+ ### Forms
114
+
115
+ `<Form>` from `@rsc-kit/core/form` submits to an action, shows pending state,
116
+ places field errors, and works before hydration. It is uncontrolled by
117
+ default like React Hook Form's `register`, with `field()` for a controlled
118
+ binding and `useField()` for a value read anywhere. shadcn's `Field`
119
+ components fit as they are. See [Forms](/guides/forms).
120
+
121
+ ### Query strings are typed
122
+
123
+ Export a schema beside the page and the values arrive parsed; the same schema
124
+ types every `<Link search={…}>` to it. `Number(searchParams.get('page'))` is
125
+ not a thing you write here. See [URL validation](/guides/url-validation).
126
+
127
+ ### There is no image optimizer, and no `opengraph-image.tsx`
128
+
129
+ Both are processes Next runs for you at request time. Put `opengraph-image.png`
130
+ in `src/app` and it is picked up; generate one at build time if it has to be
131
+ generated. Images: [unpic or imagetools](/guides/images).
132
+
133
+ ### Testing does not need a browser
134
+
135
+ `createTestApp()` hands back the deployed `Request → Response` handler.
136
+ Actions, queries and api routes are plain functions. There is no equivalent
137
+ in Next; see [Testing](/guides/testing).
138
+
139
+ ## The porting order that worked
140
+
141
+ 1. `bun create rsc-kit@latest` and copy `src/app` over the scaffold's.
142
+ 2. Fix imports from the table. `bun run typecheck` finds the rest.
143
+ 3. `bun run build` and **read the output**: every route that is not `○` says
144
+ why. Most surprises are a `cookies()` in a layout making everything
145
+ dynamic — the build says so under the summary.
146
+ 4. Actions not built from a client are listed. Decide for each.
147
+ 5. `bun run check`. Then a browser, for the parts that are a browser's.
148
+
149
+ An agent doing the port has all of this: the `.mcp.json` in the scaffold
150
+ answers `how_to({ topic })` and `read_guide({ slug })` from the installed
151
+ version, and the build report is what it reads instead of guessing.
@@ -0,0 +1,132 @@
1
+ # Getting started
2
+
3
+ > Serve React Server Components from any JavaScript backend.
4
+
5
+ A host is a `Request` in and a `Response` out. You do not normally write the
6
+ server that calls it: [Nitro](https://nitro.build) builds one around your route
7
+ tree, and where it runs is a preset in `vite.config.ts`.
8
+
9
+ ## Building
10
+
11
+ Three steps, and only the first is required:
12
+
13
+ ```bash
14
+ npm run dev # vite — serves from source, no build step
15
+ npm run build # bundles, renders every route once, then Nitro assembles .output/
16
+ npm run start # runs .output/server/index.mjs
17
+ ```
18
+
19
+ `dev` is Vite's own dev server, and edits reach the browser without a reload:
20
+
21
+ | you edit | what happens |
22
+ | --- | --- |
23
+ | a client component | Fast Refresh — the code updates and its state survives |
24
+ | a page or layout | the payload is re-fetched and the tree re-rendered |
25
+ | adding or deleting a page | the server restarts to pick up the new route table |
26
+
27
+ The browser never holds a server component, so Vite cannot hot-swap it. Instead
28
+ the client re-fetches the page — which remounts any client component below, so
29
+ their state resets.
30
+
31
+ Edit a client component directly and its state does survive; that is Fast
32
+ Refresh.
33
+
34
+ The restart on a new page is because the route tree is read when the server
35
+ starts: a page that appears later would otherwise 404 while sitting right there
36
+ on disk.
37
+
38
+ `build` ends by rendering every route once and storing what it can, which is
39
+ what turns a route into a file on disk instead of a render per request.
40
+
41
+ It is part of `build` rather than a separate command because forgetting it costs
42
+ you everything and looks like nothing — every page still works, each one just
43
+ renders again for every visitor.
44
+
45
+ Turn it off with `rscKit({ prerender: false })` when the build machine
46
+ cannot do what the pages need. See [Static
47
+ generation](/guides/static-generation).
48
+
49
+ <Aside type="note" title="There is no NODE_ENV to set">
50
+ React picks its build from it, and getting it wrong gives you a page that
51
+ renders perfectly and never hydrates. The build stamps the mode it ran in
52
+ into the server bundles, so a server is production because it was built that
53
+ way — `npm run start` needs no environment at all.
54
+ </Aside>
55
+
56
+ ## Compiling to a single binary
57
+
58
+ With the Bun preset the whole application ends up in one file:
59
+
60
+ ```bash
61
+ npm run compile # builds, then bun build --compile
62
+ ./dist/app
63
+ ```
64
+
65
+ That works because the generated config sets `serveStatic: 'inline'`. Without
66
+ it the binary compiles, starts, serves pages, and 404s every asset — the static
67
+ path resolves into Bun's virtual filesystem, where the files on disk are not.
68
+
69
+ ### What it costs
70
+
71
+ Measured on the example — eleven routes, 240 KB of assets, 164 KB of stored
72
+ pages:
73
+
74
+ | | compiled | run from `.output/` |
75
+ | --- | --- | --- |
76
+ | Cold start | 45 ms | 56–65 ms |
77
+ | Resident, idle | 18 MB | 19 MB |
78
+ | Resident, after 300 requests | 27 MB | 32 MB |
79
+ | On disk | 62 MB | 240 KB + a runtime |
80
+
81
+ Memory is flat between the two: the binary holds its assets as `Response`
82
+ objects built at boot, which for 240 KB is nothing, and both settle in the high
83
+ twenties once React has warmed up. A live render is what moves it — 40 MB after
84
+ 200 of them — and that is the renderer, not the packaging.
85
+
86
+ <Aside type="note" title="What compiling is actually for">
87
+ Deployment shape, not speed. One file, no `node_modules`, no runtime to
88
+ install. Throughput between the two is inside the noise of any benchmark I
89
+ could run on one machine — the only differences I can defend are the ~15 ms
90
+ of cold start and the shape of what you ship.
91
+ </Aside>
92
+
93
+ <Aside type="caution" title="What embedding costs">
94
+ Every asset goes in whole. Three files is nothing; a media-heavy app is
95
+ hundreds of megabytes of executable, with no CDN in front and no streaming
96
+ from disk. Keep large or rarely-read files outside the binary and serve them
97
+ from wherever they already live.
98
+
99
+ Frozen pages are not embedded either. The build writes them to
100
+ `.output/server/rsc-static` and the server reads them from beside itself; a
101
+ binary has no filesystem to read, so it renders those pages live. Everything
102
+ still answers — what you lose is the stored render, not the page.
103
+ </Aside>
104
+
105
+ ## If you work with an AI agent
106
+
107
+ The scaffold writes an `AGENTS.md` beside your `README.md`. Claude Code, Cursor
108
+ and the rest read it, and it covers the things an agent otherwise gets wrong
109
+ from React or Next habits — `"use client"` versus `"use server"`, where an
110
+ authorisation check belongs, which props are async, and that the build output is
111
+ worth reading rather than ignoring.
112
+
113
+ Beside it is `.mcp.json`, which connects the [MCP server](/guides/mcp): Claude
114
+ Code asks you to approve it on first use, and from then on an agent can read
115
+ what your last build actually did instead of guessing.
116
+
117
+ And there is a test to extend, `tests/app.test.ts`, which goes through the
118
+ real build: `bun run check` runs typecheck, lint and tests together, and is
119
+ the one command an agent — or you — runs before calling something done.
120
+
121
+ Edit it as your project grows. It is yours; nothing regenerates it.
122
+
123
+ There is also an MCP server, which answers from your actual build rather than
124
+ from memory:
125
+
126
+ ```sh
127
+ claude mcp add rsc-kit -- npx -y @rsc-kit/mcp
128
+ ```
129
+
130
+ It can say why a particular page is not static, what each route ships to the
131
+ browser, and how to build a form or an api route the way this framework
132
+ expects. See [Working with an AI agent](/guides/mcp/).
package/guides/index.json CHANGED
@@ -14,6 +14,11 @@
14
14
  "title": "Asking once per request",
15
15
  "description": "cache() — one lookup, however many places need it."
16
16
  },
17
+ {
18
+ "slug": "coming-from-next",
19
+ "title": "Coming from Next.js",
20
+ "description": "What carries over unchanged, what to rename, and what is different on purpose."
21
+ },
17
22
  {
18
23
  "slug": "connection",
19
24
  "title": "Rendering per request",
@@ -44,11 +49,26 @@
44
49
  "title": "Forms",
45
50
  "description": "Progressive forms, pending state and validation errors."
46
51
  },
52
+ {
53
+ "slug": "getting-started",
54
+ "title": "Getting started",
55
+ "description": "Serve React Server Components from any JavaScript backend."
56
+ },
47
57
  {
48
58
  "slug": "images",
49
59
  "title": "Images",
50
60
  "description": "Responsive images with no optimizer to run — unpic for a CDN, imagetools for files in the repo."
51
61
  },
62
+ {
63
+ "slug": "installation",
64
+ "title": "Installation",
65
+ "description": "From an empty directory to a streaming RSC app."
66
+ },
67
+ {
68
+ "slug": "introduction",
69
+ "title": "Introduction",
70
+ "description": "React Server Components as a Vite plugin, deployed wherever you like."
71
+ },
52
72
  {
53
73
  "slug": "mcp",
54
74
  "title": "Working with an AI agent",
@@ -67,7 +87,7 @@
67
87
  {
68
88
  "slug": "no-javascript",
69
89
  "title": "A page with no JavaScript",
70
- "description": "Rendering a route to HTML and stopping there."
90
+ "description": "The default is none. A client component is how a page asks for it."
71
91
  },
72
92
  {
73
93
  "slug": "offline",
@@ -89,6 +109,11 @@
89
109
  "title": "Queries",
90
110
  "description": "Reading from the server over GET, and letting TanStack Query or SWR own everything above it."
91
111
  },
112
+ {
113
+ "slug": "quick-start",
114
+ "title": "Quick start",
115
+ "description": "A running app in one command, or added to a project you already have."
116
+ },
92
117
  {
93
118
  "slug": "react-compiler",
94
119
  "title": "React Compiler",