@rsc-kit/mcp 0.16.3 → 0.18.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.d.ts +14 -1
- package/dist/answers.js +76 -34
- package/dist/answers.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +434 -14
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +12 -0
- package/dist/report.js +17 -15
- package/dist/report.js.map +1 -1
- package/guides/api-routes.md +106 -6
- package/guides/authorization.md +39 -0
- package/guides/backend-answered-pages.md +163 -0
- package/guides/coming-from-next.md +12 -4
- package/guides/deployment.md +129 -0
- package/guides/domains.md +129 -0
- package/guides/emails.md +90 -0
- package/guides/errors.md +16 -2
- package/guides/go.md +194 -0
- package/guides/images.md +10 -4
- package/guides/index.json +50 -0
- package/guides/installation.md +31 -13
- package/guides/instrumentation.md +91 -0
- package/guides/introduction.md +4 -0
- package/guides/laravel.md +406 -0
- package/guides/mcp.md +1 -1
- package/guides/metadata.md +1 -1
- package/guides/offline.md +8 -0
- 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/routing.md +5 -1
- package/guides/seo-files.md +140 -0
- package/guides/typed-routes.md +26 -0
- package/guides/view-transitions.md +53 -7
- package/guides/where-it-runs.md +155 -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/images.md
CHANGED
|
@@ -62,12 +62,18 @@ build what resizing costs, once per image per width, and nothing at request
|
|
|
62
62
|
time. It is opt-in for that reason: a hundred hero images at four widths is a
|
|
63
63
|
noticeable build, and most of them belong on a CDN.
|
|
64
64
|
|
|
65
|
-
Declare the
|
|
65
|
+
Declare the queries so TypeScript stops asking — and the build's typecheck
|
|
66
|
+
lets them through:
|
|
66
67
|
|
|
67
68
|
```ts title="src/images.d.ts"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// A pattern may hold one `*`, so each matches on the tail of the query.
|
|
70
|
+
declare module '*&as=srcset' {
|
|
71
|
+
const srcset: string;
|
|
72
|
+
export default srcset;
|
|
73
|
+
}
|
|
74
|
+
declare module '*&format=webp' {
|
|
75
|
+
const url: string;
|
|
76
|
+
export default url;
|
|
71
77
|
}
|
|
72
78
|
```
|
|
73
79
|
|
package/guides/index.json
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
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
|
+
},
|
|
12
17
|
{
|
|
13
18
|
"slug": "caching",
|
|
14
19
|
"title": "Asking once per request",
|
|
@@ -24,11 +29,26 @@
|
|
|
24
29
|
"title": "Rendering per request",
|
|
25
30
|
"description": "Marking work that belongs to the visitor, not to the build."
|
|
26
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"slug": "deployment",
|
|
34
|
+
"title": "Deploying",
|
|
35
|
+
"description": "What to ship, and the two settings that fail quietly."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"slug": "domains",
|
|
39
|
+
"title": "Domains and subdomains",
|
|
40
|
+
"description": "A host as a route segment — admin.example.com reaches app/admin, a tenant's host binds [domain] — with nothing to rewrite."
|
|
41
|
+
},
|
|
27
42
|
{
|
|
28
43
|
"slug": "edge-caching",
|
|
29
44
|
"title": "Serving shells from a CDN",
|
|
30
45
|
"description": "Putting build-time shells on the edge, and what rsc-kit does not do."
|
|
31
46
|
},
|
|
47
|
+
{
|
|
48
|
+
"slug": "emails",
|
|
49
|
+
"title": "Emails and other HTML",
|
|
50
|
+
"description": "Rendering React to HTML on the server — an email, a PDF, a feed — from a server action or a route, with \"use ssr\"."
|
|
51
|
+
},
|
|
32
52
|
{
|
|
33
53
|
"slug": "errors",
|
|
34
54
|
"title": "Errors and 404s",
|
|
@@ -54,6 +74,11 @@
|
|
|
54
74
|
"title": "Getting started",
|
|
55
75
|
"description": "Serve React Server Components from any JavaScript backend."
|
|
56
76
|
},
|
|
77
|
+
{
|
|
78
|
+
"slug": "go",
|
|
79
|
+
"title": "Go",
|
|
80
|
+
"description": "A Go process behind the renderer — functions, guards and actions in Go."
|
|
81
|
+
},
|
|
57
82
|
{
|
|
58
83
|
"slug": "images",
|
|
59
84
|
"title": "Images",
|
|
@@ -64,11 +89,21 @@
|
|
|
64
89
|
"title": "Installation",
|
|
65
90
|
"description": "From an empty directory to a streaming RSC app."
|
|
66
91
|
},
|
|
92
|
+
{
|
|
93
|
+
"slug": "instrumentation",
|
|
94
|
+
"title": "Startup",
|
|
95
|
+
"description": "instrumentation.ts runs once, before anything else."
|
|
96
|
+
},
|
|
67
97
|
{
|
|
68
98
|
"slug": "introduction",
|
|
69
99
|
"title": "Introduction",
|
|
70
100
|
"description": "React Server Components as a Vite plugin, deployed wherever you like."
|
|
71
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"slug": "laravel",
|
|
104
|
+
"title": "Laravel",
|
|
105
|
+
"description": "React Server Components in front, a Laravel application behind them."
|
|
106
|
+
},
|
|
72
107
|
{
|
|
73
108
|
"slug": "mcp",
|
|
74
109
|
"title": "Working with an AI agent",
|
|
@@ -144,6 +179,11 @@
|
|
|
144
179
|
"title": "Sections",
|
|
145
180
|
"description": "Refreshing one region of a page without re-rendering the rest."
|
|
146
181
|
},
|
|
182
|
+
{
|
|
183
|
+
"slug": "seo-files",
|
|
184
|
+
"title": "robots, sitemap and llms.txt",
|
|
185
|
+
"description": "The files a site describes itself with, from a file beside the root layout — written the way Next writes them, stored at build when they can be."
|
|
186
|
+
},
|
|
147
187
|
{
|
|
148
188
|
"slug": "server-actions",
|
|
149
189
|
"title": "Server actions",
|
|
@@ -183,5 +223,15 @@
|
|
|
183
223
|
"slug": "view-transitions",
|
|
184
224
|
"title": "View transitions",
|
|
185
225
|
"description": "What React's ViewTransition animates in an app built with this, and what it does not."
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"slug": "where-it-runs",
|
|
229
|
+
"title": "Where it runs",
|
|
230
|
+
"description": "A host is a Nitro preset, not a server you write."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"slug": "your-own-backend",
|
|
234
|
+
"title": "Your own backend",
|
|
235
|
+
"description": "The one endpoint a backend in any language answers."
|
|
186
236
|
}
|
|
187
237
|
]
|
package/guides/installation.md
CHANGED
|
@@ -43,10 +43,11 @@ needs, not ones this package pins for you.
|
|
|
43
43
|
## Configure Vite
|
|
44
44
|
|
|
45
45
|
```ts title="vite.config.ts"
|
|
46
|
-
import { defineConfig } from
|
|
47
|
-
import react from
|
|
48
|
-
import { nitro } from
|
|
49
|
-
import { rscKit } from
|
|
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
|
+
import { fileURLToPath } from "node:url";
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* The full route tree, built the way every scaffolded app is built.
|
|
@@ -56,16 +57,22 @@ import { rscKit } from '@rsc-kit/core/vite'
|
|
|
56
57
|
*/
|
|
57
58
|
export default defineConfig({
|
|
58
59
|
plugins: [
|
|
59
|
-
nitro({ preset:
|
|
60
|
+
nitro({ preset: "bun", serveStatic: "inline" }),
|
|
60
61
|
rscKit({
|
|
61
|
-
sourceDir:
|
|
62
|
-
outDir:
|
|
63
|
-
viewTransitions: true,
|
|
62
|
+
sourceDir: "src",
|
|
63
|
+
outDir: "build",
|
|
64
64
|
offline: true,
|
|
65
65
|
}),
|
|
66
66
|
react(),
|
|
67
67
|
],
|
|
68
|
-
|
|
68
|
+
// `@/thing` for `src/thing`, the same alias tsconfig.json declares in
|
|
69
|
+
// `paths`: both, or the import type-checks and then fails to resolve.
|
|
70
|
+
resolve: {
|
|
71
|
+
alias: {
|
|
72
|
+
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
`sourceDir` is where your route tree lives. The defaults are plain Vite ones —
|
|
@@ -211,15 +218,18 @@ not match its `[param]` segment is a compile error rather than a blank page.
|
|
|
211
218
|
"target": "ESNext",
|
|
212
219
|
"module": "ESNext",
|
|
213
220
|
"moduleResolution": "bundler",
|
|
221
|
+
"paths": {
|
|
222
|
+
"@/*": ["./src/*"]
|
|
223
|
+
},
|
|
214
224
|
"jsx": "react-jsx",
|
|
215
225
|
"strict": true,
|
|
216
226
|
"noEmit": true,
|
|
227
|
+
"isolatedModules": true,
|
|
228
|
+
"moduleDetection": "force",
|
|
229
|
+
"verbatimModuleSyntax": true,
|
|
217
230
|
"skipLibCheck": true,
|
|
218
231
|
"resolveJsonModule": true,
|
|
219
|
-
"types": [
|
|
220
|
-
"@types/bun",
|
|
221
|
-
"vite/client"
|
|
222
|
-
]
|
|
232
|
+
"types": ["@types/bun", "vite/client"]
|
|
223
233
|
},
|
|
224
234
|
"include": ["src/**/*", "server/**/*", ".rsc-kit/**/*", "vite.config.ts"]
|
|
225
235
|
}
|
|
@@ -229,6 +239,14 @@ not match its `[param]` segment is a compile error rather than a blank page.
|
|
|
229
239
|
of a stylesheet (`import './styles.css'` in the root layout) is an error, and
|
|
230
240
|
`import.meta.env` is untyped.
|
|
231
241
|
|
|
242
|
+
Three of those are worth keeping even if you trim the rest.
|
|
243
|
+
`verbatimModuleSyntax` makes a type-only import say so, which is what keeps a
|
|
244
|
+
`type` import of a client component from pulling the component into the server
|
|
245
|
+
bundle. `isolatedModules` and `moduleDetection` are what a per-file transpiler
|
|
246
|
+
like Vite's assumes. And `paths` pairs with `resolve.alias` in `vite.config.ts`
|
|
247
|
+
— `@/thing` for `src/thing` — which some tools (shadcn's `init`, for one) check
|
|
248
|
+
for before they will run.
|
|
249
|
+
|
|
232
250
|
### Environment variables
|
|
233
251
|
|
|
234
252
|
There is nothing to install and nothing this package adds — Vite already owns
|
|
@@ -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">
|