@rsc-kit/mcp 0.18.1 → 0.20.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/recipes.js +242 -17
- package/dist/recipes.js.map +1 -1
- package/guides/api-routes.md +22 -5
- package/guides/authorization.md +22 -2
- package/guides/backend-answered-pages.md +27 -3
- package/guides/coming-from-next.md +17 -3
- package/guides/deployment.md +25 -0
- package/guides/emails.md +13 -1
- package/guides/feature-flags.md +63 -0
- package/guides/fonts.md +25 -0
- package/guides/forms.md +103 -0
- package/guides/index.json +10 -0
- package/guides/installation.md +15 -9
- package/guides/laravel.md +27 -2
- package/guides/offline.md +9 -4
- package/guides/openapi.md +99 -0
- package/guides/quick-start.md +3 -1
- package/guides/redirects.md +19 -4
- package/guides/routing.md +4 -4
- package/guides/server-actions.md +51 -7
- package/guides/testing.md +22 -1
- package/guides/typed-routes.md +15 -9
- package/guides/where-it-runs.md +74 -9
- package/guides/your-own-backend.md +28 -7
- package/package.json +1 -1
package/guides/server-actions.md
CHANGED
|
@@ -84,6 +84,12 @@ server with it.
|
|
|
84
84
|
Files travel this way too, without any encoding of your own — see
|
|
85
85
|
[File uploads](/guides/file-uploads).
|
|
86
86
|
|
|
87
|
+
The id is a hash of the module and the export in a build —
|
|
88
|
+
`9396f92f746f#createOrder` — and nothing about the file is in it. In
|
|
89
|
+
development it is the module's path as Vite serves it
|
|
90
|
+
(`/@fs/Users/…/actions.ts#createOrder`), which is what a
|
|
91
|
+
`$ACTION_ID_…` hidden input in a form shows there; a build never carries it.
|
|
92
|
+
|
|
87
93
|
## Returning UI
|
|
88
94
|
|
|
89
95
|
Step 5 is the same serialiser a page goes through, so an action can answer
|
|
@@ -269,11 +275,18 @@ forgotten `next()` would otherwise look exactly like a check that passed.
|
|
|
269
275
|
```ts
|
|
270
276
|
const result = await createPost({ title: 'x', body: 'y' });
|
|
271
277
|
|
|
272
|
-
result
|
|
273
|
-
result
|
|
274
|
-
result
|
|
278
|
+
result?.data // what the handler returned
|
|
279
|
+
result?.validationErrors // { title: ['Too short'] }
|
|
280
|
+
result?.serverError // 'Something went wrong.'
|
|
275
281
|
```
|
|
276
282
|
|
|
283
|
+
The `?.` is the type's, not decoration: an action that `redirect()`s
|
|
284
|
+
resolves with `undefined` on the client — the page is on its way elsewhere —
|
|
285
|
+
and `result.serverError` on that is a `TypeError` inside a transition, which
|
|
286
|
+
unmounts the root. The type is `Promise<ActionResult<Data> | undefined>`,
|
|
287
|
+
so the unasked read does not compile. In a test, where nothing redirects,
|
|
288
|
+
`result!` is fine.
|
|
289
|
+
|
|
277
290
|
Returned rather than thrown, and that is not a style choice: React serialises a
|
|
278
291
|
rejected server action opaquely — production strips the message and leaves a
|
|
279
292
|
digest — so a thrown validation error reaches the browser as "an error
|
|
@@ -357,6 +370,37 @@ one can be wired in without this package having an opinion about which. See
|
|
|
357
370
|
[Writing to the response](/guides/authorization#writing-to-the-response) for
|
|
358
371
|
where else it works, and where it does not.
|
|
359
372
|
|
|
373
|
+
## Work the visitor should not wait for
|
|
374
|
+
|
|
375
|
+
An audit row, a welcome email, a cache warm: the action's answer should not
|
|
376
|
+
wait for it, and it must still finish. `after()` queues work to run once the
|
|
377
|
+
answer is on its way:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
'use server';
|
|
381
|
+
|
|
382
|
+
import { after } from '@rsc-kit/core/request';
|
|
383
|
+
|
|
384
|
+
export async function signup(formData: FormData) {
|
|
385
|
+
const user = await createUser(formData);
|
|
386
|
+
|
|
387
|
+
after(() => sendWelcomeEmail(user));
|
|
388
|
+
after(() => audit('signup', user.id));
|
|
389
|
+
|
|
390
|
+
redirect('/welcome');
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Why not a detached promise: on a long-lived process it happens to run to
|
|
395
|
+
completion; on a Worker the isolate is torn down when the response ends
|
|
396
|
+
unless the work was handed to the platform's `waitUntil`, so a promise nobody
|
|
397
|
+
awaited dies silently — some of the time, which is the worst way. `after()`
|
|
398
|
+
is the same call on every host: handed to `waitUntil` where one exists, kept
|
|
399
|
+
by the process elsewhere. A rejection is reported to the log and never
|
|
400
|
+
reaches the response, which has already gone. It works from a component, a
|
|
401
|
+
`middleware.ts`, an api route and an action alike; outside a request — a
|
|
402
|
+
build — the work simply runs.
|
|
403
|
+
|
|
360
404
|
## Two at once
|
|
361
405
|
|
|
362
406
|
Nothing queues them. Each call is an ordinary `fetch`, so two submits fired
|
|
@@ -409,11 +453,11 @@ double-click sends two requests unless you disable the button.
|
|
|
409
453
|
An action that fails does not answer with a Flight stream, so the client turns
|
|
410
454
|
the response into an error before the decoder ever sees it:
|
|
411
455
|
|
|
412
|
-
| Response | What the client
|
|
456
|
+
| Response | What the client does | What you do |
|
|
413
457
|
| --- | --- | --- |
|
|
414
|
-
| `X-RSC-Redirect` header | `
|
|
415
|
-
| `422` | `ServerValidationError`, carrying `errors` | Show the messages. `<Form>` does it for you. |
|
|
416
|
-
| Any other failure | `Error`, naming the status | Whatever the app needs. |
|
|
458
|
+
| `X-RSC-Redirect` header | Starts the navigation and **resolves** with `{ redirected }` | Nothing — the page is on its way to the location, and there is nothing to catch. |
|
|
459
|
+
| `422` | Throws `ServerValidationError`, carrying `errors` | Show the messages. `<Form>` does it for you. |
|
|
460
|
+
| Any other failure | Throws `Error`, naming the status | Whatever the app needs. |
|
|
417
461
|
|
|
418
462
|
```tsx
|
|
419
463
|
"use client";
|
package/guides/testing.md
CHANGED
|
@@ -54,6 +54,24 @@ there is the round trip. Here the function *is* the interesting part — the
|
|
|
54
54
|
validation, the middleware, the authorisation — and it is a unit test.
|
|
55
55
|
:::
|
|
56
56
|
|
|
57
|
+
One line a port brings needs a stub: `import 'server-only'`. Keep it — the
|
|
58
|
+
build honours it, and a client file that imports the module fails to build
|
|
59
|
+
rather than shipping a secret. But under Vite the package resolves to an
|
|
60
|
+
empty module on the server, and under `bun test` it is the real package,
|
|
61
|
+
which throws on import; an action file that carries the line is not callable
|
|
62
|
+
in a unit test until a preload stubs it. The scaffold ships the two files:
|
|
63
|
+
|
|
64
|
+
```toml title="bunfig.toml"
|
|
65
|
+
[test]
|
|
66
|
+
preload = ["./tests/preload.ts"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ts title="tests/preload.ts"
|
|
70
|
+
import { mock } from 'bun:test'
|
|
71
|
+
|
|
72
|
+
mock.module('server-only', () => ({}))
|
|
73
|
+
```
|
|
74
|
+
|
|
57
75
|
### Reading the request
|
|
58
76
|
|
|
59
77
|
`cookies()`, `headers()` and the rest read from a scope the host opens per
|
|
@@ -120,7 +138,10 @@ that never ran, a stored page that should not have been, a `404` that came back
|
|
|
120
138
|
otherwise — the first run pays for it, the rest do not, and an edit is picked
|
|
121
139
|
up. It runs your own `build` script — `bun run build` under Bun, `npm run
|
|
122
140
|
build` under Node — so what is tested is what ships, on the runtime it ships
|
|
123
|
-
on.
|
|
141
|
+
on. Files the build wrote to `.output/public` — the hashed assets, `sw.js`,
|
|
142
|
+
`manifest.webmanifest`, the icons — are answered too, the way Nitro's static
|
|
143
|
+
layer answers them in production, so a test can prove a page's links resolve
|
|
144
|
+
and not only that the page renders. Pass
|
|
124
145
|
`{ build: false }` in a ci step that already built.
|
|
125
146
|
|
|
126
147
|
One build and one loaded module per test run, shared across files. That is both
|
package/guides/typed-routes.md
CHANGED
|
@@ -55,7 +55,7 @@ export const searchParams = z.object({
|
|
|
55
55
|
A key the page requires is required on the link — a `q: z.string()` with no
|
|
56
56
|
default makes `search` itself required, so the page's error boundary is not
|
|
57
57
|
where a missing `q` is found. A page with no schema takes any scalars, and so
|
|
58
|
-
does an href that is not one route (`path as
|
|
58
|
+
does an href that is not one route (`path as Route`), because there is nothing
|
|
59
59
|
to check it against.
|
|
60
60
|
|
|
61
61
|
Values are typed by what the page will **see**, not what the schema accepts:
|
|
@@ -88,7 +88,7 @@ so `/posts/a/b` type-checks even though it does not match at runtime.
|
|
|
88
88
|
const nav = [
|
|
89
89
|
{ href: '/', label: 'Home' },
|
|
90
90
|
{ href: '/about', label: 'About' },
|
|
91
|
-
] satisfies { href:
|
|
91
|
+
] satisfies { href: Route; label: string }[]
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
Without `satisfies`, TypeScript infers `string` for `href` and you lose the
|
|
@@ -117,8 +117,17 @@ middleware — so typing it would make the common case a cast.
|
|
|
117
117
|
|
|
118
118
|
## Api routes
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
A `route.ts` is a `Route` too — `<Link href="/logout">`, `visit('/agent-account')`
|
|
121
|
+
and `redirect('/files/export.csv')` all typecheck, as they do in Next. What
|
|
122
|
+
differs is what the browser does with one: a route answers with a `Response`,
|
|
123
|
+
not a page, so the client treats a link to it as the anchor it is — never
|
|
124
|
+
prefetched (a hover must not sign someone out) and a full navigation rather
|
|
125
|
+
than a payload fetch. The build hands the client every route's pattern for
|
|
126
|
+
that.
|
|
127
|
+
|
|
128
|
+
Every build also writes the `route.ts` files in their own narrower union,
|
|
129
|
+
`ApiRoute`, so a `fetch` to an endpoint that no longer exists stops
|
|
130
|
+
compiling:
|
|
122
131
|
|
|
123
132
|
```ts
|
|
124
133
|
import { apiUrl } from '@rsc-kit/core/routes'
|
|
@@ -131,11 +140,8 @@ await fetch(apiUrl('/api/ordrs')) // does not compile
|
|
|
131
140
|
`string`, so without somewhere to put the type there is nothing to check
|
|
132
141
|
against — the function is the place.
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
a json document; and `apiUrl('/orders')` does not compile either, because
|
|
137
|
-
fetching a page gets html where json was expected. Each refuses the other's
|
|
138
|
-
urls, which is the pair of mistakes worth catching.
|
|
143
|
+
`apiUrl('/orders')` does not compile, because fetching a page gets html where
|
|
144
|
+
json was expected — the one mistake the narrower union is for.
|
|
139
145
|
|
|
140
146
|
:::note[Paths, not response types]
|
|
141
147
|
This checks the **url**. It does not infer what the endpoint returns — that
|
package/guides/where-it-runs.md
CHANGED
|
@@ -62,13 +62,21 @@ npm run deploy # nitro deploy --prebuilt
|
|
|
62
62
|
## Compiling to a single binary
|
|
63
63
|
|
|
64
64
|
Bun only, and the whole application ends up inside one file — engine, route
|
|
65
|
-
tree and assets:
|
|
65
|
+
tree, frozen pages and assets, the precompressed variants included:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
npm run compile # builds, then bun build --compile
|
|
68
|
+
npm run compile # builds, then bun build --compile .output/server/compile.mjs
|
|
69
69
|
./dist/app
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
`compile.mjs` is written by the build beside the server. It is what puts the
|
|
73
|
+
frozen pages inside the binary: the server reads them through a computed
|
|
74
|
+
import, which a compile cannot see, and this entry imports them by name and
|
|
75
|
+
hands them over before starting the server. Compile `index.mjs` instead and
|
|
76
|
+
the binary still works — it renders those pages live. Nothing else has to
|
|
77
|
+
travel with the binary: not `.output/public`, not `.output/server`. A
|
|
78
|
+
`Dockerfile` copies `dist/app` and runs it.
|
|
79
|
+
|
|
72
80
|
It builds first on purpose. Compiling whatever `.output` happens to hold means
|
|
73
81
|
a binary one version behind the source with nothing to say so — and on a
|
|
74
82
|
project that has never been built, an `ENOENT` naming a path the app did not
|
|
@@ -84,13 +92,6 @@ write.
|
|
|
84
92
|
worth knowing on a site with hundreds of frozen pages.
|
|
85
93
|
</Aside>
|
|
86
94
|
|
|
87
|
-
<Aside type="note" title="Frozen pages stay outside the binary">
|
|
88
|
-
The build freezes pages into `.output/server/rsc-static` and the server reads
|
|
89
|
-
them from there. A compiled binary has no filesystem to read — the directory
|
|
90
|
-
is not embedded — so it renders those pages live instead. Everything still
|
|
91
|
-
answers; what you lose is the stored render, not the page.
|
|
92
|
-
</Aside>
|
|
93
|
-
|
|
94
95
|
<Aside type="caution" title="serveStatic: 'inline' is what makes this work">
|
|
95
96
|
Without it the binary compiles, starts, serves pages, and 404s every asset.
|
|
96
97
|
Inside a compiled binary the static path resolves into Bun's virtual
|
|
@@ -113,6 +114,48 @@ the list:
|
|
|
113
114
|
rscKit({ serverExternalPackages: ['@acme/native-thing'] })
|
|
114
115
|
```
|
|
115
116
|
|
|
117
|
+
## A polyfill runs first
|
|
118
|
+
|
|
119
|
+
A dependency that checks for a polyfill at module evaluation — `tsyringe`
|
|
120
|
+
wants `Reflect.getMetadata`, under `@peculiar/x509`, under
|
|
121
|
+
`@simplewebauthn/server` — depends on `import 'reflect-metadata'` running
|
|
122
|
+
before it, and the source has it there. A bundler does not keep that place:
|
|
123
|
+
it emits a chunk's imports before its external ones, whatever the source
|
|
124
|
+
said, so the check ran first — survivable by luck as a directory, fatal
|
|
125
|
+
compiled into a binary. When the project's graph has `reflect-metadata`
|
|
126
|
+
anywhere in it, the build loads it in a Nitro plugin, which the server's
|
|
127
|
+
entry evaluates before any of the app is imported; a project without it
|
|
128
|
+
has nothing that checks. Nothing to add to `instrumentation.ts`.
|
|
129
|
+
|
|
130
|
+
## A dependency's `"use client"` is read
|
|
131
|
+
|
|
132
|
+
The other direction. A dependency is bundled into the server graphs — and
|
|
133
|
+
so has its `"use client"` directives read — when it declares `react` as a
|
|
134
|
+
peer dependency, which is what a React library does. One that imports React
|
|
135
|
+
and never says so (a generated component wrapper, a workspace package with
|
|
136
|
+
`react` under `dependencies`, a package whose author forgot) would be left
|
|
137
|
+
external, and its directive is then a string nobody reads: the server loads
|
|
138
|
+
it as a server module and the first `useState` in it fails at render, with an
|
|
139
|
+
error that names React and not the package.
|
|
140
|
+
|
|
141
|
+
So the build reads the direct dependencies once, and bundles any that carry
|
|
142
|
+
a `"use client"` file and plugin-rsc would not have. It says so:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
[rsc-kit] bundling @acme/chat-widget: it has "use client" files but does not
|
|
146
|
+
declare react as a peer dependency, so its components would otherwise run
|
|
147
|
+
on the server.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Nothing to configure; the line is there so the package's author hears about
|
|
151
|
+
the missing peer. The React-using dependencies under such a package — the
|
|
152
|
+
runtime a generated wrapper calls into — are bundled with it, so there is
|
|
153
|
+
one React for all of them: left external, that runtime would load React
|
|
154
|
+
through Node while everything else got it through Vite, and the hooks
|
|
155
|
+
dispatcher is null in one of the two. Only direct dependencies are read for
|
|
156
|
+
the directive itself — a directive two levels down is the concern of the
|
|
157
|
+
package between, which declared it.
|
|
158
|
+
|
|
116
159
|
## Offline
|
|
117
160
|
|
|
118
161
|
`rscKit({ offline: true })` writes a service worker into `.output/public`
|
|
@@ -147,6 +190,28 @@ Assets live in `.output/public` and are served by the same process that serves
|
|
|
147
190
|
your pages. If you want nginx or a CDN serving them instead, point it at
|
|
148
191
|
`.output/public` — that directory is the deployment.
|
|
149
192
|
|
|
193
|
+
## Nothing is sent raw
|
|
194
|
+
|
|
195
|
+
A bun or node server answering the internet by itself compresses what it
|
|
196
|
+
sends, because nothing in front of it will. The build writes a `.br` and a
|
|
197
|
+
`.gz` beside every public asset larger than a kilobyte, and Nitro serves
|
|
198
|
+
whichever the request accepts — a 147 kB stylesheet is 18 kB on the wire.
|
|
199
|
+
The host gzips what it answers itself — documents, streamed pages, RSC
|
|
200
|
+
payloads, stored pages, api routes — for a request that accepts it, flushing
|
|
201
|
+
every chunk so a streamed shell still reaches the browser before the holes
|
|
202
|
+
fill; a stored page is compressed once and kept.
|
|
203
|
+
|
|
204
|
+
A port measured what this is worth: with every byte raw, first paint on a
|
|
205
|
+
throttled phone was 3.8 s where 1.2 s was the baseline. That was the whole
|
|
206
|
+
regression.
|
|
207
|
+
|
|
208
|
+
On a Worker the platform compresses and the host does nothing. Behind a
|
|
209
|
+
CDN or nginx that compresses, the proxy sees an already-encoded answer and
|
|
210
|
+
passes it through. A deployment that would rather its proxy did all of it
|
|
211
|
+
turns the host's half off with `compress: false` on the handler, and Nitro's
|
|
212
|
+
with `compressPublicAssets: false` in its config; `Cache-Control:
|
|
213
|
+
no-transform` on an answer leaves that answer alone.
|
|
214
|
+
|
|
150
215
|
---
|
|
151
216
|
|
|
152
217
|
Next: [Deploying →](/hosts/deployment)
|
|
@@ -125,19 +125,40 @@ rather than one each:
|
|
|
125
125
|
{ "calls": [ { "function": "Orders.recent", "args": [5] }, { "function": "Me.profile", "args": [] } ] }
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
Answer
|
|
129
|
-
|
|
128
|
+
Answer **as each call finishes**: `Content-Type: application/x-ndjson`, one
|
|
129
|
+
JSON line per call, carrying its `index` in the batch, the `status` it would
|
|
130
|
+
have had alone, and the reply — in whatever order the calls complete, flushed
|
|
131
|
+
as they do:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
{ "index": 1, "status": 401, "unauthenticated": true }
|
|
135
|
+
{ "index": 0, "status": 200, "result": [ … ] }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
That is what keeps a page's boundaries streaming independently after their
|
|
139
|
+
reads travelled together: the renderer resolves each call the moment its
|
|
140
|
+
line lands, so a component waiting on a fast read paints while a slow
|
|
141
|
+
sibling's is still running. The Go module runs the calls concurrently and
|
|
142
|
+
writes each as it returns; Laravel runs them in order and flushes after each.
|
|
143
|
+
Set `X-Accel-Buffering: no` so a proxy in front does not hold the lines back.
|
|
144
|
+
|
|
145
|
+
A backend that would rather answer the whole batch at once may: one JSON
|
|
146
|
+
object of `replies`, one per call in order, each with its `status`:
|
|
130
147
|
|
|
131
148
|
```json
|
|
132
149
|
{ "replies": [ { "status": 200, "result": [ … ] }, { "status": 401, "unauthenticated": true } ] }
|
|
133
150
|
```
|
|
134
151
|
|
|
135
|
-
|
|
152
|
+
The renderer reads either. The saving of the batch is the same; with the
|
|
153
|
+
whole-batch form every call in it waits for the slowest.
|
|
154
|
+
|
|
155
|
+
Either way, run every call and answer every one — a refusal in the second is
|
|
136
156
|
that call's answer, not a reason to leave the third out. Each call's
|
|
137
|
-
`revalidate` stays with that call. A backend that has not implemented
|
|
138
|
-
loses nothing but the saving: the renderer reads its "no
|
|
139
|
-
as "no batches here" and sends single calls from then
|
|
140
|
-
visitors; every call in one carried the same forwarded
|
|
157
|
+
`revalidate` stays with that call. A backend that has not implemented
|
|
158
|
+
batches at all loses nothing but the saving: the renderer reads its "no
|
|
159
|
+
function name" answer as "no batches here" and sends single calls from then
|
|
160
|
+
on. Batches never mix visitors; every call in one carried the same forwarded
|
|
161
|
+
headers.
|
|
141
162
|
|
|
142
163
|
## Route middleware
|
|
143
164
|
|
package/package.json
CHANGED