@rsc-kit/mcp 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/answers.js +2 -0
- package/dist/answers.js.map +1 -1
- package/dist/bundleGuides.d.ts +7 -2
- package/dist/bundleGuides.js +14 -6
- package/dist/bundleGuides.js.map +1 -1
- package/dist/recipes.js +57 -11
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +2 -0
- package/dist/report.js.map +1 -1
- package/guides/coming-from-next.md +151 -0
- package/guides/getting-started.md +132 -0
- package/guides/index.json +25 -0
- package/guides/installation.md +338 -0
- package/guides/introduction.md +119 -0
- package/guides/no-javascript.md +48 -11
- package/guides/quick-start.md +99 -0
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -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",
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
> From an empty directory to a streaming RSC app.
|
|
4
|
+
|
|
5
|
+
There is no scaffolding command. An app is a Vite project with one plugin and
|
|
6
|
+
one request handler, so this is the whole of it.
|
|
7
|
+
|
|
8
|
+
## Create an app
|
|
9
|
+
|
|
10
|
+
One command, and it asks the rest:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
bun create rsc-kit@latest my-app
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It asks where it will run (Bun, Node or Cloudflare Workers), whether you want the
|
|
17
|
+
React Compiler and which implementation, and whether to include Tailwind. Every
|
|
18
|
+
answer has a flag, so it also runs unattended:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bun create rsc-kit@latest my-app --host=bun --compiler=oxc --tailwind
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
What comes out builds, prerenders, typechecks and serves before you edit it.
|
|
25
|
+
|
|
26
|
+
The rest of this page is for adding rsc-kit to an app you already have.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
<PackageManagers pkg="@rsc-kit/core react react-dom" />
|
|
31
|
+
|
|
32
|
+
<PackageManagers pkg="vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom" dev />
|
|
33
|
+
|
|
34
|
+
`@rsc-kit/core` brings the Vite plugin, the render engine and the client
|
|
35
|
+
runtime. React 19 and Vite 8 are peer dependencies — the versions the RSC build
|
|
36
|
+
needs, not ones this package pins for you.
|
|
37
|
+
|
|
38
|
+
<Aside type="note" title="What @vitejs/plugin-react is for">
|
|
39
|
+
Fast Refresh: edit a client component and React keeps its state, instead of
|
|
40
|
+
the page reloading and losing it. Put it *after* `rscKit()`.
|
|
41
|
+
</Aside>
|
|
42
|
+
|
|
43
|
+
## Configure Vite
|
|
44
|
+
|
|
45
|
+
```ts title="vite.config.ts"
|
|
46
|
+
import { defineConfig } from 'vite'
|
|
47
|
+
import react from '@vitejs/plugin-react'
|
|
48
|
+
import { nitro } from 'nitro/vite'
|
|
49
|
+
import { rscKit } from '@rsc-kit/core/vite'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The full route tree, built the way every scaffolded app is built.
|
|
53
|
+
*
|
|
54
|
+
* Nitro owns the server and the entry is generated — there is no server file
|
|
55
|
+
* in this directory, which is the point.
|
|
56
|
+
*/
|
|
57
|
+
export default defineConfig({
|
|
58
|
+
plugins: [
|
|
59
|
+
nitro({ preset: 'bun', serveStatic: 'inline' }),
|
|
60
|
+
rscKit({
|
|
61
|
+
sourceDir: 'src',
|
|
62
|
+
outDir: 'build',
|
|
63
|
+
viewTransitions: true,
|
|
64
|
+
offline: true,
|
|
65
|
+
}),
|
|
66
|
+
react(),
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`sourceDir` is where your route tree lives. The defaults are plain Vite ones —
|
|
72
|
+
`src/app` in, `dist/client` and `.rsc` out — so a project that adopts them can
|
|
73
|
+
call `rscKit()` with nothing at all.
|
|
74
|
+
|
|
75
|
+
<Aside type="note" title="Add your own plugins after it">
|
|
76
|
+
`rscKit()` includes `@vitejs/plugin-rsc`, which has to see modules before
|
|
77
|
+
any React layer transforms them. Tailwind, the React Compiler and anything
|
|
78
|
+
else go after it in the array.
|
|
79
|
+
</Aside>
|
|
80
|
+
|
|
81
|
+
## Write a root layout
|
|
82
|
+
|
|
83
|
+
The root layout renders the **whole document** — there is no separate HTML
|
|
84
|
+
template, and the build injects the bootstrap script and stylesheet links into
|
|
85
|
+
what you return here.
|
|
86
|
+
|
|
87
|
+
```tsx title="src/app/layout.tsx"
|
|
88
|
+
import './styles.css'
|
|
89
|
+
// Preloaded so the browser finds it before the stylesheet does. ?url is Vite's
|
|
90
|
+
// and hands back the hashed path the build serves.
|
|
91
|
+
import frauncesLatin from '@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2?url'
|
|
92
|
+
import type { ReactNode } from 'react'
|
|
93
|
+
import { Nav } from '../components/Nav'
|
|
94
|
+
import type { Metadata } from '@rsc-kit/core/metadata'
|
|
95
|
+
|
|
96
|
+
export const metadata: Metadata = {
|
|
97
|
+
title: { template: '%s · rsc-kit', default: 'rsc-kit' },
|
|
98
|
+
description: 'React Server Components as a Vite plugin',
|
|
99
|
+
// Once, here. A share-card scraper needs an absolute image url, and this is
|
|
100
|
+
// what turns the opengraph-image.png in app/ into one.
|
|
101
|
+
metadataBase: new URL('https://example.rsc-kit.dev'),
|
|
102
|
+
openGraph: {
|
|
103
|
+
siteName: 'rsc-kit example',
|
|
104
|
+
type: 'website',
|
|
105
|
+
},
|
|
106
|
+
twitter: {
|
|
107
|
+
card: 'summary_large_image',
|
|
108
|
+
site: '@rsckit',
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// `modal` is a parallel slot: the @modal directory beside this file fills it.
|
|
113
|
+
// It renders alongside children, not instead of them.
|
|
114
|
+
export default function RootLayout({ children, modal }: { children: ReactNode; modal?: ReactNode }) {
|
|
115
|
+
return (
|
|
116
|
+
<html lang="en">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charSet="utf-8" />
|
|
119
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
120
|
+
<link rel="preload" href={frauncesLatin} as="font" type="font/woff2" crossOrigin="anonymous" />
|
|
121
|
+
</head>
|
|
122
|
+
<body>
|
|
123
|
+
<Nav />
|
|
124
|
+
<main>{children}</main>
|
|
125
|
+
{modal}
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Write a page
|
|
133
|
+
|
|
134
|
+
```tsx title="src/app/page.tsx"
|
|
135
|
+
export default function HomePage() {
|
|
136
|
+
return <h1>Hello</h1>;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`app/page.tsx` serves `GET /`, wrapped in `app/layout.tsx`. Nothing registers
|
|
141
|
+
it — the plugin reads the directory at build time. See
|
|
142
|
+
[Routing](/guides/routing) for the rest of the conventions.
|
|
143
|
+
|
|
144
|
+
## Serve it
|
|
145
|
+
|
|
146
|
+
There is no server file to write. Nitro builds one around the route tree, and
|
|
147
|
+
`npm run build` leaves it in `.output/`:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
npm run build
|
|
151
|
+
npm run start
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Where it runs is the preset in `vite.config.ts` — see [Where it
|
|
155
|
+
runs](/hosts/where-it-runs).
|
|
156
|
+
|
|
157
|
+
## Scripts
|
|
158
|
+
|
|
159
|
+
```json title="package.json"
|
|
160
|
+
{
|
|
161
|
+
"type": "module",
|
|
162
|
+
"scripts": {
|
|
163
|
+
"dev": "vite",
|
|
164
|
+
"build": "vite build",
|
|
165
|
+
"start": "bun .output/server/index.mjs",
|
|
166
|
+
"compile": "bun build --compile .output/server/index.mjs --outfile my-app"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`build` freezes every page it can, so there is no separate prerender step and
|
|
172
|
+
no command for one — freezing runs the app, which needs the bundle the build
|
|
173
|
+
just wrote, and only the build knows where that is. Turn it off with
|
|
174
|
+
`rscKit({ prerender: false })`.
|
|
175
|
+
|
|
176
|
+
`start` runs what the build wrote. There is no server file in a scaffolded app —
|
|
177
|
+
Nitro builds the server around the route tree, and where it runs is a preset in
|
|
178
|
+
`vite.config.ts`. See [Where it runs](/hosts/where-it-runs).
|
|
179
|
+
|
|
180
|
+
`compile` produces a standalone binary containing the engine, the route tree,
|
|
181
|
+
the frozen pages **and** the assets. That last part is `serveStatic: 'inline'`
|
|
182
|
+
in the vite config; without it the binary serves pages and 404s every asset.
|
|
183
|
+
|
|
184
|
+
<Aside type="note" title="Do not set NODE_ENV">
|
|
185
|
+
The build stamps its mode into the bundle, so `npm run start` is
|
|
186
|
+
production because it was *built* that way. Setting `NODE_ENV` yourself adds a
|
|
187
|
+
second source of truth that can disagree — and when it disagrees, every page
|
|
188
|
+
renders and none of them become interactive.
|
|
189
|
+
|
|
190
|
+
For a development build with React's real error messages, use
|
|
191
|
+
`vite build --mode development`.
|
|
192
|
+
</Aside>
|
|
193
|
+
|
|
194
|
+
Then:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm run build # bundles, and freezes every page it can
|
|
198
|
+
npm run start
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Worth adding a `typecheck` script beside them (`tsc --noEmit`) and running it
|
|
202
|
+
in CI. Server components are ordinary functions and pages are ordinary
|
|
203
|
+
modules, so the typechecker sees the whole route tree — a page whose props do
|
|
204
|
+
not match its `[param]` segment is a compile error rather than a blank page.
|
|
205
|
+
|
|
206
|
+
## TypeScript
|
|
207
|
+
|
|
208
|
+
```json title="tsconfig.json"
|
|
209
|
+
{
|
|
210
|
+
"compilerOptions": {
|
|
211
|
+
"target": "ESNext",
|
|
212
|
+
"module": "ESNext",
|
|
213
|
+
"moduleResolution": "bundler",
|
|
214
|
+
"jsx": "react-jsx",
|
|
215
|
+
"strict": true,
|
|
216
|
+
"noEmit": true,
|
|
217
|
+
"skipLibCheck": true,
|
|
218
|
+
"resolveJsonModule": true,
|
|
219
|
+
"types": [
|
|
220
|
+
"@types/bun",
|
|
221
|
+
"vite/client"
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
"include": ["src/**/*", "server/**/*", ".rsc-kit/**/*", "vite.config.ts"]
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`vite/client` is doing real work in that list: without it a side-effect import
|
|
229
|
+
of a stylesheet (`import './styles.css'` in the root layout) is an error, and
|
|
230
|
+
`import.meta.env` is untyped.
|
|
231
|
+
|
|
232
|
+
### Environment variables
|
|
233
|
+
|
|
234
|
+
There is nothing to install and nothing this package adds — Vite already owns
|
|
235
|
+
this. Two rules and one declaration.
|
|
236
|
+
|
|
237
|
+
Anything named `VITE_*` is **inlined into the client bundle** and ships to the
|
|
238
|
+
browser, whether or not a browser file reads it. Everything else stays on the
|
|
239
|
+
server, read through `process.env`.
|
|
240
|
+
|
|
241
|
+
The prefix is the whole boundary, so never put a secret behind it.
|
|
242
|
+
`VITE_STRIPE_KEY` is a published key.
|
|
243
|
+
|
|
244
|
+
Declare the ones you use, and Vite types them:
|
|
245
|
+
|
|
246
|
+
```ts title="src/env.d.ts"
|
|
247
|
+
interface ViteTypeOptions {
|
|
248
|
+
// Turns an unknown import.meta.env key into an error instead of `any`.
|
|
249
|
+
strictImportMetaEnv: unknown;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface ImportMetaEnv {
|
|
253
|
+
readonly VITE_API_URL: string;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
<Aside type="caution" title="On Bun, a misspelled variable still compiles">
|
|
258
|
+
`@types/bun` declares its own `ImportMetaEnv`, which cancels out the strictness
|
|
259
|
+
Vite's option adds. You still get autocomplete and `string` instead of
|
|
260
|
+
`string | undefined` — but a typo is `any`, not an error.
|
|
261
|
+
</Aside>
|
|
262
|
+
|
|
263
|
+
#### When they are read
|
|
264
|
+
|
|
265
|
+
`import.meta.env.VITE_*` is a literal in the bundle: changing one means a
|
|
266
|
+
rebuild. `process.env.*` is a live read, so the deploy's environment wins —
|
|
267
|
+
with one exception the build makes for you. A route that reads an environment
|
|
268
|
+
variable and nothing request-bound is frozen at build time, value included,
|
|
269
|
+
because nothing marked it as needing a request. That is the right answer for
|
|
270
|
+
`PUBLIC_SITE_NAME` and the wrong one for a feature flag flipped per deploy. For
|
|
271
|
+
the second kind, read it after `await connection()`, which keeps the route on
|
|
272
|
+
the server. See [Static generation](/guides/static-generation).
|
|
273
|
+
|
|
274
|
+
#### Validating them
|
|
275
|
+
|
|
276
|
+
Vite checks the prefix and nothing else: a missing `DATABASE_URL` is
|
|
277
|
+
`undefined` until the first query fails. There is nothing native for this
|
|
278
|
+
because nothing native is needed — [t3-env](https://env.t3.gg/docs/core) works
|
|
279
|
+
unchanged, with any Standard Schema validator:
|
|
280
|
+
|
|
281
|
+
```ts title="src/env.ts"
|
|
282
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
283
|
+
import { z } from 'zod';
|
|
284
|
+
|
|
285
|
+
export const env = createEnv({
|
|
286
|
+
server: { DATABASE_URL: z.string().url() },
|
|
287
|
+
clientPrefix: 'VITE_',
|
|
288
|
+
client: { VITE_API_URL: z.string().url() },
|
|
289
|
+
runtimeEnv: typeof process === 'undefined' ? import.meta.env : { ...import.meta.env, ...process.env },
|
|
290
|
+
emptyStringAsUndefined: true,
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Read `env.DATABASE_URL` instead of `process.env.DATABASE_URL` and three things
|
|
295
|
+
follow. It is a `string`, not `string | undefined`. A server variable touched
|
|
296
|
+
from a client component throws by name rather than being silently `undefined`.
|
|
297
|
+
And because the module validates when it is first imported, a missing variable
|
|
298
|
+
fails the **build** — the prerender imports it — instead of the first request
|
|
299
|
+
in production.
|
|
300
|
+
|
|
301
|
+
The build writes its declaration files into `.rsc-kit/` at the project root,
|
|
302
|
+
not among your own source. Three of them:
|
|
303
|
+
|
|
304
|
+
- `rsc-routes.d.ts` — the routes it found, which is what makes `Link` and
|
|
305
|
+
`redirect()` [typed](/guides/routing#typed-links)
|
|
306
|
+
- `rsc-env.d.ts` — the host global
|
|
307
|
+
- `rsc-engine.d.ts` — the generated bundle
|
|
308
|
+
|
|
309
|
+
Only what has to be generated is. Anything you can import, you import —
|
|
310
|
+
[`Metadata`](/guides/metadata/) comes from the package and works before you have
|
|
311
|
+
built anything.
|
|
312
|
+
|
|
313
|
+
These are ambient, which needs only that they are inside the project and that
|
|
314
|
+
the typechecker is told the directory exists:
|
|
315
|
+
|
|
316
|
+
```json title="tsconfig.json"
|
|
317
|
+
{ "include": ["src/**/*", ".rsc-kit/**/*"] }
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
A scaffolded app has that line, and `rsc-kit init` adds it. Leave it out and
|
|
321
|
+
the build says so — otherwise typed routes fall back to plain `string` and
|
|
322
|
+
nothing else would tell you.
|
|
323
|
+
|
|
324
|
+
Add `.rsc-kit/` to `.gitignore`.
|
|
325
|
+
|
|
326
|
+
## Where next
|
|
327
|
+
|
|
328
|
+
<CardGrid>
|
|
329
|
+
<Card title="Routing" icon="ph:tree-structure">
|
|
330
|
+
[Pages, layouts, slots and navigation →](/guides/routing)
|
|
331
|
+
</Card>
|
|
332
|
+
<Card title="Server actions" icon="ph:lightning">
|
|
333
|
+
[Calling the server from a client component →](/guides/server-actions)
|
|
334
|
+
</Card>
|
|
335
|
+
<Card title="Static generation" icon="ph:snowflake">
|
|
336
|
+
[Rendering ahead of time, and exporting →](/guides/static-generation)
|
|
337
|
+
</Card>
|
|
338
|
+
</CardGrid>
|