@rsc-kit/mcp 0.17.0 → 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.
@@ -44,7 +44,10 @@ it, the server evaluates the library's internals for nothing on every render.
44
44
  | `next/script` | [a `<script>` tag](/guides/third-party-scripts): React 19 hoists and dedupes `async` scripts itself |
45
45
  | `NEXT_PUBLIC_*` | `VITE_*`, read through `import.meta.env`; everything else stays `process.env` on the server |
46
46
  | `next.config.js` | `vite.config.ts` — Tailwind, aliases and plugins are Vite's |
47
+ | `instrumentation.ts` with `register()` | the same file, in `src/` — imported before any page and awaited before the first request; [startup](/guides/instrumentation) |
47
48
  | `next-safe-action` | `createActionClient()` — same shape, [below](#actions) |
49
+ | `experimental.optimizePackageImports` | on by default for every barrel package the server imports, with no list — read from the barrel itself; `rscKit({ barrelImports: false })` turns it off |
50
+ | a `middleware.ts` rewrite for subdomains | nothing — a host is a route segment, so `acme.example.com/` reaches `app/[domain]/page.tsx` and `admin.example.com/` reaches `app/admin/page.tsx` — [domains](/guides/domains) |
48
51
  | `app/robots.ts`, `app/sitemap.ts` | the same files, the same shapes — [robots, sitemap and llms.txt](/guides/seo-files); `app/llms.ts` beside them |
49
52
  | `@react-email/render` in a server action | the same call, in a module that starts with `"use ssr"` — [emails](/guides/emails). Next fails the same way where it renders server components; the directive is how this one moves it |
50
53
  | `cache` from `react` | `cache` from `@rsc-kit/core/cache` — React's memoises only inside a component render; this one spans the request, so a guard, the layout and the action share one call. The build names server files still importing React's |
@@ -0,0 +1,129 @@
1
+ # Deploying
2
+
3
+ > What to ship, and the two settings that fail quietly.
4
+
5
+ `npm run build` writes one directory, and it is the deployment:
6
+
7
+ ```
8
+ .output/server the server Nitro built, and the engine it calls
9
+ .output/public hashed assets and the pages frozen at build time
10
+ ```
11
+
12
+ ```sh
13
+ npm run build
14
+ npm run start # node or bun .output/server/index.mjs
15
+ ```
16
+
17
+ `npx vite preview` runs the same build through Vite's preview server, which is
18
+ what Nitro suggests at the end of a build. Both serve the real thing; `start`
19
+ is what a deployment runs.
20
+
21
+ ## Anywhere that runs the runtime
22
+
23
+ `.output/` is self-contained. Copy it and start it — a container, a VPS, a
24
+ process manager, a platform that runs a Node or Bun process. There is nothing to
25
+ register and no platform API to satisfy, and **no `node_modules`**: the
26
+ dependencies are in the bundle.
27
+
28
+ ```dockerfile
29
+ FROM oven/bun:1 AS build
30
+ WORKDIR /app
31
+ COPY package.json bun.lock ./
32
+ RUN bun install --frozen-lockfile
33
+ COPY . .
34
+ RUN bun run build
35
+
36
+ FROM oven/bun:1
37
+ WORKDIR /app
38
+ COPY --from=build /app/.output ./.output
39
+ CMD ["bun", ".output/server/index.mjs"]
40
+ ```
41
+
42
+ The second stage carries `.output` and nothing else. On the docs application
43
+ that is 1.0 MB against 104 MB of `node_modules`.
44
+
45
+ ## Or a platform, without a Dockerfile
46
+
47
+ Change the preset and Nitro produces what that platform expects — a Worker and
48
+ its `wrangler.json`, a Vercel function, a Netlify handler. See
49
+ [Where it runs](/hosts/where-it-runs).
50
+
51
+ ## Two things that fail quietly
52
+
53
+ **Do not set `NODE_ENV` when starting the server.** The build bakes its mode
54
+ into the bundle, so a server started with nothing set is production because it
55
+ was *built* that way. Setting it at start time is a second source of truth and
56
+ the one that can disagree — and when it disagrees the failure is silent: every
57
+ page renders, and none of them hydrate.
58
+
59
+ The check is React's debug rows in the payload:
60
+
61
+ ```sh
62
+ curl -s https://your-app.example.com/ | grep -c ':D{'
63
+ ```
64
+
65
+ `0` on a correct production build. Anything else means a development bundle
66
+ reached the client.
67
+
68
+ **Serve `.output/public` at the root.** Nitro does this itself, so this only
69
+ matters behind a CDN: point it at that directory and let the hashed filenames do
70
+ the caching — they are content-addressed, so they can be cached forever.
71
+
72
+ ## Server actions across a deploy
73
+
74
+ A server action can close over server-side values, and React encrypts those
75
+ before sending them to the browser so the page cannot read them. The process
76
+ that decrypts them on the way back has to hold the same key.
77
+
78
+ By default that key is generated at build time and baked in. Every instance
79
+ running the same build agrees, so the only exposure is the deploy itself: a
80
+ browser sitting on a page from the old build calls an action on the new one,
81
+ and the key has changed underneath it. The call fails.
82
+
83
+ For most apps that window is seconds and nobody notices. If yours is long
84
+ enough to care about — a slow rollout, long-lived pages, an app people leave
85
+ open — pin the key:
86
+
87
+ ```sh
88
+ # once, kept wherever you keep secrets
89
+ openssl rand -base64 32
90
+ ```
91
+
92
+ ```sh
93
+ RSC_ACTION_ENCRYPTION_KEY=<that value>
94
+ ```
95
+
96
+ Set it at **build time** and the build stops baking its own; the value is read
97
+ from the environment when the server runs, so the same artifact deploys
98
+ anywhere.
99
+
100
+ :::danger[Set it everywhere or nowhere]
101
+ Half-configured is worse than unconfigured. If one instance reads the variable
102
+ and another falls back to a baked key, they disagree, and the symptom is an
103
+ action that fails for some visitors and not others with nothing in the logs
104
+ pointing at a key.
105
+
106
+ Unset, everything works — the build-time key is used, which is the default
107
+ precisely because it cannot be got half right.
108
+ :::
109
+
110
+ ## Frozen pages and the routes that own them
111
+
112
+ `.output/public` holds whole frozen pages and PPR shells alongside the assets.
113
+ They are read through the `prerendered` reader, which is a function rather than a
114
+ directory precisely so a runtime with no filesystem — a Worker — can supply them
115
+ from a binding instead.
116
+
117
+ A route that declares middleware is never cached publicly: it is sent as
118
+ `private, no-store`, because middleware runs per visitor. If something in front
119
+ of your app also owns the response — an auth proxy re-issuing a session on
120
+ pass-through — give the paths it covers a `middleware.ts` so this host knows
121
+ they are covered. See [serving shells from a CDN](/guides/edge-caching).
122
+
123
+ ## Rebuild on deploy
124
+
125
+ Cached responses carry a build version, so a deploy invalidates them.
126
+
127
+ Ship `.output/` from the same commit as the code that serves it. A server
128
+ running one build against another's frozen pages is the one combination nothing
129
+ checks for you.
@@ -0,0 +1,129 @@
1
+ # Domains and subdomains
2
+
3
+ > A host as a route segment — admin.example.com reaches app/admin, a tenant's host binds [domain] — with nothing to rewrite.
4
+
5
+ Next routes a subdomain with a `middleware.ts` that rewrites
6
+ `acme.example.com/settings` to `/acme/settings` before matching, and the
7
+ route tree never learns a host was involved. Here the same rule is the
8
+ router's own, so the build can see it: typed routes, one stored page per
9
+ tenant, and no middleware to write.
10
+
11
+ ## The rule
12
+
13
+ A request from a host that is not the site's own is matched **with the host
14
+ in front of the path**:
15
+
16
+ | request | matched as | file |
17
+ | --- | --- | --- |
18
+ | `example.com/admin` | `/admin` | `app/admin/page.tsx` |
19
+ | `admin.example.com/` | `/admin` | `app/admin/page.tsx` — the same file |
20
+ | `acme.example.com/settings` | `/acme/settings` | `app/[domain]/settings/page.tsx`, `domain: "acme"` |
21
+ | `acme.com/settings` | `/acme.com/settings` | the same file, `domain: "acme.com"` |
22
+
23
+ A subdomain of the site contributes its label; any other host contributes the
24
+ whole host. The site's own hosts — the one in the root layout's
25
+ `metadataBase`, `www.` of it, and any named in `rscKit({ hosts })` — contribute
26
+ nothing, so the apex keeps path routing and an app adds tenants without moving
27
+ a file. `localhost` and an ip address are always the site's own.
28
+
29
+ Nothing to configure for that: `metadataBase` names the apex, and a
30
+ directory does the rest. `hosts` is for a name that is neither the apex nor a
31
+ subdomain of it and is still the site rather than a tenant — a staging or
32
+ internal name, or a second brand domain:
33
+
34
+ ```ts title="vite.config.ts"
35
+ rscKit({ hosts: ['app.internal', 'example.co.uk'] })
36
+ ```
37
+
38
+ Listing a subdomain there makes it the site by path instead of a tenant —
39
+ the "app on `app.example.com`, marketing on the apex" split — which is a
40
+ choice, not a requirement.
41
+
42
+ And only when a route could answer it: a `[domain]` directory at the top of
43
+ `app/`, or a directory named for the host. An app with a `metadataBase` and no
44
+ tenant tree routes every host by path, and a proxy that forwards to the app by
45
+ an internal name is not read as a tenant called `internal`.
46
+
47
+ The visitor's url is untouched: `acme.example.com/settings` stays in the
48
+ address bar, and a link to `/billing` on that page goes to
49
+ `acme.example.com/billing`. Only the match changed.
50
+
51
+ ## A tenant tree
52
+
53
+ ```tsx title="src/app/[domain]/layout.tsx"
54
+ import { notFound } from '@rsc-kit/core/not-found';
55
+ import { tenantByDomain } from '@/lib/tenants';
56
+
57
+ export default async function TenantLayout({ params, children }) {
58
+ const { domain } = await params;
59
+ const tenant = await tenantByDomain(domain); // "acme" or "acme.com", as stored
60
+
61
+ if (!tenant) notFound();
62
+
63
+ return <TenantProvider tenant={tenant}>{children}</TenantProvider>;
64
+ }
65
+ ```
66
+
67
+ `[domain]` is an ordinary dynamic segment: `params.domain` in every page and
68
+ layout below it, `route('/[domain]/settings', { domain })` typed, `loading.tsx`
69
+ and `error.tsx` where you put them. A directory named for a host,
70
+ `app/admin/`, wins over `[domain]` the way a static segment wins over a
71
+ parameter anywhere else.
72
+
73
+ One difference from a parameter deeper in the tree: a `[domain]` at the top
74
+ of `app/` binds **only from a host**, never from a path. `example.com/nope`
75
+ is a 404, not a tenant called `nope`, and `acme.example.com/` cannot be
76
+ reached as `example.com/acme`. Next has no such guard — its `[domain]` folder
77
+ matches any path once the rewrite is in place — which is why Next apps tuck
78
+ the tenant tree under a route group.
79
+
80
+ ## Domains in a database
81
+
82
+ `generateStaticParams` on the tenant route is the hook. The listed hosts are
83
+ rendered at build and stored, one file per host; a host added afterwards
84
+ falls through to the plain tree, or — if the page reads the request — renders
85
+ on demand and resolves at request time:
86
+
87
+ ```ts title="src/app/[domain]/page.tsx"
88
+ export async function generateStaticParams() {
89
+ const tenants = await db.tenant.findMany({ select: { domain: true } });
90
+
91
+ return tenants.map((t) => ({ domain: t.domain }));
92
+ }
93
+ ```
94
+
95
+ A tenant's page that reads `cookies()` or awaits `connection()` is dynamic
96
+ for that tenant and stored for none, exactly as any page is.
97
+
98
+ ## Locally
99
+
100
+ Keep `metadataBase` as the production host. `localhost` and an ip address
101
+ are always the site's own, so the dev server routes by path as it always
102
+ did, and nothing changes until a `[domain]` directory exists. To try a tenant
103
+ without DNS, send the host the router will see in production:
104
+
105
+ ```bash
106
+ curl -H 'X-Forwarded-Host: acme.example.com' http://localhost:3000/
107
+ ```
108
+
109
+ Or point `acme.example.com` at `127.0.0.1` in `/etc/hosts` and open it on
110
+ the dev server's port in a browser.
111
+
112
+ ## Behind a proxy
113
+
114
+ The host is read from `X-Forwarded-Host` first, then `Host`. A load balancer
115
+ that terminates TLS and forwards to the app by an internal name still routes
116
+ by the name the visitor typed.
117
+
118
+ ## Not for a static export
119
+
120
+ An export is served by a file server, which sees no host. Host routing is a
121
+ server feature; an exported site is the site's own on every host it is
122
+ served from.
123
+
124
+ ## Coming from Next
125
+
126
+ Delete the rewrite in `middleware.ts` and the `[domain]` directory works as
127
+ it did; the segment binds the same value the rewrite put there. Next's
128
+ `rewrite()` for anything else is not here — a host maps to a tree by file,
129
+ not by code.
package/guides/errors.md CHANGED
@@ -7,6 +7,15 @@ nearest one wins.
7
7
 
8
8
  ## When a page throws
9
9
 
10
+ With nothing of your own, a page that throws shows the engine's error page
11
+ where the page was, and the layouts around it stay: in development the
12
+ message and stack, in production "Something went wrong" with the digest to
13
+ search the server log for, and a **Try again** in both. It used to show
14
+ nothing — React unmounted the document on hydration, a black page with the
15
+ cause nowhere near it.
16
+
17
+ That page is the fallback. To show something of your own:
18
+
10
19
  Put an `error.tsx` in the directory you want to cover:
11
20
 
12
21
  ```tsx title="src/app/orders/error.tsx"
@@ -48,7 +57,8 @@ Log the digest where you log the error, and the two line up.
48
57
  ### It does not catch everything
49
58
 
50
59
  - **Errors in the layout above it.** The boundary sits inside that layout, so a
51
- layout that throws needs an `error.tsx` a directory up.
60
+ layout that throws needs an `error.tsx` a directory up. The engine's own
61
+ page is outermost and catches those too.
52
62
  - **The build.** A page that throws every time it renders fails the build
53
63
  rather than shipping a stored error page. The boundary is for a request that
54
64
  goes wrong, not a page that is broken.
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,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,6 +29,16 @@
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",
@@ -59,6 +74,11 @@
59
74
  "title": "Getting started",
60
75
  "description": "Serve React Server Components from any JavaScript backend."
61
76
  },
77
+ {
78
+ "slug": "go",
79
+ "title": "Go",
80
+ "description": "A Go process behind the renderer — functions, guards and actions in Go."
81
+ },
62
82
  {
63
83
  "slug": "images",
64
84
  "title": "Images",
@@ -69,11 +89,21 @@
69
89
  "title": "Installation",
70
90
  "description": "From an empty directory to a streaming RSC app."
71
91
  },
92
+ {
93
+ "slug": "instrumentation",
94
+ "title": "Startup",
95
+ "description": "instrumentation.ts runs once, before anything else."
96
+ },
72
97
  {
73
98
  "slug": "introduction",
74
99
  "title": "Introduction",
75
100
  "description": "React Server Components as a Vite plugin, deployed wherever you like."
76
101
  },
102
+ {
103
+ "slug": "laravel",
104
+ "title": "Laravel",
105
+ "description": "React Server Components in front, a Laravel application behind them."
106
+ },
77
107
  {
78
108
  "slug": "mcp",
79
109
  "title": "Working with an AI agent",
@@ -193,5 +223,15 @@
193
223
  "slug": "view-transitions",
194
224
  "title": "View transitions",
195
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."
196
236
  }
197
237
  ]
@@ -47,6 +47,7 @@ import { defineConfig } from "vite";
47
47
  import react from "@vitejs/plugin-react";
48
48
  import { nitro } from "nitro/vite";
49
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.
@@ -64,6 +65,13 @@ export default defineConfig({
64
65
  }),
65
66
  react(),
66
67
  ],
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
+ },
67
75
  });
68
76
  ```
69
77
 
@@ -210,15 +218,18 @@ not match its `[param]` segment is a compile error rather than a blank page.
210
218
  "target": "ESNext",
211
219
  "module": "ESNext",
212
220
  "moduleResolution": "bundler",
221
+ "paths": {
222
+ "@/*": ["./src/*"]
223
+ },
213
224
  "jsx": "react-jsx",
214
225
  "strict": true,
215
226
  "noEmit": true,
227
+ "isolatedModules": true,
228
+ "moduleDetection": "force",
229
+ "verbatimModuleSyntax": true,
216
230
  "skipLibCheck": true,
217
231
  "resolveJsonModule": true,
218
- "types": [
219
- "@types/bun",
220
- "vite/client"
221
- ]
232
+ "types": ["@types/bun", "vite/client"]
222
233
  },
223
234
  "include": ["src/**/*", "server/**/*", ".rsc-kit/**/*", "vite.config.ts"]
224
235
  }
@@ -228,6 +239,14 @@ not match its `[param]` segment is a compile error rather than a blank page.
228
239
  of a stylesheet (`import './styles.css'` in the root layout) is an error, and
229
240
  `import.meta.env` is untyped.
230
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
+
231
250
  ### Environment variables
232
251
 
233
252
  There is nothing to install and nothing this package adds — Vite already owns