@rsc-kit/mcp 0.17.0 → 0.18.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/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +367 -17
- package/dist/recipes.js.map +1 -1
- package/guides/api-routes.md +125 -9
- package/guides/authorization.md +39 -0
- package/guides/backend-answered-pages.md +210 -0
- package/guides/bun.md +76 -0
- package/guides/coming-from-next.md +4 -0
- package/guides/deployment.md +129 -0
- package/guides/domains.md +129 -0
- package/guides/errors.md +11 -1
- package/guides/go.md +194 -0
- package/guides/index.json +45 -0
- package/guides/installation.md +47 -9
- package/guides/instrumentation.md +91 -0
- package/guides/introduction.md +4 -0
- package/guides/laravel.md +406 -0
- package/guides/mcp.md +11 -5
- package/guides/queries.md +138 -8
- package/guides/quick-start.md +31 -0
- package/guides/redirects.md +14 -0
- package/guides/response-headers.md +22 -0
- package/guides/seo-files.md +45 -0
- package/guides/testing.md +4 -2
- package/guides/typed-routes.md +17 -0
- package/guides/url-validation.md +8 -2
- package/guides/where-it-runs.md +168 -0
- package/guides/your-own-backend.md +238 -0
- package/package.json +2 -2
package/guides/go.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Go
|
|
2
|
+
|
|
3
|
+
> A Go process behind the renderer — functions, guards and actions in Go.
|
|
4
|
+
|
|
5
|
+
Go owns the request: sessions, auth, the database. The renderer owns
|
|
6
|
+
rendering, because that half is React. A server component reaches Go by
|
|
7
|
+
calling `rpc()`, which leaves the renderer as an ordinary POST; a `middleware.ts`
|
|
8
|
+
names guards Go runs before a page renders; a server action the browser calls
|
|
9
|
+
is a Go function the build wrote a stub for.
|
|
10
|
+
|
|
11
|
+
A [BAP](/hosts/backend-answered-pages) with Go behind it. There is no
|
|
12
|
+
JavaScript to write for Go: the app is what the scaffold writes, and the Go
|
|
13
|
+
side is one package answering
|
|
14
|
+
[the endpoint every backend answers](/hosts/your-own-backend).
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
go get github.com/rsc-kit/go
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Wiring
|
|
21
|
+
|
|
22
|
+
In a Go module, `init` sees `go.mod` and does the JavaScript half — the route
|
|
23
|
+
tree, `vite.config.ts`, `.env` with `RSC_BACKEND` and a generated secret, the
|
|
24
|
+
scripts — and prints the Go half for you to add:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bunx rsc-kit@latest init # in the directory with go.mod
|
|
28
|
+
bun create rsc-kit@latest my-app --backend=http://127.0.0.1:8080 # or a new app
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
What it wrote is two lines, read by `vite` in development and by the built
|
|
32
|
+
server in production:
|
|
33
|
+
|
|
34
|
+
```ini title=".env"
|
|
35
|
+
RSC_BACKEND=http://127.0.0.1:8080
|
|
36
|
+
RSC_HOST_CALL_SECRET=a-long-random-string
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
And the endpoint, on whatever mux you already have:
|
|
40
|
+
|
|
41
|
+
```go title="main.go"
|
|
42
|
+
reg := rsckit.NewRegistry()
|
|
43
|
+
// … Register, Middleware, RegisterAction below …
|
|
44
|
+
|
|
45
|
+
callback, err := rsckit.NewCallbackHandler(reg, os.Getenv("RSC_HOST_CALL_SECRET"))
|
|
46
|
+
if err != nil {
|
|
47
|
+
log.Fatal(err) // refuses to exist without a secret
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mux := http.NewServeMux()
|
|
51
|
+
mux.Handle("POST /__rsc/host-call", callback)
|
|
52
|
+
http.ListenAndServe("127.0.0.1:8080", mux)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The renderer forwards any url its route tree does not own to `RSC_BACKEND`,
|
|
56
|
+
so a Go route on that mux — `/login`, a webhook, an upload — is reachable at
|
|
57
|
+
the renderer's origin too.
|
|
58
|
+
|
|
59
|
+
## Reading from Go
|
|
60
|
+
|
|
61
|
+
```go
|
|
62
|
+
reg.Register("Orders.recent", func(ctx context.Context, args rsckit.Args) (any, error) {
|
|
63
|
+
var limit int
|
|
64
|
+
if err := args.Bind(&limit); err != nil {
|
|
65
|
+
return nil, err
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The visitor's cookie, forwarded from the page request: this runs as
|
|
69
|
+
// them, not as nobody.
|
|
70
|
+
session := rsckit.HeadersFrom(ctx).Get("Cookie")
|
|
71
|
+
|
|
72
|
+
return db.RecentOrders(ctx, session, limit)
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx title="src/app/orders/page.tsx"
|
|
77
|
+
export default async function Orders() {
|
|
78
|
+
const orders = await rpc<Order[]>('Orders.recent', 5)
|
|
79
|
+
|
|
80
|
+
return <ul>{orders.map((o) => <li key={o.id}>{o.number}</li>)}</ul>
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`args.Bind` decodes the positional arguments `rpc()` was given; too few is an
|
|
85
|
+
error, extra ones are ignored. Whatever the function returns is encoded as
|
|
86
|
+
JSON and decoded on the other side as whatever `rpc<T>()` was told.
|
|
87
|
+
|
|
88
|
+
### Refusing
|
|
89
|
+
|
|
90
|
+
Return a value and it is the result. Return one of these and the render is
|
|
91
|
+
told what happened, rather than handed a 500:
|
|
92
|
+
|
|
93
|
+
| return | the render gets |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `rsckit.InvalidField("name", "…")`, `rsckit.Invalid(map)` | 422, each message under its input on the form that submitted |
|
|
96
|
+
| `rsckit.Unauthenticated()` | 401, the engine's own authentication error |
|
|
97
|
+
| `rsckit.Unauthorized("…")` | 403 |
|
|
98
|
+
| `rsckit.Redirect("/login")` | the browser goes there |
|
|
99
|
+
| `rsckit.Refuse(429, "slow down")` | that status, kept |
|
|
100
|
+
| any other `error` | 500, with the message |
|
|
101
|
+
|
|
102
|
+
Wrapped errors still answer as what they are — `errors.As` finds the refusal
|
|
103
|
+
inside `fmt.Errorf("…: %w", err)` — and a panic becomes an error for that one
|
|
104
|
+
call rather than taking the server down.
|
|
105
|
+
|
|
106
|
+
## Route middleware
|
|
107
|
+
|
|
108
|
+
```ts title="src/app/admin/middleware.ts"
|
|
109
|
+
export const middleware = ['auth', 'can:manage-orders', 'throttle:60,1']
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```go
|
|
113
|
+
reg.Middleware("auth", func(ctx context.Context, _ string) error {
|
|
114
|
+
if !signedIn(rsckit.HeadersFrom(ctx)) {
|
|
115
|
+
return rsckit.Redirect("/login")
|
|
116
|
+
}
|
|
117
|
+
return nil
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
reg.Middleware("can", func(ctx context.Context, ability string) error {
|
|
121
|
+
if !allowed(ctx, ability) {
|
|
122
|
+
return rsckit.Unauthorized("you may not " + ability)
|
|
123
|
+
}
|
|
124
|
+
return nil
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The renderer sends the list before anything at or below `admin/` renders —
|
|
129
|
+
including a page it froze at build time, before the file is served. A guard
|
|
130
|
+
receives what follows the colon in its name (`"manage-orders"`, `"60,1"`,
|
|
131
|
+
`""`); guards run in order and stop at the first refusal. A name nothing is
|
|
132
|
+
registered for refuses rather than passes: a declared check that silently
|
|
133
|
+
does not happen is the failure this exists to prevent.
|
|
134
|
+
|
|
135
|
+
## Server actions
|
|
136
|
+
|
|
137
|
+
```go
|
|
138
|
+
reg.RegisterAction("ordersCancel", "Orders.cancel", func(ctx context.Context, args rsckit.Args) (any, error) {
|
|
139
|
+
var id int
|
|
140
|
+
if err := args.Bind(&id); err != nil {
|
|
141
|
+
return nil, err
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if err := orders.Cancel(ctx, id); err != nil {
|
|
145
|
+
return nil, err
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
rsckit.Revalidate(ctx, "orders")
|
|
149
|
+
|
|
150
|
+
return nil, nil
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Before each build. The build reads it and writes the stubs.
|
|
154
|
+
reg.WriteActionManifest("rsc-host-actions.json")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```tsx title="src/app/orders/CancelButton.tsx"
|
|
158
|
+
'use client'
|
|
159
|
+
import { ordersCancel } from '../../server-actions.generated'
|
|
160
|
+
|
|
161
|
+
export function CancelButton({ id }: { id: number }) {
|
|
162
|
+
return <button onClick={() => ordersCancel(id)}>Cancel</button>
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`Revalidate` names a [section](/guides/sections) or `page`, and the answer to
|
|
167
|
+
the action carries it re-rendered rather than the browser being told to ask
|
|
168
|
+
again. Write the manifest as part of the build — the example's `build`
|
|
169
|
+
script runs the Go program with `-manifest-only` first — because a stale map
|
|
170
|
+
names a function that has since been renamed, and nothing fails until the
|
|
171
|
+
browser calls it.
|
|
172
|
+
|
|
173
|
+
## Production
|
|
174
|
+
|
|
175
|
+
`bun run build` and `bun run start` with the same two variables in the
|
|
176
|
+
renderer's environment. Put the renderer in front and restrict
|
|
177
|
+
`/__rsc/host-call` at the web server, as [Laravel's page](/hosts/laravel/#which-process-faces-the-internet)
|
|
178
|
+
shows; a Go worker is held for the length of a host call, never a render.
|
|
179
|
+
|
|
180
|
+
Or put Go in front: `rsckit.NewRenderer(url)` is a streaming reverse proxy
|
|
181
|
+
to the renderer, and `rsckit.NewHandler(renderer, callback, path)` routes
|
|
182
|
+
the callback path to the endpoint and everything else through it. Both sides
|
|
183
|
+
mark what they forward, so a url neither owns is a 404 rather than a loop.
|
|
184
|
+
|
|
185
|
+
On a Worker, the two variables are a var and a secret in `wrangler.json`,
|
|
186
|
+
and the Go process has to be reachable from Cloudflare's network — an
|
|
187
|
+
`https` origin behind the secret and, ideally, an allow-list, since loopback
|
|
188
|
+
is not an option there.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
The runnable version is
|
|
193
|
+
[`examples/go-backend`](https://github.com/rsc-kit/rsc-kit/tree/main/examples/go-backend)
|
|
194
|
+
in the repository: `bun run backend`, `bun run dev`, open the renderer.
|
package/guides/index.json
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
"title": "Authorization",
|
|
10
10
|
"description": "Protecting pages, server actions and API routes."
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"slug": "backend-answered-pages",
|
|
14
|
+
"title": "Backend-Answered Pages",
|
|
15
|
+
"description": "BAP — the model for an rsc-kit app with a backend behind it, and how to build for it."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"slug": "bun",
|
|
19
|
+
"title": "Running on Bun",
|
|
20
|
+
"description": "What is different when the runtime is Bun — and what only looks like it is."
|
|
21
|
+
},
|
|
12
22
|
{
|
|
13
23
|
"slug": "caching",
|
|
14
24
|
"title": "Asking once per request",
|
|
@@ -24,6 +34,16 @@
|
|
|
24
34
|
"title": "Rendering per request",
|
|
25
35
|
"description": "Marking work that belongs to the visitor, not to the build."
|
|
26
36
|
},
|
|
37
|
+
{
|
|
38
|
+
"slug": "deployment",
|
|
39
|
+
"title": "Deploying",
|
|
40
|
+
"description": "What to ship, and the two settings that fail quietly."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"slug": "domains",
|
|
44
|
+
"title": "Domains and subdomains",
|
|
45
|
+
"description": "A host as a route segment — admin.example.com reaches app/admin, a tenant's host binds [domain] — with nothing to rewrite."
|
|
46
|
+
},
|
|
27
47
|
{
|
|
28
48
|
"slug": "edge-caching",
|
|
29
49
|
"title": "Serving shells from a CDN",
|
|
@@ -59,6 +79,11 @@
|
|
|
59
79
|
"title": "Getting started",
|
|
60
80
|
"description": "Serve React Server Components from any JavaScript backend."
|
|
61
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"slug": "go",
|
|
84
|
+
"title": "Go",
|
|
85
|
+
"description": "A Go process behind the renderer — functions, guards and actions in Go."
|
|
86
|
+
},
|
|
62
87
|
{
|
|
63
88
|
"slug": "images",
|
|
64
89
|
"title": "Images",
|
|
@@ -69,11 +94,21 @@
|
|
|
69
94
|
"title": "Installation",
|
|
70
95
|
"description": "From an empty directory to a streaming RSC app."
|
|
71
96
|
},
|
|
97
|
+
{
|
|
98
|
+
"slug": "instrumentation",
|
|
99
|
+
"title": "Startup",
|
|
100
|
+
"description": "instrumentation.ts runs once, before anything else."
|
|
101
|
+
},
|
|
72
102
|
{
|
|
73
103
|
"slug": "introduction",
|
|
74
104
|
"title": "Introduction",
|
|
75
105
|
"description": "React Server Components as a Vite plugin, deployed wherever you like."
|
|
76
106
|
},
|
|
107
|
+
{
|
|
108
|
+
"slug": "laravel",
|
|
109
|
+
"title": "Laravel",
|
|
110
|
+
"description": "React Server Components in front, a Laravel application behind them."
|
|
111
|
+
},
|
|
77
112
|
{
|
|
78
113
|
"slug": "mcp",
|
|
79
114
|
"title": "Working with an AI agent",
|
|
@@ -193,5 +228,15 @@
|
|
|
193
228
|
"slug": "view-transitions",
|
|
194
229
|
"title": "View transitions",
|
|
195
230
|
"description": "What React's ViewTransition animates in an app built with this, and what it does not."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"slug": "where-it-runs",
|
|
234
|
+
"title": "Where it runs",
|
|
235
|
+
"description": "A host is a Nitro preset, not a server you write."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"slug": "your-own-backend",
|
|
239
|
+
"title": "Your own backend",
|
|
240
|
+
"description": "The one endpoint a backend in any language answers."
|
|
196
241
|
}
|
|
197
242
|
]
|
package/guides/installation.md
CHANGED
|
@@ -32,8 +32,9 @@ The rest of this page is for adding rsc-kit to an app you already have.
|
|
|
32
32
|
<PackageManagers pkg="vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom" dev />
|
|
33
33
|
|
|
34
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
|
|
36
|
-
needs, not ones this package pins for you.
|
|
35
|
+
runtime. React 19.2 or later and Vite 8 are peer dependencies — the versions
|
|
36
|
+
the RSC build needs, not ones this package pins for you. 19.2 is where
|
|
37
|
+
`useEffectEvent` arrived, which the live-data hooks are built on.
|
|
37
38
|
|
|
38
39
|
<Aside type="note" title="What @vitejs/plugin-react is for">
|
|
39
40
|
Fast Refresh: edit a client component and React keeps its state, instead of
|
|
@@ -47,6 +48,7 @@ import { defineConfig } from "vite";
|
|
|
47
48
|
import react from "@vitejs/plugin-react";
|
|
48
49
|
import { nitro } from "nitro/vite";
|
|
49
50
|
import { rscKit } from "@rsc-kit/core/vite";
|
|
51
|
+
import { fileURLToPath } from "node:url";
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
54
|
* The full route tree, built the way every scaffolded app is built.
|
|
@@ -64,6 +66,13 @@ export default defineConfig({
|
|
|
64
66
|
}),
|
|
65
67
|
react(),
|
|
66
68
|
],
|
|
69
|
+
// `@/thing` for `src/thing`, the same alias tsconfig.json declares in
|
|
70
|
+
// `paths`: both, or the import type-checks and then fails to resolve.
|
|
71
|
+
resolve: {
|
|
72
|
+
alias: {
|
|
73
|
+
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
67
76
|
});
|
|
68
77
|
```
|
|
69
78
|
|
|
@@ -159,14 +168,20 @@ runs](/hosts/where-it-runs).
|
|
|
159
168
|
{
|
|
160
169
|
"type": "module",
|
|
161
170
|
"scripts": {
|
|
162
|
-
"dev": "vite",
|
|
163
|
-
"build": "vite build",
|
|
171
|
+
"dev": "bun --bun vite",
|
|
172
|
+
"build": "bun --bun vite build",
|
|
164
173
|
"start": "bun .output/server/index.mjs",
|
|
165
|
-
"compile": "bun build --compile .output/server/index.mjs --outfile
|
|
174
|
+
"compile": "bun --bun vite build && bun build --compile .output/server/index.mjs --outfile dist/app"
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
```
|
|
169
178
|
|
|
179
|
+
`bun --bun` runs Vite on Bun's runtime. The `vite` bin has a Node shebang, so
|
|
180
|
+
`bun run dev` alone would start the dev server, the build and the prerender
|
|
181
|
+
under Node — and a project importing `bun` or `bun:sqlite` fails at the first
|
|
182
|
+
render. On Node the scripts are plain `vite` and `vite build`;
|
|
183
|
+
[Running on Bun](/hosts/bun) has the rest.
|
|
184
|
+
|
|
170
185
|
`build` freezes every page it can, so there is no separate prerender step and
|
|
171
186
|
no command for one — freezing runs the app, which needs the bundle the build
|
|
172
187
|
just wrote, and only the build knows where that is. A page that must render
|
|
@@ -210,15 +225,18 @@ not match its `[param]` segment is a compile error rather than a blank page.
|
|
|
210
225
|
"target": "ESNext",
|
|
211
226
|
"module": "ESNext",
|
|
212
227
|
"moduleResolution": "bundler",
|
|
228
|
+
"paths": {
|
|
229
|
+
"@/*": ["./src/*"]
|
|
230
|
+
},
|
|
213
231
|
"jsx": "react-jsx",
|
|
214
232
|
"strict": true,
|
|
215
233
|
"noEmit": true,
|
|
234
|
+
"isolatedModules": true,
|
|
235
|
+
"moduleDetection": "force",
|
|
236
|
+
"verbatimModuleSyntax": true,
|
|
216
237
|
"skipLibCheck": true,
|
|
217
238
|
"resolveJsonModule": true,
|
|
218
|
-
"types": [
|
|
219
|
-
"@types/bun",
|
|
220
|
-
"vite/client"
|
|
221
|
-
]
|
|
239
|
+
"types": ["@types/bun", "vite/client"]
|
|
222
240
|
},
|
|
223
241
|
"include": ["src/**/*", "server/**/*", ".rsc-kit/**/*", "vite.config.ts"]
|
|
224
242
|
}
|
|
@@ -228,6 +246,14 @@ not match its `[param]` segment is a compile error rather than a blank page.
|
|
|
228
246
|
of a stylesheet (`import './styles.css'` in the root layout) is an error, and
|
|
229
247
|
`import.meta.env` is untyped.
|
|
230
248
|
|
|
249
|
+
Three of those are worth keeping even if you trim the rest.
|
|
250
|
+
`verbatimModuleSyntax` makes a type-only import say so, which is what keeps a
|
|
251
|
+
`type` import of a client component from pulling the component into the server
|
|
252
|
+
bundle. `isolatedModules` and `moduleDetection` are what a per-file transpiler
|
|
253
|
+
like Vite's assumes. And `paths` pairs with `resolve.alias` in `vite.config.ts`
|
|
254
|
+
— `@/thing` for `src/thing` — which some tools (shadcn's `init`, for one) check
|
|
255
|
+
for before they will run.
|
|
256
|
+
|
|
231
257
|
### Environment variables
|
|
232
258
|
|
|
233
259
|
There is nothing to install and nothing this package adds — Vite already owns
|
|
@@ -240,6 +266,18 @@ server, read through `process.env`.
|
|
|
240
266
|
The prefix is the whole boundary, so never put a secret behind it.
|
|
241
267
|
`VITE_STRIPE_KEY` is a published key.
|
|
242
268
|
|
|
269
|
+
And one line never to write into a `.env`: `NODE_ENV`. Vite sets it itself —
|
|
270
|
+
`development` under `vite`, `production` under `vite build` — and honours a
|
|
271
|
+
`.env` that sets it, so `NODE_ENV=development` in `.env` turns `vite build`
|
|
272
|
+
into a development build: the pages compile against React's development JSX
|
|
273
|
+
runtime, the server bundles carry React's production build, and every route
|
|
274
|
+
fails to render with React's opaque "message omitted in production builds".
|
|
275
|
+
The build refuses that up front and names the file and line, because a
|
|
276
|
+
plugin cannot override it — Vite applies the `.env` value after plugins have
|
|
277
|
+
run. If another tool in the repository wants the line, keep it in that
|
|
278
|
+
tool's own `.env`, not the app's; the env schema still validates
|
|
279
|
+
`NODE_ENV` from what the runtime sets.
|
|
280
|
+
|
|
243
281
|
Declare the ones you use, and Vite types them:
|
|
244
282
|
|
|
245
283
|
```ts title="src/env.d.ts"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Startup
|
|
2
|
+
|
|
3
|
+
> instrumentation.ts runs once, before anything else.
|
|
4
|
+
|
|
5
|
+
Some setup belongs to the process, not to a page: validating the
|
|
6
|
+
environment, configuring a shared package, warming a connection. Put it in
|
|
7
|
+
`src/instrumentation.ts` and it runs once — before any page module
|
|
8
|
+
evaluates, and before the first request.
|
|
9
|
+
|
|
10
|
+
```ts title="src/instrumentation.ts"
|
|
11
|
+
// Runs at import, before any page: a bad variable stops the server here.
|
|
12
|
+
import './env'
|
|
13
|
+
|
|
14
|
+
// Anything asynchronous the app needs before it serves. The first render
|
|
15
|
+
// waits for it.
|
|
16
|
+
export async function register() {
|
|
17
|
+
await db.connect()
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The name is Next's, so a reader arriving from there knows what it is. Both
|
|
22
|
+
halves are optional: a file with only imports is a bootstrap, a file with
|
|
23
|
+
only `register()` is a hook.
|
|
24
|
+
|
|
25
|
+
## Why it is a framework file
|
|
26
|
+
|
|
27
|
+
Two things make this a file the framework has to know about rather than a
|
|
28
|
+
module the app imports.
|
|
29
|
+
|
|
30
|
+
**Import order.** A page that configures a shared package at import time —
|
|
31
|
+
a logger, an ORM, a metrics client — runs before any module the app could
|
|
32
|
+
put in front of it, because the generated entry imports the pages. The entry
|
|
33
|
+
imports `instrumentation.ts` *first*, so whatever it sets up is set up when
|
|
34
|
+
the first page module evaluates. Importing a bootstrap module from every
|
|
35
|
+
file that needs it works until someone forgets, and the failure is a page
|
|
36
|
+
that throws "not configured" for the one visitor who reached it first.
|
|
37
|
+
|
|
38
|
+
**Before the first request.** `register()` is awaited by every entry point
|
|
39
|
+
a render can come through — the built server, the dev server, the
|
|
40
|
+
prerender, a route's middleware check, a server action, an api route — so
|
|
41
|
+
"before the first render" holds without the app knowing the list.
|
|
42
|
+
|
|
43
|
+
## When it runs
|
|
44
|
+
|
|
45
|
+
| where | when |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| a server (`bun`, `node`, any long-lived preset) | at startup, before the server reports itself up |
|
|
48
|
+
| a Worker | at the isolate's first request — there is no startup, and a binding is only readable once a request has arrived |
|
|
49
|
+
| `vite dev` | when the dev server first evaluates the app, and again when the file or anything it imports changes |
|
|
50
|
+
| `vite build` | before the first page is prerendered |
|
|
51
|
+
|
|
52
|
+
Once per process. A `register()` that resolved is not called again; one
|
|
53
|
+
that rejected is retried by the next request, so a database that was not up
|
|
54
|
+
yet is asked again rather than leaving the process permanently refusing.
|
|
55
|
+
|
|
56
|
+
On a server, a failure at startup — the file throwing at import, or
|
|
57
|
+
`register()` rejecting — is reported and the process exits. A server that
|
|
58
|
+
could not bootstrap has nothing correct to serve, and a health check that
|
|
59
|
+
passed on a server about to fail its first visitor is worse than one that
|
|
60
|
+
never passed. The dev server stays up and reports the error on the page.
|
|
61
|
+
|
|
62
|
+
## Environment validation
|
|
63
|
+
|
|
64
|
+
The scaffold writes this file when the project validates its environment,
|
|
65
|
+
and the only line it needs is the import: `src/env.ts` refuses at import, and
|
|
66
|
+
importing it here is what makes a missing variable the server's failure at
|
|
67
|
+
startup rather than a visitor's three calls later.
|
|
68
|
+
|
|
69
|
+
The build prerenders pages, so it runs the bootstrap too. A build machine
|
|
70
|
+
without the production variables sets `SKIP_ENV_VALIDATION=1`, which the
|
|
71
|
+
generated schema honours; the server that runs the build validates at
|
|
72
|
+
startup regardless.
|
|
73
|
+
|
|
74
|
+
## On a Worker
|
|
75
|
+
|
|
76
|
+
Read bindings inside `register()`, not at the top of the module. A Worker
|
|
77
|
+
evaluates the module before any request exists, and `process.env` is empty
|
|
78
|
+
until Nitro maps the first request's bindings onto it — so a top-level
|
|
79
|
+
`process.env.DATABASE_URL` is `undefined` at exactly the moment it looks
|
|
80
|
+
like it should not be. Inside `register()`, which runs at the first request,
|
|
81
|
+
it is there.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
export async function register() {
|
|
85
|
+
const url = process.env.DATABASE_URL // readable here on every host
|
|
86
|
+
await db.connect(url)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The same file works unchanged on a server, where both moments have the
|
|
91
|
+
environment.
|
package/guides/introduction.md
CHANGED
|
@@ -96,6 +96,10 @@ Swap the preset for `node`, `cloudflare_module`, `vercel`, `netlify` or `deno`
|
|
|
96
96
|
and the same app deploys there instead. The build produces a `.output`
|
|
97
97
|
directory; nothing else changes.
|
|
98
98
|
|
|
99
|
+
With a backend behind it — Laravel, Go, anything answering one endpoint — the
|
|
100
|
+
app is a **BAP**, [Backend-Answered Pages](/hosts/backend-answered-pages):
|
|
101
|
+
the page is rendered in front of the backend rather than by it.
|
|
102
|
+
|
|
99
103
|
## What it is not
|
|
100
104
|
|
|
101
105
|
<Aside type="note" title="No data layer, no CSS pipeline, no dev server of its own">
|