@rsc-kit/mcp 0.18.1 → 0.19.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 +225 -15
- package/dist/recipes.js.map +1 -1
- package/guides/api-routes.md +22 -5
- package/guides/authorization.md +2 -2
- package/guides/backend-answered-pages.md +27 -3
- package/guides/coming-from-next.md +17 -3
- package/guides/deployment.md +10 -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 +14 -8
- package/guides/laravel.md +27 -2
- package/guides/offline.md +9 -4
- package/guides/openapi.md +99 -0
- package/guides/redirects.md +12 -1
- package/guides/server-actions.md +51 -7
- package/guides/testing.md +22 -1
- package/guides/typed-routes.md +16 -9
- package/guides/where-it-runs.md +74 -9
- package/guides/your-own-backend.md +28 -7
- package/package.json +1 -1
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,9 @@ 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
|
-
|
|
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. `Href` and
|
|
145
|
+
`ApiHref` are the same two types under their older names.
|
|
139
146
|
|
|
140
147
|
:::note[Paths, not response types]
|
|
141
148
|
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