@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.
@@ -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 Href`), because there is nothing
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: Href; label: string }[]
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
- Every build writes the `route.ts` files it found as well, in their own union —
121
- so a `fetch` to an endpoint that no longer exists stops compiling:
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
- **Pages and api routes are separate unions on purpose.** `<Link href="/api/health">`
135
- does not compile, because linking to an api route navigates the browser away to
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. `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
@@ -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 with `replies`, one per call in order, each the reply it would have had
129
- alone plus the `status` it would have carried:
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
- Run every call, in order, and answer every one a refusal in the second is
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 this
138
- loses nothing but the saving: the renderer reads its "no function name" answer
139
- as "no batches here" and sends single calls from then on. Batches never mix
140
- visitors; every call in one carried the same forwarded headers.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/mcp",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.",
5
5
  "type": "module",
6
6
  "license": "MIT",