@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,143 @@
1
+ # Redirects
2
+
3
+ > Leaving a page from inside the render, and what that costs.
4
+
5
+ ```tsx title="src/app/products/[slug]/page.tsx"
6
+ import { redirect } from '@rsc-kit/core/redirect';
7
+ import { findProduct } from '../../../data';
8
+
9
+ export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
10
+ const { slug } = await params;
11
+ const product = await findProduct(slug);
12
+
13
+ if (!product) redirect('/products');
14
+
15
+ return <h1>{product.name}</h1>;
16
+ }
17
+ ```
18
+
19
+ `redirect` never returns — it throws, which is what stops the component. The
20
+ default status is `307`, because it preserves the method: a redirect out of a
21
+ `POST` does not silently become a `GET` of the target.
22
+
23
+ A redirect during a navigation stays a navigation. The document does not reload,
24
+ the layouts you are inside stay mounted, and only the part below them changes.
25
+
26
+ The url that redirected replaces its history entry rather than adding one, so
27
+ Back does not land on it and redirect you again.
28
+
29
+ ## Where you call it matters
30
+
31
+ This is the part worth understanding, because it is also the security-relevant
32
+ part. Headers flush early on purpose — that is what makes the first paint fast
33
+ — so a redirect decided late has no status line left to use. There are two
34
+ windows, and nothing you write chooses between them:
35
+
36
+ | Called | Answered with | What the browser saw first |
37
+ | --- | --- | --- |
38
+ | Above every `<Suspense>` boundary | A real `3xx`, or `X-RSC-Redirect` on a navigation | Nothing at all |
39
+ | Inside a boundary | The shell, then the redirect | Layouts, and the fallbacks standing in for what never arrived |
40
+
41
+ Neither buffers the response. Before anything is written the host is still
42
+ waiting on the shell, so a component that redirects instead of rendering is
43
+ caught there. After that, React already carries an error digest to the client
44
+ and the destination rides along in it.
45
+
46
+ <Aside type="caution" title="Middleware belongs above the boundaries">
47
+ A `loading.tsx` wraps the whole page in `<Suspense>`. That is usually what you
48
+ want — it is how a page gets a frozen shell — but it also means an `await` at
49
+ the top of that page is *inside* a boundary, and a redirect after it arrives
50
+ in the second window.
51
+
52
+ Nothing from inside the boundary was shown, so a not-found redirect is fine
53
+ there. An authorization check is different: the layouts above it already
54
+ rendered and already went out. If a page must reveal nothing at all, the
55
+ check has to run somewhere that blocks the shell.
56
+ </Aside>
57
+
58
+ ## Where to put an authorization check
59
+
60
+ In order of preference:
61
+
62
+ **In `middleware.ts`.** A file beside the layout, run before anything at or below it
63
+ renders, on every path. This is the one built for the job.
64
+
65
+
66
+ <Aside type="danger" title="A layout is not a security boundary">
67
+ It is tempting — a layout renders above the page's boundary, so on a full
68
+ page load a redirect there happens before anything is sent. But a navigation
69
+ tells the server which layouts the client already has, in `X-RSC-Segments`,
70
+ and the server skips re-rendering those. A client that *claims* to hold your
71
+ layout skips the check:
72
+
73
+ ```bash
74
+ curl -H 'X-RSC: true' -H 'X-RSC-Segments: app/layout' /guarded
75
+ # 204, X-RSC-Redirect: /orders ← the middleware ran
76
+
77
+ curl -H 'X-RSC: true' -H 'X-RSC-Segments: app/layout,app/guarded/layout' /guarded
78
+ # 200, and the page's content ← it did not
79
+ ```
80
+
81
+ The header is not verified and cannot be. Put the check in a `middleware.ts`
82
+ beside the layout instead — it runs before anything below it renders, on
83
+ every path. See [Authorization](/guides/authorization).
84
+ </Aside>
85
+
86
+ **At the top of a page with no `loading.tsx`.** Runs on every render of that
87
+ page, so it is not skippable the way a layout is — but fragile in a different
88
+ way: adding a `loading.tsx` later silently moves the check into the second
89
+ window, and nothing warns.
90
+
91
+ <Aside type="danger" title="Never catch it and continue">
92
+ `redirect` communicates by throwing. A `try`/`catch` that swallows everything
93
+ turns the redirect into a blank region — the component stops, and nothing
94
+ takes its place. If you must wrap the call, rethrow what you do not
95
+ recognise:
96
+
97
+ ```ts
98
+ import { isRedirectSignal } from '@rsc-kit/core/redirect';
99
+
100
+ try {
101
+ await mightRedirect();
102
+ } catch (error) {
103
+ if (isRedirectSignal(error)) throw error;
104
+ // …
105
+ }
106
+ ```
107
+ </Aside>
108
+
109
+ ## From a server action
110
+
111
+ An action is not a render, so there is no shell to be on either side of. Throw
112
+ from the action and the client follows it:
113
+
114
+ ```ts title="src/actions.ts"
115
+ 'use server'
116
+
117
+ import { redirect } from '@rsc-kit/core/redirect';
118
+
119
+ export async function createPost(title: string) {
120
+ const post = await savePost(title);
121
+
122
+ redirect(`/posts/${post.slug}`);
123
+ }
124
+ ```
125
+
126
+ ## A route that only redirects
127
+
128
+ The redirect itself is stored, so it costs no render at all:
129
+
130
+ ```text
131
+ ○ /old-pricing (redirects to /pricing)
132
+ ```
133
+
134
+ The build writes the status and the location, and the host answers from that
135
+ file — a document gets the status code, a navigation gets `X-RSC-Redirect` and
136
+ does it as an SPA navigation. Nothing is rendered per request, and a static
137
+ export can carry it.
138
+
139
+ ## Loops
140
+
141
+ A navigation follows at most **8** redirects before throwing. A page that
142
+ redirects to itself is a mistake someone will make, and without a ceiling it is
143
+ an unbounded run of full renders rather than an error you can see.
@@ -0,0 +1,66 @@
1
+ # Headers and cookies
2
+
3
+ > Setting response headers and cookies during a render.
4
+
5
+ ## Reading
6
+
7
+ Anywhere in a server component or action:
8
+
9
+ ```tsx
10
+ import { headers, cookies, searchParams } from '@rsc-kit/core/request'
11
+
12
+ export default async function Page() {
13
+ const h = await headers()
14
+ const jar = await cookies()
15
+
16
+ return <p>Hello {jar.get('name')?.value ?? 'stranger'}</p>
17
+ }
18
+ ```
19
+
20
+ ## Writing
21
+
22
+ ```tsx
23
+ import { responseHeaders, cookies } from '@rsc-kit/core/request'
24
+
25
+ export async function middleware() {
26
+ responseHeaders().set('X-Frame-Options', 'DENY')
27
+
28
+ const jar = await cookies()
29
+ jar.set('last-seen', new Date().toISOString(), { httpOnly: true, path: '/' })
30
+ }
31
+ ```
32
+
33
+ **Writing only works in middleware.** Middleware runs before the render, while
34
+ the response line has not been sent yet. A component runs *during* streaming,
35
+ when the headers are already on the wire — writing from one throws rather than
36
+ being silently dropped, so you find out immediately.
37
+
38
+ A redirect carries them too, which is what lets middleware remember where
39
+ someone was going before sending them to log in:
40
+
41
+ ```tsx
42
+ export async function middleware() {
43
+ const jar = await cookies()
44
+
45
+ if (!jar.get('session')) {
46
+ jar.set('intended', '/dashboard', { path: '/' })
47
+ redirect('/login')
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## Cookie options
53
+
54
+ ```ts
55
+ jar.set('name', 'value', {
56
+ httpOnly: true,
57
+ secure: true,
58
+ sameSite: 'lax', // 'strict' | 'lax' | 'none'
59
+ path: '/',
60
+ maxAge: 60 * 60 * 24,
61
+ expires: new Date('2027-01-01'),
62
+ })
63
+ ```
64
+
65
+ Names are validated as cookie tokens and `sameSite` / `expires` are checked, so
66
+ a typo is an error rather than a header the browser quietly ignores.
@@ -0,0 +1,206 @@
1
+ # Route interception
2
+
3
+ > Opening a route as a modal over the page you were on.
4
+
5
+ Route interception lets you load a route from a different part of your app within the current layout. When a user clicks a link, the intercepted component renders in a parallel slot (like a modal) while the current page stays visible behind it. On hard navigation (refresh or direct URL), the normal page renders instead.
6
+
7
+ This follows the same convention as Next.js — using `(.)folder`, `(..)folder`, and `(...)folder` prefixes.
8
+
9
+ ## Convention
10
+
11
+ | Prefix | Intercepts |
12
+ | --- | --- |
13
+ | (.)folder | Same level — matches a sibling route |
14
+ | (..)folder | One level up — matches a route in the parent segment |
15
+ | (...)folder | Root level — matches a route from the app root |
16
+
17
+ ## A photo modal
18
+
19
+ The most common use case is showing content in a modal on SPA navigation, with the full page as a fallback on hard navigation.
20
+
21
+ ### File Structure
22
+
23
+ ```tsx title="File structure"
24
+ src/app/
25
+ ├── layout.tsx ← renders {children} + {modal}
26
+ ├── page.tsx ← feed page
27
+ ├── @modal/
28
+ │ ├── default.tsx ← empty by default (no modal open)
29
+ │ └── (.)photo/[id]/
30
+ │ └── page.tsx ← photo modal (interceptor)
31
+ └── photo/[id]/
32
+ └── page.tsx ← full photo page (hard nav)
33
+ ```
34
+
35
+ ### Root Layout
36
+
37
+ The layout receives the `modal` parallel slot as a prop. No special wrapper component needed — just render it directly:
38
+
39
+ ```tsx title="app/layout.tsx"
40
+ export default function Layout({
41
+ children,
42
+ modal,
43
+ }: {
44
+ children: React.ReactNode;
45
+ modal: React.ReactNode;
46
+ }) {
47
+ return (
48
+ <div>
49
+ <main>{children}</main>
50
+ {modal}
51
+ </div>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ### Default Slot
57
+
58
+ The `@modal/default.tsx` renders when no interception is active:
59
+
60
+ ```tsx title="app/@modal/default.tsx"
61
+ export default function ModalDefault() {
62
+ return null;
63
+ }
64
+ ```
65
+
66
+ ### Interceptor Component
67
+
68
+ The interceptor at `@modal/(.)photo/[id]/page.tsx` receives the target route's params. It can be a server component — only the frame around it needs to be interactive:
69
+
70
+ ```tsx title="app/@modal/(.)photo/[id]/page.tsx"
71
+ import { ModalShell } from '../../../components/ModalShell';
72
+ import { findPhoto } from '../../../../data';
73
+
74
+ export default async function PhotoModal({ id }: { id: string }) {
75
+ const photo = await findPhoto(id);
76
+
77
+ return (
78
+ <ModalShell>
79
+ <h2>{photo.title}</h2>
80
+ <p>This renders in a modal on SPA navigation.</p>
81
+ </ModalShell>
82
+ );
83
+ }
84
+ ```
85
+
86
+ Read the same data the full page reads. An interceptor pointed at a
87
+ different source than the page it stands in for looks exactly like a
88
+ broken interception: the modal opens correctly and says the record does
89
+ not exist.
90
+
91
+ ### Closing It
92
+
93
+ The engine puts the interceptor in the slot; the affordances for dismissing it are yours to write. A modal needs three, and the last two are the ones that get forgotten — a close control, the `Escape` key, and a click on the backdrop:
94
+
95
+ ```tsx title="components/ModalShell.tsx"
96
+ "use client";
97
+
98
+ import { useEffect, useRef } from 'react';
99
+ import type { ReactNode } from 'react';
100
+
101
+ export function ModalShell({ children }: { children: ReactNode }) {
102
+ const close = useRef<HTMLButtonElement>(null);
103
+
104
+ useEffect(() => {
105
+ // The only way to dismiss a dialog without a pointer.
106
+ const onKey = (event: KeyboardEvent) => {
107
+ if (event.key === 'Escape') history.back();
108
+ };
109
+
110
+ window.addEventListener('keydown', onKey);
111
+ close.current?.focus();
112
+
113
+ return () => window.removeEventListener('keydown', onKey);
114
+ }, []);
115
+
116
+ return (
117
+ <div className="modal-backdrop" onClick={() => history.back()}>
118
+ {/* A click inside the dialog is not a click on the backdrop. */}
119
+ <article role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
120
+ <button ref={close} aria-label="Close" onClick={() => history.back()}>
121
+ ×
122
+ </button>
123
+ {children}
124
+ </article>
125
+ </div>
126
+ );
127
+ }
128
+ ```
129
+
130
+ Closing is `history.back()` rather than a link to a fixed URL. The modal was opened by pushing a history entry over the page beneath, so going back is both what the browser's own back button does and what returns to whatever page it was opened *over* — a hardcoded destination is wrong the moment the same modal is reachable from two places.
131
+
132
+ The router recognises that as leaving an interception and empties the slot without asking the server for anything. The page underneath never left, so closing costs no request and everything typed into it is still there.
133
+
134
+ A modal reachable from exactly one place can link to that place instead, and a real `<Link>` is worth keeping for it — a link can be opened in a new tab, and it still works before the page hydrates. `history.back()` is for a shell used from several.
135
+
136
+ ### Full Page (Hard Nav)
137
+
138
+ When a user navigates directly to `/photo/123` (hard refresh, shared link), the normal page renders:
139
+
140
+ ```tsx title="app/photo/[id]/page.tsx"
141
+ export default function PhotoPage({ id }: { id: string }) {
142
+ return (
143
+ <div>
144
+ <h1>Photo {id}</h1>
145
+ <p>Full photo page — shown on direct navigation or refresh.</p>
146
+ </div>
147
+ );
148
+ }
149
+ ```
150
+
151
+ ## How it works
152
+
153
+ On **SPA navigation** (clicking a Link):
154
+
155
+ - The client checks the intercept manifest (generated at build time)
156
+ - If the target URL matches an intercept pattern, headers are added to the request
157
+ - The server resolves the current page from the referer URL, to know which slot the interceptor belongs in
158
+ - It renders the interceptor *alone*, and says so with `X-RSC-Revalidate: modal`
159
+ - The client puts it in that slot. The page underneath is never re-rendered — so a half-filled form on it is still half-filled
160
+
161
+ On **hard navigation** (direct URL, refresh):
162
+
163
+ - Normal route matching — `/photo/123` renders `photo/[id]/page.tsx`
164
+ - No interception — the full photo page renders
165
+
166
+ Re-rendering the page underneath would put the modal on screen at the
167
+ cost of rebuilding everything below the layout that declares the slot.
168
+ That page is already mounted and still correct; only the slot is new.
169
+
170
+ ## Nested interception
171
+
172
+ Use `(..)` to intercept routes one level up, or `(...)` to intercept from the app root:
173
+
174
+ ```tsx title="Intercepting from a nested page"
175
+ // From /feed, intercept /photo/[id] at the same level
176
+ app/@modal/(.)photo/[id]/page.tsx
177
+
178
+ // From /feed/trending, intercept /photo/[id] one level up
179
+ app/feed/@modal/(..)photo/[id]/page.tsx
180
+
181
+ // From /dashboard/settings, intercept /photo/[id] from root
182
+ app/dashboard/settings/@drawer/(...)photo/[id]/page.tsx
183
+ ```
184
+
185
+ ## Several slots
186
+
187
+ You can intercept the same route into different slots:
188
+
189
+ ```tsx title="File structure"
190
+ app/
191
+ ├── @modal/(.)photo/[id]/page.tsx ← renders in "modal" slot
192
+ ├── @preview/(.)photo/[id]/page.tsx ← renders in "preview" slot
193
+ └── photo/[id]/page.tsx ← full page
194
+ ```
195
+
196
+ ## Seeing it work
197
+
198
+ The example app in the repository has a working one: `@modal/(.)posts/[slug]`
199
+ over a feed of posts. Click a post to open it in a modal, then refresh to get
200
+ the same route as a page.
201
+
202
+ ---
203
+
204
+ Route interception builds on [parallel routes](/guides/routing). The `@folder`
205
+ convention is worth being comfortable with first — an interceptor is a page
206
+ inside a slot, and everything a slot does applies to it.