@rsc-kit/mcp 0.14.0 → 0.16.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.
Files changed (52) hide show
  1. package/dist/answers.d.ts +7 -0
  2. package/dist/answers.js +28 -0
  3. package/dist/answers.js.map +1 -1
  4. package/dist/bundleGuides.d.ts +27 -0
  5. package/dist/bundleGuides.js +138 -0
  6. package/dist/bundleGuides.js.map +1 -0
  7. package/dist/index.js +21 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/recipes.js +138 -17
  10. package/dist/recipes.js.map +1 -1
  11. package/dist/report.d.ts +13 -0
  12. package/dist/report.js +1 -1
  13. package/dist/report.js.map +1 -1
  14. package/guides/api-routes.md +168 -0
  15. package/guides/authorization.md +288 -0
  16. package/guides/caching.md +57 -0
  17. package/guides/coming-from-next.md +151 -0
  18. package/guides/connection.md +98 -0
  19. package/guides/edge-caching.md +159 -0
  20. package/guides/errors.md +109 -0
  21. package/guides/file-uploads.md +119 -0
  22. package/guides/fonts.md +117 -0
  23. package/guides/forms.md +528 -0
  24. package/guides/getting-started.md +132 -0
  25. package/guides/images.md +83 -0
  26. package/guides/index.json +187 -0
  27. package/guides/installation.md +338 -0
  28. package/guides/introduction.md +119 -0
  29. package/guides/mcp.md +113 -0
  30. package/guides/metadata.md +289 -0
  31. package/guides/navigation.md +84 -0
  32. package/guides/no-javascript.md +76 -0
  33. package/guides/offline.md +215 -0
  34. package/guides/ppr.md +181 -0
  35. package/guides/pwa.md +260 -0
  36. package/guides/queries.md +340 -0
  37. package/guides/quick-start.md +99 -0
  38. package/guides/react-compiler.md +153 -0
  39. package/guides/redirects.md +143 -0
  40. package/guides/response-headers.md +66 -0
  41. package/guides/route-interception.md +206 -0
  42. package/guides/routing.md +458 -0
  43. package/guides/sections.md +74 -0
  44. package/guides/server-actions.md +444 -0
  45. package/guides/static-generation.md +347 -0
  46. package/guides/testing.md +158 -0
  47. package/guides/third-party-scripts.md +105 -0
  48. package/guides/typed-routes.md +139 -0
  49. package/guides/url-validation.md +143 -0
  50. package/guides/validation.md +175 -0
  51. package/guides/view-transitions.md +120 -0
  52. package/package.json +4 -3
