@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/answers.js +2 -0
- package/dist/answers.js.map +1 -1
- package/dist/bundleGuides.d.ts +7 -2
- package/dist/bundleGuides.js +17 -9
- package/dist/bundleGuides.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +66 -14
- 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 +26 -1
- package/guides/installation.md +338 -0
- package/guides/introduction.md +119 -0
- package/guides/no-javascript.md +57 -25
- package/guides/quick-start.md +99 -0
- package/guides/static-generation.md +6 -0
- package/package.json +1 -1
|
@@ -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>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
> React Server Components as a Vite plugin, deployed wherever you like.
|
|
4
|
+
|
|
5
|
+
`rsc-kit` is a Vite plugin that gives you React Server Components. You write
|
|
6
|
+
pages, layouts and server actions the way you would in the Next.js App Router;
|
|
7
|
+
it renders them, streams the HTML, and turns links into navigations that replace
|
|
8
|
+
only the part of the page that changed.
|
|
9
|
+
|
|
10
|
+
It brings no build system of its own — it is a Vite plugin — and it does not
|
|
11
|
+
write a server for you either. [Nitro](https://nitro.build) builds one around
|
|
12
|
+
your route tree, so where an app runs is a preset: Bun, Node, a Cloudflare
|
|
13
|
+
Worker, Vercel, Netlify, Deno.
|
|
14
|
+
|
|
15
|
+
## What an app looks like
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
src/app/
|
|
19
|
+
layout.tsx the document — <html>, <head>, <body>
|
|
20
|
+
loading.tsx Suspense fallback for everything below
|
|
21
|
+
page.tsx GET /
|
|
22
|
+
posts/
|
|
23
|
+
[slug]/
|
|
24
|
+
page.tsx GET /posts/:slug
|
|
25
|
+
@modal/
|
|
26
|
+
default.tsx a parallel slot, empty until something fills it
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Nothing registers those files. The plugin reads the directory at build time and
|
|
30
|
+
generates the entry that knows about them.
|
|
31
|
+
|
|
32
|
+
```tsx title="src/app/posts/[slug]/page.tsx"
|
|
33
|
+
import { findPost } from '../../../data'
|
|
34
|
+
|
|
35
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
36
|
+
const { slug } = await params
|
|
37
|
+
const post = await findPost(slug)
|
|
38
|
+
|
|
39
|
+
if (!post) return <h1>No such post</h1>
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<article>
|
|
43
|
+
<h1>{post.title}</h1>
|
|
44
|
+
<p>{post.body}</p>
|
|
45
|
+
</article>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A server component is an ordinary async function. It imports its data module
|
|
51
|
+
directly, and neither the module nor its secrets reach the browser.
|
|
52
|
+
|
|
53
|
+
## What you get
|
|
54
|
+
|
|
55
|
+
<CardGrid>
|
|
56
|
+
<Card title="Streaming, not waterfalls" icon="ph:waves">
|
|
57
|
+
The shell paints before the data resolves. Suspense boundaries fill in as
|
|
58
|
+
they finish, in whatever order they finish.
|
|
59
|
+
</Card>
|
|
60
|
+
<Card title="Navigations that keep the page" icon="ph:arrows-left-right">
|
|
61
|
+
A navigation sends the layout chain it already has, and the server answers
|
|
62
|
+
with only the part that changed. Scroll, focus and half-typed forms survive.
|
|
63
|
+
</Card>
|
|
64
|
+
<Card title="Parallel routes and interception" icon="ph:frame-corners">
|
|
65
|
+
`@slot` directories render alongside the page. `(.)folder` opens a route as
|
|
66
|
+
a modal over the page you were on, and as a real page on refresh.
|
|
67
|
+
</Card>
|
|
68
|
+
<Card title="Server actions" icon="ph:lightning">
|
|
69
|
+
`"use server"` makes an async function callable from a client component.
|
|
70
|
+
The body never ships; the call becomes one POST.
|
|
71
|
+
</Card>
|
|
72
|
+
<Card title="Queries, without an endpoint" icon="ph:magnifying-glass">
|
|
73
|
+
`query()` marks a read. It travels as a GET, so it can be cached and
|
|
74
|
+
prefetched — and reads that happen together leave as one request.
|
|
75
|
+
</Card>
|
|
76
|
+
<Card title="Frozen where it can be" icon="ph:snowflake">
|
|
77
|
+
Pages that ask for nothing dynamic are rendered at build time. Pages that do
|
|
78
|
+
get their shell frozen and their data streamed.
|
|
79
|
+
</Card>
|
|
80
|
+
<Card title="An export target" icon="ph:folder-open">
|
|
81
|
+
The same app can build to a directory of files and be served by anything —
|
|
82
|
+
including a CDN with no origin at all.
|
|
83
|
+
</Card>
|
|
84
|
+
</CardGrid>
|
|
85
|
+
|
|
86
|
+
## Where it runs
|
|
87
|
+
|
|
88
|
+
You do not write a server. [Nitro](https://nitro.build) builds one from your
|
|
89
|
+
route tree, and where it runs is one line in your Vite config:
|
|
90
|
+
|
|
91
|
+
```ts title="vite.config.ts"
|
|
92
|
+
nitro({ preset: 'bun' })
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Swap the preset for `node`, `cloudflare_module`, `vercel`, `netlify` or `deno`
|
|
96
|
+
and the same app deploys there instead. The build produces a `.output`
|
|
97
|
+
directory; nothing else changes.
|
|
98
|
+
|
|
99
|
+
## What it is not
|
|
100
|
+
|
|
101
|
+
<Aside type="note" title="No data layer, no CSS pipeline, no dev server of its own">
|
|
102
|
+
The build runs your project's Vite config, so Tailwind, PostCSS and the rest
|
|
103
|
+
are configured the way they are in any Vite app. Data access is an import.
|
|
104
|
+
There is no `getServerSideProps` equivalent, and no request context to learn.
|
|
105
|
+
</Aside>
|
|
106
|
+
|
|
107
|
+
## Where to go next
|
|
108
|
+
|
|
109
|
+
<CardGrid>
|
|
110
|
+
<Card title="Getting started" icon="ph:play">
|
|
111
|
+
[Build and deploy your first app →](/getting-started)
|
|
112
|
+
</Card>
|
|
113
|
+
<Card title="Routing" icon="ph:tree-structure">
|
|
114
|
+
[Pages, layouts, slots and navigation →](/guides/routing)
|
|
115
|
+
</Card>
|
|
116
|
+
<Card title="How it works" icon="ph:gear">
|
|
117
|
+
[What happens between a request and a page →](/reference/how-it-works)
|
|
118
|
+
</Card>
|
|
119
|
+
</CardGrid>
|
package/guides/no-javascript.md
CHANGED
|
@@ -1,39 +1,71 @@
|
|
|
1
1
|
# A page with no JavaScript
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The default is none. A client component is how a page asks for it.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
There is no JavaScript on a page until something on it needs some. A
|
|
6
|
+
marketing page, a changelog, an article, a policy: the build stores them as
|
|
7
|
+
HTML and stops. No bootstrap script, so no React, no Flight client, no router.
|
|
7
8
|
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export default function AboutPage() {
|
|
12
|
-
return <h1>About</h1>
|
|
13
|
-
}
|
|
9
|
+
```
|
|
10
|
+
○ /about no js
|
|
11
|
+
no client components, so ships no javascript; stylesheet inlined
|
|
14
12
|
```
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
**`"use client"` is the opt-in.** A route that freezes whole, renders none of
|
|
15
|
+
your client components, and has no server action in its tree ships nothing.
|
|
16
|
+
Put a client component in it — a counter, a `<Link>`, an update prompt — and
|
|
17
|
+
that page has the runtime, because there is now something for the runtime to
|
|
18
|
+
do. That is the whole rule, and there is no switch beside it in either
|
|
19
|
+
direction, on purpose:
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
- Nothing can *need* the runtime without a client component or an action in
|
|
22
|
+
its tree, so there is nothing an "on" switch could say that the tree does
|
|
23
|
+
not already say.
|
|
24
|
+
- A page that must *stay* this way is an assertion, not a setting: the route's
|
|
25
|
+
`clientJs` in `build-report.json` is `null` when it shipped none, and a CI
|
|
26
|
+
step can hold it there.
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
The check reads the rendered tree, not the source, so a client component
|
|
29
|
+
reached through a shared layout counts. Most pages under a layout with a
|
|
30
|
+
`<Link>` in its header keep the runtime; a route group with its own plain
|
|
31
|
+
layout drops it. That is the honest shape of it: the pages that can be
|
|
32
|
+
nothing are nothing, and the page with a nav is a page with a nav.
|
|
24
33
|
|
|
34
|
+
## What a page without the runtime still does
|
|
35
|
+
|
|
36
|
+
**Its stylesheet is inlined** into the document when it is small on the wire
|
|
37
|
+
(at most 10 kB gzipped), so the first paint waits on no request but the
|
|
38
|
+
document itself. The build says so on the route's line. Fonts are the one
|
|
39
|
+
thing left to you, because only you know which face the first paint needs —
|
|
40
|
+
the [fonts guide](/guides/fonts) shows the two-line preload.
|
|
41
|
+
|
|
42
|
+
The default is not always the right call. A site of many small pages — a
|
|
43
|
+
blog, documentation — may prefer one stylesheet the browser caches once over
|
|
44
|
+
the same 4 kB repeated in every document, and a sheet just over the cap may
|
|
45
|
+
be worth inlining anyway. It is one setting on the plugin:
|
|
46
|
+
|
|
47
|
+
```ts title="vite.config.ts"
|
|
48
|
+
rscKit({ inlineStylesheets: 'auto' }) // the default: when at most 10 kB gzipped
|
|
49
|
+
rscKit({ inlineStylesheets: false }) // never — keep the link, cache one file
|
|
50
|
+
rscKit({ inlineStylesheets: true }) // always, whatever the size
|
|
51
|
+
rscKit({ inlineStylesheets: 24_000 }) // your own cap, in gzipped bytes
|
|
25
52
|
```
|
|
26
|
-
app/about/page.tsx declares clientJs = false but renders client components:
|
|
27
|
-
src/components/Counter.tsx
|
|
28
|
-
```
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
**It still registers the [service worker](/guides/offline)** when the app has
|
|
55
|
+
one: the runtime's one-line registration is inlined in its place, so a visitor
|
|
56
|
+
who lands here first gets the worker the second visit is for. What such a
|
|
57
|
+
page cannot show is the update prompt — that is a client component, and a
|
|
58
|
+
page that uses it has the runtime.
|
|
59
|
+
|
|
60
|
+
**Navigation is unchanged in both directions.** A page with no client
|
|
61
|
+
component has no `<Link>`, so its anchors were already full loads. A `<Link>`
|
|
62
|
+
elsewhere that points *at* it still fetches its flight payload and swaps the
|
|
63
|
+
segment, because that payload is written from the render with the runtime.
|
|
34
64
|
|
|
35
|
-
##
|
|
65
|
+
## Only a page stored whole
|
|
36
66
|
|
|
37
67
|
With no client runtime there is nothing to fill a Suspense hole after the
|
|
38
|
-
shell arrives
|
|
39
|
-
|
|
68
|
+
shell arrives, so a partial prerender always keeps it, and so does a page
|
|
69
|
+
rendered per request. The line is what the runtime would *do*: hydrate a
|
|
70
|
+
client component, submit a `<form action={serverFn}>`, fill a hole. A page
|
|
71
|
+
with none of those is HTML, and is stored as HTML.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Quick start
|
|
2
|
+
|
|
3
|
+
> A running app in one command, or added to a project you already have.
|
|
4
|
+
|
|
5
|
+
## A new app
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun create rsc-kit@latest my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
bun run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That is it. You get a page, a layout, a client component and a Vite config,
|
|
14
|
+
wired together and running. There is no server file — Nitro builds one from the
|
|
15
|
+
preset in that config when you build.
|
|
16
|
+
|
|
17
|
+
When you are ready to ship:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun run build
|
|
21
|
+
bun run start
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The build prints what it did:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
○ /
|
|
28
|
+
|
|
29
|
+
○ (Static) prerendered as static content
|
|
30
|
+
|
|
31
|
+
1 static
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pages marked `○` are **static**: rendered once at build time rather than for
|
|
35
|
+
each visitor.
|
|
36
|
+
|
|
37
|
+
## An app you already have
|
|
38
|
+
|
|
39
|
+
Already have a project? Add rsc-kit to it:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
bunx rsc-kit@latest init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It reads your `package.json`, works out which server you use and where your
|
|
46
|
+
source lives, and writes only what is missing:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
server hono
|
|
50
|
+
source src
|
|
51
|
+
react will be added
|
|
52
|
+
|
|
53
|
+
+ src/app/layout.tsx
|
|
54
|
+
+ src/app/page.tsx
|
|
55
|
+
+ vite.config.ts
|
|
56
|
+
~ package.json — added @rsc-kit/core, react, react-dom, vite, nitro, …
|
|
57
|
+
~ scripts — dev, build, start, compile, typecheck
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No server file. Nitro builds one around the route tree, so the app owns a route
|
|
61
|
+
tree and a vite config and nothing in between.
|
|
62
|
+
|
|
63
|
+
**It never overwrites anything.** If you already have a `vite.config.ts`, it
|
|
64
|
+
prints the edit to make instead of replacing work you have done. Running it twice is safe — the second run just tells you what is already
|
|
65
|
+
in place.
|
|
66
|
+
|
|
67
|
+
Then:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
bun install
|
|
71
|
+
bun run dev
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Add a page
|
|
75
|
+
|
|
76
|
+
Routes are directories under `src/app`. Create a folder, put a `page.tsx` in
|
|
77
|
+
it, and it exists:
|
|
78
|
+
|
|
79
|
+
```tsx title="src/app/about/page.tsx"
|
|
80
|
+
export default function AboutPage() {
|
|
81
|
+
return <h1>About</h1>
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
bun run build # the route tree is read at build time
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Doing it by hand
|
|
90
|
+
|
|
91
|
+
If you would rather wire it up yourself — or the CLI cannot reach your
|
|
92
|
+
setup — [Installation](/installation) walks through the same result one file at
|
|
93
|
+
a time.
|
|
94
|
+
|
|
95
|
+
## Where next
|
|
96
|
+
|
|
97
|
+
- [Routing](/guides/routing) — layouts, dynamic segments, parallel slots
|
|
98
|
+
- [Server actions](/guides/server-actions) — mutations without an API route
|
|
99
|
+
- [Where it runs](/hosts/where-it-runs) — presets, and compiling to a binary
|
|
@@ -118,6 +118,12 @@ rscKit({ sourceDir: 'src', prerender: false });
|
|
|
118
118
|
**Try [`connection()`](/guides/connection) first.** It is almost always the
|
|
119
119
|
better answer, and this is almost always too big a hammer.
|
|
120
120
|
|
|
121
|
+
It is also one hammer for two things. The decision that a page has
|
|
122
|
+
[nothing to hydrate](/guides/no-javascript) is made in the same build-time
|
|
123
|
+
render, so with prerendering off every page renders per request, with the
|
|
124
|
+
runtime. That is the right trade for a build machine that cannot reach the
|
|
125
|
+
data — the reason this exists — and the wrong one for anything else.
|
|
126
|
+
|
|
121
127
|
Prerendering **runs your application code**, so it needs whatever that code
|
|
122
128
|
needs — a page that queries a database needs that database reachable from the
|
|
123
129
|
build. When it is not, marking that one query is a smaller and more accurate
|
package/package.json
CHANGED