@@ -0,0 +1,458 @@
1
+ # Routing
2
+
3
+ > File-based routes, layouts, loading states and parallel slots.
4
+
5
+ Routes are directories. The plugin reads your source tree at build time and
6
+ generates the entry that knows about them — there is nothing to register, and
7
+ no route table to keep in sync with the files.
8
+
9
+ ## File-based routing
10
+
11
+ Pages and layouts follow the Next.js App Router conventions. The default source
12
+ directory is `src/app`:
13
+
14
+ ```text
15
+ src/app/
16
+ layout.tsx root layout — wraps every page
17
+ loading.tsx Suspense fallback for every page
18
+ page.tsx GET /
19
+ @sidebar/
20
+ page.tsx parallel slot "sidebar"
21
+ about/
22
+ page.tsx GET /about
23
+ docs/
24
+ layout.tsx nested layout for /docs/*
25
+ page.tsx GET /docs
26
+ sidebar.tsx colocated component — NOT a route
27
+ [slug]/
28
+ page.tsx GET /docs/:slug
29
+ ```
30
+
31
+ <Aside type="note" title="Somewhere else?">
32
+ `src/app` is the default. Set `sourceDir` in the plugin options to put it
33
+ elsewhere — everything below works the same, only the prefix differs.
34
+ </Aside>
35
+
36
+ ### Special files
37
+
38
+ - `page.tsx` — defines a route. Only files named `page.*` create routes.
39
+ - `layout.tsx` — wraps every page in the same directory and below. Receives `children`.
40
+ - `loading.tsx` — Suspense fallback. Wraps the page in `<Suspense>` automatically. Hierarchical: the nearest one to the page wins.
41
+ - `default.tsx` — what a parallel slot renders when no page matches it.
42
+ - `middleware.ts` — runs before anything at or below the directory renders, on every path. Not a component: it returns nothing and decides, by redirecting or throwing. Composes up the tree like layouts. See [Authorization](/guides/authorization).
43
+ - `@folder/` — a parallel route slot. Rendered as a named prop on the layout that declares it. Adds no URL segment.
44
+
45
+ Everything else in the directory is a colocated component — importable by pages
46
+ and layouts, but not a route.
47
+
48
+ ### Dynamic segments
49
+
50
+ Square brackets mark a dynamic segment. `[slug]` matches one segment and
51
+ arrives as a `slug` prop on the page; `[...path]` catches the rest of the URL.
52
+
53
+ ```tsx title="src/app/docs/[slug]/page.tsx"
54
+ export default async function DocsPage({ params }: { params: Promise<{ slug: string }> }) {
55
+ const { slug } = await params;
56
+
57
+ return <h1>{slug}</h1>;
58
+ }
59
+ ```
60
+
61
+ `params` is a promise, and that is deliberate. Awaiting it lets everything above
62
+ the read paint first, and the build stores that as one shell serving every url
63
+ the route matches. Reading it synchronously would force a fresh render for every
64
+ visitor. `searchParams` works the same way.
65
+
66
+ ### Route groups
67
+
68
+ Parenthesised directories like `(marketing)` group files without adding a URL
69
+ segment. `app/(marketing)/pricing/page.tsx` serves `GET /pricing`.
70
+
71
+ ## Fetching data
72
+
73
+ A server component runs on the server, so it reads its data the way any
74
+ server-side module does — by importing it:
75
+
76
+ ```tsx title="src/app/dashboard/page.tsx"
77
+ import { revenue, orderCount } from '../../db/stats';
78
+
79
+ export default async function DashboardPage() {
80
+ const [amount, orders] = await Promise.all([revenue(), orderCount()]);
81
+
82
+ return (
83
+ <div>
84
+ <h1>Dashboard</h1>
85
+ <p>Revenue: {amount}</p>
86
+ <p>Orders: {orders}</p>
87
+ </div>
88
+ );
89
+ }
90
+ ```
91
+
92
+ There is no data-fetching API to learn, and no request context to thread
93
+ through. `db/stats` never enters the client module graph, so neither does the
94
+ connection string it opens.
95
+
96
+ ## Server components
97
+
98
+ Server components are the default: any `.tsx` file without a `"use client"`
99
+ directive is one. They may be async, and they may render client components.
100
+
101
+ ## Client components
102
+
103
+ `"use client"` on the first line marks a module as client code. It hydrates in
104
+ the browser and can use hooks, event handlers and browser APIs.
105
+
106
+ ```tsx title="src/components/Counter.tsx"
107
+ "use client";
108
+
109
+ import { useState } from 'react';
110
+
111
+ export default function Counter() {
112
+ const [count, setCount] = useState(0);
113
+
114
+ return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
115
+ }
116
+ ```
117
+
118
+ The build turns each one into a client reference, so a server component can
119
+ import and render it without shipping its own source.
120
+
121
+ ## Navigation
122
+
123
+ `Link` intercepts the click, fetches the payload, and updates the React tree
124
+ without a reload.
125
+
126
+ ```tsx
127
+ import Link from '@rsc-kit/core/Link';
128
+
129
+ <Link href="/docs/installation">Docs</Link>
130
+
131
+ // Prefetch on hover (the default)
132
+ <Link href="/settings" prefetch="hover">Settings</Link>
133
+
134
+ // Prefetch as soon as the link mounts
135
+ <Link href="/dashboard" prefetch="mount">Dashboard</Link>
136
+
137
+ // Keep the scroll position instead of jumping to the top
138
+ <Link href="/docs/rsc" preserveScroll>Stay here</Link>
139
+ ```
140
+
141
+ A navigation sends the layout chain the client already has mounted, and the
142
+ server answers with only the part below the deepest layout they share. The
143
+ layouts above it are never re-rendered, which is why the nav does not flicker
144
+ and why a half-typed form in a retained page survives.
145
+
146
+ ### Programmatic navigation
147
+
148
+ ```tsx
149
+ "use client";
150
+ import { visit, prefetch } from '@rsc-kit/core/router';
151
+
152
+ await visit('/about');
153
+ await visit('/dashboard', { replace: true }); // no back-button entry
154
+ prefetch('/settings'); // warm the cache
155
+ ```
156
+
157
+ A default export is available too, if you prefer `router.visit('/about')`.
158
+
159
+ ### Link props
160
+
161
+ | Prop | Type | Default | What it does |
162
+ | --- | --- | --- | --- |
163
+ | `href` | `string` | — | The destination. An off-origin URL is left to the browser. |
164
+ | `prefetch` | `"hover" \| "mount" \| "click" \| "none"` | `"hover"` | When to warm the cache. `hover` waits 100 ms for the pointer to settle, so crossing a nav bar does not fire a request per link. `true`/`false` are accepted as aliases for `hover`/`none`. |
165
+ | `cacheFor` | `number` | `30000` | How long a prefetched payload stays usable, in milliseconds. |
166
+ | `replace` | `boolean` | `false` | Replace the history entry instead of pushing one. |
167
+ | `preserveScroll` | `boolean` | `false` | Stay where you are instead of scrolling to the top. |
168
+
169
+ Anything else is passed straight to the underlying `<a>`, `ref` included.
170
+
171
+
172
+ ### Loading state
173
+
174
+ Each `Link` carries a `data-pending` attribute while its navigation is in
175
+ flight, which is usually enough on its own:
176
+
177
+ ```css
178
+ a[data-pending] {
179
+ opacity: 0.5;
180
+ pointer-events: none;
181
+ }
182
+ ```
183
+
184
+ When the affected UI is not the link itself, `useLinkStatus` reads the nearest
185
+ parent `Link`'s state:
186
+
187
+ ```tsx
188
+ "use client";
189
+
190
+ import { useLinkStatus } from '@rsc-kit/core/useLinkStatus';
191
+
192
+ function NavContent({ children }: { children: React.ReactNode }) {
193
+ const { pending } = useLinkStatus();
194
+
195
+ return (
196
+ <span className={pending ? 'opacity-50' : 'opacity-100'}>
197
+ {children}
198
+ {pending && <span className="spinner" />}
199
+ </span>
200
+ );
201
+ }
202
+ ```
203
+
204
+ ## Typed links
205
+
206
+ Every build writes the routes it found into `.rsc-kit/rsc-routes.d.ts`, so
207
+ `href` is checked against them rather than being any old string:
208
+
209
+ ```tsx
210
+ <Link href="/orders">Orders</Link>
211
+ <Link href="/ordres">Orders</Link>
212
+ // ^ Type '"/ordres"' is not assignable. Did you mean '"/orders"'?
213
+ ```
214
+
215
+ For a route with params, write the url — a template literal is checked the same
216
+ way, including the static part around the value:
217
+
218
+ ```tsx
219
+ <Link href={`/posts/${post.slug}`}>Read</Link>
220
+ <Link href={`/postz/${post.slug}`}>Read</Link>
221
+ // ^ Type '`/postz/${string}`' is not assignable
222
+ ```
223
+
224
+ The same type covers `redirect()`, `visit()`, `prefetch()` and `Form`'s
225
+ `action`. Nothing to
226
+ configure and no flag — the file is regenerated on every build, so deleting a
227
+ page makes every link to it stop compiling.
228
+
229
+ One thing you will hit immediately: a list of links widens to `string` unless
230
+ you say otherwise. `satisfies` keeps each literal *and* checks it, so the typo
231
+ fails at the list rather than at the `Link` that renders it:
232
+
233
+ ```tsx title="src/components/Nav.tsx"
234
+ // `satisfies` rather than a type annotation: an annotation would widen href to
235
+ // Href and lose which one each entry is, while this keeps the literals and
236
+ // still checks them — so a typo fails here, at the list, rather than at the
237
+ // Link that renders it.
238
+ const links = [
239
+ { href: '/', label: 'Home' },
240
+ { href: '/dashboard', label: 'Dashboard' },
241
+ { href: '/posts/hello-world', label: 'A Post' },
242
+ ] satisfies { href: Href; label: string }[]
243
+ ```
244
+
245
+ ### When the value is not already a url
246
+
247
+ A template literal interpolates whatever it is given, and the type cannot stop
248
+ it — `${string}` covers spaces, slashes and `?` as happily as a slug. So a
249
+ title used as an identifier quietly produces a url that means something else:
250
+
251
+ ```ts
252
+ `/posts/${'Q3 report / draft?v=2'}`
253
+ // '/posts/Q3 report / draft?v=2' — three segments and a query string
254
+ ```
255
+
256
+ Encode it in the template, the same as anywhere else. There is no url builder
257
+ to reach for and nothing new to remember:
258
+
259
+ ```tsx
260
+ <Link href={`/posts/${encodeURIComponent(post.title)}`}>Read</Link>
261
+ ```
262
+
263
+ <Aside type="note" title="Why there is no route() helper">
264
+ The url *is* the file path, and a template literal is already checked against
265
+ it. A builder would only give you a second way to write the same line.
266
+ </Aside>
267
+
268
+ When a destination is computed rather than written, cast it — that is the seam
269
+ where you are telling the typechecker something it cannot know:
270
+
271
+ ```ts
272
+ import type { Href } from '@rsc-kit/core/routes';
273
+
274
+ <Link href={savedPath as Href}>Resume</Link>
275
+ ```
276
+
277
+ Two limits. A dynamic segment widens to `${string}`, and a template literal type
278
+ cannot say "no slashes here" — so `/posts/a/b` type-checks against
279
+ `/posts/[slug]`. And before your first build there are no route types yet, so
280
+ every href falls back to `string`.
281
+
282
+ ## Reading the url from a client component
283
+
284
+ `usePathname` and `useSearchParams` read the current url and follow
285
+ navigations:
286
+
287
+ ```tsx title="src/components/Filters.tsx"
288
+ "use client";
289
+
290
+ import { useSearchParams } from '@rsc-kit/core/useSearchParams';
291
+
292
+ export function Filters() {
293
+ const q = useSearchParams().get('q');
294
+
295
+ return <p>Showing results for {q}</p>;
296
+ }
297
+ ```
298
+
299
+ They behave differently on the server, for a reason. A **pathname** is fixed for
300
+ a stored page, so `usePathname` just answers with the url being rendered.
301
+
302
+ A **query string** is not — the same route is asked for with `?q=shoes` and
303
+ `?q=hats` — so there is no honest answer at build time, and `useSearchParams`
304
+ throws rather than pretending it is empty.
305
+
306
+ Wrap it, and the throw becomes the fallback:
307
+
308
+ ```tsx title="src/app/search/page.tsx"
309
+ export default function SearchPage() {
310
+ return (
311
+ <Suspense fallback={<p>Reading the query…</p>}>
312
+ <Filters />
313
+ </Suspense>
314
+ );
315
+ }
316
+ ```
317
+
318
+ React treats the throw as recoverable at the nearest boundary: the fallback is
319
+ what gets stored, and the browser renders the real value on hydration.
320
+
321
+ <Aside type="note" title="Without a boundary it still works, and that is the problem">
322
+ A root `loading.tsx` catches it, so the page is stored showing the fallback
323
+ the whole app shares — for a search page, the word "Loading" where the query
324
+ should be. The build says so rather than leaving you to find it:
325
+
326
+ ```text
327
+ ○ /search
328
+ ⚠ nothing painted without the root loading.tsx — the fallback the whole
329
+ app shares is standing in for this page. Put a boundary where the
330
+ waiting is.
331
+ ```
332
+ </Aside>
333
+
334
+ For a **server** component, the query arrives as a prop instead — `searchParams`,
335
+ awaited like `params`. Reach for the hook only when the component is
336
+ `"use client"` and has to follow navigations.
337
+
338
+ ## Search params as state
339
+
340
+ [nuqs](https://nuqs.dev) is the usual answer for a filter or a tab that lives
341
+ in the url, and it works here with its own adapter:
342
+
343
+ ```tsx title="src/app/layout.tsx"
344
+ import { NuqsAdapter } from '@rsc-kit/core/nuqs'
345
+
346
+ export default function RootLayout({ children }) {
347
+ return <NuqsAdapter>{children}</NuqsAdapter>
348
+ }
349
+ ```
350
+
351
+ ```tsx
352
+ const [kind, setKind] = useQueryState('kind')
353
+ ```
354
+
355
+ The stock `nuqs/adapters/react` also works, for a **shallow** update — one
356
+ that changes the url and nothing else. Where it gives out is `shallow: false`,
357
+ the case where a server component reads the query and should render again with
358
+ the new one: with no router to call, it falls back to a full page load.
359
+
360
+ This adapter hands that case to the router, so the page's payload is refetched
361
+ in place and every bit of client state survives. It also tells our own
362
+ `useSearchParams` about a shallow change — that hook listens for a navigation
363
+ event rather than watching history, so an adapter that only writes the url
364
+ would leave the rest of the page reading the old query.
365
+
366
+ `nuqs` is an optional peer dependency. Nothing else imports it.
367
+
368
+ :::note[Where this should live]
369
+ In nuqs, alongside its adapters for Next, Remix and TanStack Router — that is
370
+ how every other framework's works, and the `custom` API this is built on is
371
+ their escape hatch for a framework that does not have one in-tree yet. It is
372
+ here until there are enough people using it to make that contribution a
373
+ reasonable ask. When `nuqs/adapters/rsc-kit` exists, this import will point at
374
+ it and then be removed.
375
+ :::
376
+
377
+ ## Parallel routes
378
+
379
+ A directory prefixed with `@` is a slot. It renders alongside the page and
380
+ arrives as a named prop on the layout **in its own directory** — not on the
381
+ innermost one.
382
+
383
+ ```text
384
+ src/app/
385
+ layout.tsx receives { children, sidebar, modal }
386
+ page.tsx arrives as "children"
387
+ @sidebar/
388
+ page.tsx arrives as "sidebar"
389
+ @modal/
390
+ default.tsx arrives as "modal" — empty until something fills it
391
+ ```
392
+
393
+ ```tsx title="src/app/layout.tsx"
394
+ export default function Layout({
395
+ children,
396
+ sidebar,
397
+ modal,
398
+ }: {
399
+ children: React.ReactNode;
400
+ sidebar: React.ReactNode;
401
+ modal: React.ReactNode;
402
+ }) {
403
+ return (
404
+ <div className="flex">
405
+ <aside className="w-[250px]">{sidebar}</aside>
406
+ <main className="flex-1">{children}</main>
407
+ {modal}
408
+ </div>
409
+ );
410
+ }
411
+ ```
412
+
413
+ A layout that does not render a slot it was handed drops it silently — the page
414
+ comes out whole apart from the missing region, and nothing warns. Slots are
415
+ what [route interception](/guides/route-interception) fills.
416
+
417
+ ## Streaming
418
+
419
+ On a full page load the HTML streams. React flushes the shell — layouts, static
420
+ markup, and every Suspense fallback in it — immediately, and injects the real
421
+ content as each boundary resolves.
422
+
423
+ SPA navigations stream too: the Flight payload is read with
424
+ `createFromReadableStream()` as it arrives, so React reconciles progressively
425
+ rather than waiting for the last byte.
426
+
427
+ ## What the build stores
428
+
429
+ Nothing is declared. The build renders each route and stores what it can:
430
+
431
+ | Outcome | What happened |
432
+ | --- | --- |
433
+ | The whole page | It rendered to completion. Served from disk, no render per request. |
434
+ | A shell | Something was still waiting when the budget expired, but the static parts had already painted. The shell is stored; the rest is rendered per request and streamed in. |
435
+ | A redirect | The route only redirects. The redirect itself is stored — status and location — and served without rendering. |
436
+
437
+ There is no fourth outcome. A route that could not be stored, and did not say
438
+ it wanted to be rendered per request, **fails the build** and names the fix:
439
+
440
+ ```text
441
+ Some routes could not be prerendered:
442
+
443
+ /photo/[id] — renders its params before it can paint, and lists no urls to build
444
+
445
+ Each one reads request data — params, headers, cookies, or the host —
446
+ above every Suspense boundary, so nothing can paint without it.
447
+
448
+ Put the part that waits inside <Suspense>, or add a loading.tsx beside
449
+ the page, so there is something to store while the rest arrives.
450
+ ```
451
+
452
+ That is deliberate, and there is no flag to silence it. The alternative is a
453
+ category of routes that quietly render per request, which is where the slow
454
+ ones go to be forgotten.
455
+
456
+ See [Static generation](/guides/static-generation) for listing the urls of a
457
+ parameterised route, and [Partial prerendering](/guides/ppr) for where the
458
+ boundaries go.
@@ -0,0 +1,74 @@
1
+ # Sections
2
+
3
+ > Refreshing one region of a page without re-rendering the rest.
4
+
5
+ A page often has one part that changes and a lot that does not. A section names
6
+ that part, so an action can refresh it on its own.
7
+
8
+ ```tsx title="src/app/orders/orders.section.tsx"
9
+ import { section } from '@rsc-kit/core/section'
10
+
11
+ async function Orders() {
12
+ const orders = await db.orders()
13
+
14
+ return (
15
+ <ul>
16
+ {orders.map((o) => <li key={o.id}>{o.reference}</li>)}
17
+ </ul>
18
+ )
19
+ }
20
+
21
+ export default section('orders', Orders)
22
+ ```
23
+
24
+ Render it like any other component:
25
+
26
+ ```tsx title="src/app/orders/page.tsx"
27
+ import Orders from './orders.section'
28
+
29
+ export default function OrdersPage() {
30
+ return (
31
+ <>
32
+ <h1>Orders</h1>
33
+ <Orders />
34
+ </>
35
+ )
36
+ }
37
+ ```
38
+
39
+ ## Refreshing it
40
+
41
+ An action names what it changed, and only that region is rendered again:
42
+
43
+ ```tsx
44
+ 'use server'
45
+
46
+ import { revalidate } from '@rsc-kit/core/revalidate'
47
+
48
+ export async function placeOrder(form: FormData) {
49
+ await db.orders.create({ reference: String(form.get('reference')) })
50
+
51
+ revalidate('orders')
52
+ }
53
+ ```
54
+
55
+ The rest of the page is untouched — not re-rendered and not re-fetched. Whatever
56
+ state lives outside the section, including a half-filled form beside it, stays
57
+ exactly as it was.
58
+
59
+ ## The name is scoped to the module, not the app
60
+
61
+ Two pages may both call their section `orders`. The name is resolved through the
62
+ module the route declares, not through a table every section in the app writes
63
+ to.
64
+
65
+ That is a security property rather than a convenience. A name-keyed registry is
66
+ populated by every section at bundle load, so a lookup by name could reach any
67
+ page's region from any url — bounded only by whatever guard happened to sit on
68
+ the url that was asked for.
69
+
70
+ ## What a section is not
71
+
72
+ It is not a cache boundary and not a client component. It renders on the server
73
+ like everything else; what it adds is a seam the server can render *into* on its
74
+ own, without producing the whole page around it.