@rsc-kit/mcp 0.20.12 → 0.21.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 +121 -72
- package/dist/recipes.js.map +1 -1
- package/guides/coming-from-inertia.md +4 -4
- package/guides/coming-from-next.md +4 -3
- package/guides/deployment.md +15 -0
- package/guides/edge-caching.md +7 -2
- package/guides/forms.md +64 -67
- package/guides/getting-started.md +1 -1
- package/guides/go.md +4 -3
- package/guides/installation.md +5 -3
- package/guides/instrumentation.md +18 -9
- package/guides/laravel.md +8 -0
- package/guides/metadata.md +12 -0
- package/guides/navigation.md +15 -7
- package/guides/no-javascript.md +7 -0
- package/guides/ppr.md +17 -0
- package/guides/pwa.md +193 -2
- package/guides/quick-start.md +12 -3
- package/guides/static-generation.md +4 -0
- package/guides/validation.md +6 -6
- package/guides/where-it-runs.md +6 -16
- package/package.json +1 -1
package/guides/pwa.md
CHANGED
|
@@ -6,6 +6,25 @@ An installable app is a manifest plus a service worker. The
|
|
|
6
6
|
[offline guide](/guides/offline/) covers the worker; this covers everything
|
|
7
7
|
that makes the app feel like one the operating system knows about.
|
|
8
8
|
|
|
9
|
+
## Starting one installable
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun create rsc-kit@latest my-app --pwa
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or answer yes to *Installable app* when it asks. It turns on `offline`, and
|
|
16
|
+
writes a manifest, a static `/offline` page and four starter icons — `icon-192`,
|
|
17
|
+
`icon-512`, `icon-maskable-512` and `apple-icon`. They are files, found by name,
|
|
18
|
+
so the app is installable the moment it exists: Chrome's own installability
|
|
19
|
+
check finds nothing missing, and with the network cut the home page still loads
|
|
20
|
+
and a page never visited lands on `/offline`. Replace the icons with your own,
|
|
21
|
+
keeping the names. Nothing in rsc-kit draws, resizes or encodes an image — not
|
|
22
|
+
at build, not in dev, not at runtime.
|
|
23
|
+
|
|
24
|
+
It is off by default, and that is deliberate: a service worker outlives the
|
|
25
|
+
code that installed it, so it is a decision to take on purpose rather than one
|
|
26
|
+
a template takes for you.
|
|
27
|
+
|
|
9
28
|
## The manifest
|
|
10
29
|
|
|
11
30
|
A service worker makes an app survive a dead network. It does not make a
|
|
@@ -16,6 +35,7 @@ is a file beside your routes:
|
|
|
16
35
|
import type { WebManifest } from '@rsc-kit/core/manifest-file'
|
|
17
36
|
|
|
18
37
|
export default {
|
|
38
|
+
id: '/',
|
|
19
39
|
name: 'Orders',
|
|
20
40
|
shortName: 'Orders',
|
|
21
41
|
themeColor: '#0b0b0c',
|
|
@@ -24,6 +44,20 @@ export default {
|
|
|
24
44
|
} satisfies WebManifest
|
|
25
45
|
```
|
|
26
46
|
|
|
47
|
+
**Set `id` once and never change it.** It is the app's identity. Without it a
|
|
48
|
+
browser derives one from `startUrl` — change that later and everyone who
|
|
49
|
+
installed is left with an app their device no longer recognises as yours. `'/'`
|
|
50
|
+
is right for almost everyone; the build warns when it is missing.
|
|
51
|
+
|
|
52
|
+
The rest of the spec is typed as well, in its own names: `scope`, `orientation`,
|
|
53
|
+
`categories`, and `shortcuts` — the jumps a long press on the icon offers:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
shortcuts: [{ name: 'New order', shortName: 'New', url: '/orders/new', icons: ['icon-96.png'] }],
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Anything else the spec allows goes through `other`.
|
|
60
|
+
|
|
27
61
|
A file rather than a `vite.config.ts` key, and rather than `metadata` in a
|
|
28
62
|
layout. It is not build configuration — it is one more thing the app declares
|
|
29
63
|
about itself, so it lives where the app is, next to `layout.tsx` and
|
|
@@ -50,8 +84,13 @@ the filename** — `icon-192.png` and `icon-192x192.png` both mean 192. Declarin
|
|
|
50
84
|
the size in the config as well would be the same number written twice, and the
|
|
51
85
|
one that drifts is the one nobody looks at.
|
|
52
86
|
|
|
53
|
-
|
|
54
|
-
icon
|
|
87
|
+
Each icon has one purpose, read from its name. An ordinary icon is `any`. One
|
|
88
|
+
named `icon-maskable-512.png` is `maskable`: drawn with a safe zone — a
|
|
89
|
+
launcher only promises to show the middle 80% — so Android can mask it to its
|
|
90
|
+
own shape without cutting anything off. It goes in the manifest and never in
|
|
91
|
+
`<head>`, where the padding would make it the small icon in the tab. One file
|
|
92
|
+
marked both, which is what this used to do, is either shrunk or cropped, and
|
|
93
|
+
the Android splash screen drew the shrunk one.
|
|
55
94
|
|
|
56
95
|
### The build tells you if it will not work
|
|
57
96
|
|
|
@@ -61,6 +100,7 @@ install it. Nothing is wrong and nothing works.
|
|
|
61
100
|
|
|
62
101
|
```text
|
|
63
102
|
[rsc-kit] manifest: no icons, so no browser will offer to install this. Add a 192px and a 512px png.
|
|
103
|
+
[rsc-kit] manifest: no id, so the app's identity is its startUrl - change that and everyone who installed has a different app.
|
|
64
104
|
[rsc-kit] manifest: Orders is installable
|
|
65
105
|
```
|
|
66
106
|
|
|
@@ -68,6 +108,157 @@ A warning rather than a failed build — the manifest still does its other job
|
|
|
68
108
|
but it is said out loud, because someone who wrote `manifest: {…}` meant
|
|
69
109
|
installable and would otherwise find out months later.
|
|
70
110
|
|
|
111
|
+
## An install button
|
|
112
|
+
|
|
113
|
+
Chrome, Edge and Android make an offer — the `beforeinstallprompt` event — once,
|
|
114
|
+
whenever the page qualifies. Nothing guarantees that is after your component's
|
|
115
|
+
effect has run, and an app listening in `useEffect` could miss it for good,
|
|
116
|
+
with no way to offer *Install* until a reload. So the page catches it in its
|
|
117
|
+
inline bootstrap, ahead of the runtime, and `useInstall()` hands it over:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
'use client'
|
|
121
|
+
import { useInstall } from '@rsc-kit/core/install'
|
|
122
|
+
|
|
123
|
+
export function InstallButton() {
|
|
124
|
+
const { canInstall, install, installed, ios } = useInstall()
|
|
125
|
+
|
|
126
|
+
if (installed) return null
|
|
127
|
+
if (canInstall) return <button onClick={install}>Install</button>
|
|
128
|
+
if (ios) return <p>Tap Share, then Add to Home Screen.</p>
|
|
129
|
+
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **`canInstall`** — the browser has offered; `install()` shows its dialog and
|
|
135
|
+
resolves with `accepted` or `dismissed`. An offer can be shown once, so after
|
|
136
|
+
that it is `unavailable` until the browser makes another.
|
|
137
|
+
- **`ios`** — Safari on an iPhone or iPad. iOS makes no offer and exposes no
|
|
138
|
+
way to install from a page: the visitor does it from the Share menu, and the
|
|
139
|
+
most the app can do is say so.
|
|
140
|
+
- **`installed`** — running as the installed app, or installed while this page
|
|
141
|
+
was open. For hiding the button, or anything that should differ in the app's
|
|
142
|
+
own window.
|
|
143
|
+
|
|
144
|
+
The browser's own install UI is left alone, so an app with no button of its own
|
|
145
|
+
loses nothing. The capture is only in apps that declare a manifest — nothing
|
|
146
|
+
else can be installed, so nothing else pays for it.
|
|
147
|
+
|
|
148
|
+
## The richer install sheet
|
|
149
|
+
|
|
150
|
+
Chrome shows a plain install dialog by default, and a much fuller one — with
|
|
151
|
+
screenshots, like an app store listing — when the manifest has them. The same
|
|
152
|
+
bargain as the icons: name the files by their size, and the build reads the
|
|
153
|
+
rest.
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
src/app/
|
|
157
|
+
screenshot-wide-1280x720.png desktop
|
|
158
|
+
screenshot-narrow-750x1334.png a phone
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each becomes a manifest `screenshots` entry with its `sizes` and a `form_factor`
|
|
162
|
+
of `wide` or `narrow`. Nothing opens the file. Chrome wants at least one wide
|
|
163
|
+
screenshot for the desktop sheet and one narrow one for a phone; the build
|
|
164
|
+
mentions them when an otherwise installable app has none.
|
|
165
|
+
|
|
166
|
+
## Splash screens
|
|
167
|
+
|
|
168
|
+
What shows while an installed app starts. The two platforms do it differently,
|
|
169
|
+
and neither needs anything run over your images — you make them, the build
|
|
170
|
+
links them.
|
|
171
|
+
|
|
172
|
+
### Android, from the manifest
|
|
173
|
+
|
|
174
|
+
Chrome and Edge draw the splash themselves from three things the manifest
|
|
175
|
+
already has: the `name`, the largest icon, and `backgroundColor`. Give it a
|
|
176
|
+
512px icon and a background colour and it is done. The build warns about
|
|
177
|
+
either missing:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
[rsc-kit] manifest: no 512px icon, so a splash screen will be upscaled from a smaller one.
|
|
181
|
+
[rsc-kit] manifest: no backgroundColor, so the Android splash screen is white.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### iOS, from files named by their size
|
|
185
|
+
|
|
186
|
+
Safari ignores the manifest for this. It wants one image per device and
|
|
187
|
+
orientation, each behind a media query naming the device's size and pixel
|
|
188
|
+
ratio — and with no match it shows a blank screen. A query written by hand
|
|
189
|
+
with the ratio off by one matches nothing, silently.
|
|
190
|
+
|
|
191
|
+
So the image carries its size in its name, and the query comes from a table:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
src/app/
|
|
195
|
+
apple-splash-1179x2556.png iPhone 14 Pro, 15, 15 Pro, 16 — portrait
|
|
196
|
+
apple-splash-2556x1179.png the same phones — landscape
|
|
197
|
+
apple-splash-1290x2796.png 14 Pro Max, 15 Plus / Pro Max, 16 Plus
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Each becomes `<link rel="apple-touch-startup-image" media="…">` with the right
|
|
201
|
+
query, and the page says it launches as an app, which is when Safari shows one.
|
|
202
|
+
Nothing opens the file — no decoding, no resizing, at build, in dev or at
|
|
203
|
+
runtime. Make the images however you like, for the devices you care about;
|
|
204
|
+
every other device gets the background colour.
|
|
205
|
+
|
|
206
|
+
| Size (portrait) | Devices |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| 1320×2868 | iPhone 16 Pro Max |
|
|
209
|
+
| 1206×2622 | iPhone 16 Pro |
|
|
210
|
+
| 1290×2796 | iPhone 14 Pro Max, 15 Plus, 15 Pro Max, 16 Plus |
|
|
211
|
+
| 1179×2556 | iPhone 14 Pro, 15, 15 Pro, 16 |
|
|
212
|
+
| 1284×2778 | iPhone 12 Pro Max, 13 Pro Max, 14 Plus |
|
|
213
|
+
| 1170×2532 | iPhone 12, 12 Pro, 13, 13 Pro, 14 |
|
|
214
|
+
| 1242×2688 | iPhone XS Max, 11 Pro Max |
|
|
215
|
+
| 1125×2436 | iPhone X, XS, 11 Pro, 12 mini, 13 mini |
|
|
216
|
+
| 828×1792 | iPhone XR, 11 |
|
|
217
|
+
| 1242×2208 | iPhone 6, 7, 8 Plus |
|
|
218
|
+
| 750×1334 | iPhone 6, 7, 8, SE (2nd and 3rd) |
|
|
219
|
+
| 640×1136 | iPhone SE (1st) |
|
|
220
|
+
| 2064×2752 | iPad Pro 13" (M4) |
|
|
221
|
+
| 2048×2732 | iPad Pro 12.9" |
|
|
222
|
+
| 1668×2420 | iPad Pro 11" (M4) |
|
|
223
|
+
| 1668×2388 | iPad Pro 11" |
|
|
224
|
+
| 1640×2360 | iPad Air 10.9", iPad (10th) |
|
|
225
|
+
| 1668×2224 | iPad Pro 10.5", iPad Air (3rd) |
|
|
226
|
+
| 1620×2160 | iPad 10.2" |
|
|
227
|
+
| 1488×2266 | iPad mini (6th) |
|
|
228
|
+
| 1536×2048 | iPad mini, iPad Air, iPad 9.7" |
|
|
229
|
+
|
|
230
|
+
Swap the numbers for landscape. A size no device has is served but linked from
|
|
231
|
+
no page, and the build names it rather than dropping it:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
[rsc-kit] apple-splash: apple-splash-1000x2000.png matches no iPhone or iPad screen, so no page links to it.
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
What it costs: a page gets one `<link>` per image — a full set is about 4 KB
|
|
238
|
+
before compression and well under 1 KB after, since the tags differ only in
|
|
239
|
+
numbers. Other browsers ignore them, and iOS fetches at most the one that
|
|
240
|
+
matches. An app with no `apple-splash-*` files gets no tags at all.
|
|
241
|
+
|
|
242
|
+
### Or in metadata, the way Next writes it
|
|
243
|
+
|
|
244
|
+
A port from Next keeps its `appleWebApp` unchanged — the same shape, rendered
|
|
245
|
+
as the same tags:
|
|
246
|
+
|
|
247
|
+
```ts title="src/app/layout.tsx"
|
|
248
|
+
export const metadata: Metadata = {
|
|
249
|
+
appleWebApp: {
|
|
250
|
+
capable: true,
|
|
251
|
+
title: 'Orders',
|
|
252
|
+
statusBarStyle: 'black-translucent',
|
|
253
|
+
startupImage: [{ url: '/splash-1179x2556.png', media: '(device-width: 393px) and (-webkit-device-pixel-ratio: 3)' }],
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`appleWebApp: true` is `{ capable: true }`. The files above are the easier way
|
|
259
|
+
to supply launch screens — this is for an app that already has its queries
|
|
260
|
+
written.
|
|
261
|
+
|
|
71
262
|
## Your own worker code
|
|
72
263
|
|
|
73
264
|
Push, notification clicks and background sync are events the generated worker
|
package/guides/quick-start.md
CHANGED
|
@@ -29,8 +29,8 @@ export const env = createEnv({
|
|
|
29
29
|
},
|
|
30
30
|
clientPrefix: 'PUBLIC_',
|
|
31
31
|
client: {},
|
|
32
|
-
// No bare process.env:
|
|
33
|
-
//
|
|
32
|
+
// No bare process.env: import.meta.env carries the PUBLIC_ values, and
|
|
33
|
+
// the file does not throw in the browser if a client component imports it.
|
|
34
34
|
runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },
|
|
35
35
|
emptyStringAsUndefined: true,
|
|
36
36
|
})
|
|
@@ -43,6 +43,11 @@ one has to start with `PUBLIC_`. That is [`@t3-oss/env-core`](https://env.t3.gg)
|
|
|
43
43
|
with the schema in whichever library you chose. `.env.example` is written
|
|
44
44
|
beside it and is the one that gets committed.
|
|
45
45
|
|
|
46
|
+
Import `env` from server code. A client component reads its `PUBLIC_` value
|
|
47
|
+
from `import.meta.env.PUBLIC_…` directly: importing `src/env.ts` there brings
|
|
48
|
+
the validator along to read one string, about 20 kB gzipped for Zod. The
|
|
49
|
+
value is still checked, because the server imports `env` when it starts.
|
|
50
|
+
|
|
46
51
|
```sh
|
|
47
52
|
bun create rsc-kit@latest my-app --validation=valibot --env # scripted
|
|
48
53
|
```
|
|
@@ -76,7 +81,11 @@ bunx rsc-kit@latest init
|
|
|
76
81
|
```
|
|
77
82
|
|
|
78
83
|
It reads your `package.json`, works out which server you use and where your
|
|
79
|
-
source lives, and writes only what is missing
|
|
84
|
+
source lives, and writes only what is missing. A project with no
|
|
85
|
+
`package.json` — a Go or Rust service getting its frontend — is given its
|
|
86
|
+
first one. And in an empty directory, or a fresh clone of an empty repository,
|
|
87
|
+
there is nothing to add to, so `init` starts a new app right there, leaving the
|
|
88
|
+
README alone and adding what the app needs ignored to the `.gitignore`:
|
|
80
89
|
|
|
81
90
|
```
|
|
82
91
|
server hono
|
|
@@ -21,6 +21,10 @@ The budget is 2 seconds, and `RSC_PPR_TIMEOUT_MS` changes it. A page whose data
|
|
|
21
21
|
resolves inside it is stored whole — including one that called `fetch()`, as
|
|
22
22
|
long as the answer arrived.
|
|
23
23
|
|
|
24
|
+
Waiting does not hold up the build. A page that has rendered what it can and is
|
|
25
|
+
only waiting steps aside for the next one, so hundreds of shells cost about one
|
|
26
|
+
budget between them, not one each.
|
|
27
|
+
|
|
24
28
|
There is no way to opt out, and that is the point: a route the build cannot
|
|
25
29
|
store has its boundary in the wrong place. Put the part that waits inside
|
|
26
30
|
`<Suspense>`, or add a `loading.tsx` beside the page, and it becomes a shell.
|
package/guides/validation.md
CHANGED
|
@@ -24,13 +24,13 @@ import { createPost } from '../actions';
|
|
|
24
24
|
export function PostForm() {
|
|
25
25
|
return (
|
|
26
26
|
<Form action={createPost}>
|
|
27
|
-
{({
|
|
27
|
+
{({ error, pending }) => (
|
|
28
28
|
<>
|
|
29
29
|
<input name="title" />
|
|
30
|
-
{
|
|
30
|
+
{error('title') && <p className="error">{error('title')}</p>}
|
|
31
31
|
|
|
32
32
|
<textarea name="body" />
|
|
33
|
-
{
|
|
33
|
+
{error('body') && <p className="error">{error('body')}</p>}
|
|
34
34
|
|
|
35
35
|
<button disabled={pending}>Create</button>
|
|
36
36
|
</>
|
|
@@ -91,13 +91,13 @@ const schema = z.object({
|
|
|
91
91
|
export function PostForm() {
|
|
92
92
|
return (
|
|
93
93
|
<Form action={createPost} schema={schema}>
|
|
94
|
-
{({
|
|
94
|
+
{({ error, pending }) => (
|
|
95
95
|
<>
|
|
96
96
|
<input name="title" />
|
|
97
|
-
{
|
|
97
|
+
{error('title') && <p className="error">{error('title')}</p>}
|
|
98
98
|
|
|
99
99
|
<textarea name="body" />
|
|
100
|
-
{
|
|
100
|
+
{error('body') && <p className="error">{error('body')}</p>}
|
|
101
101
|
|
|
102
102
|
<button disabled={pending}>Create</button>
|
|
103
103
|
</>
|
package/guides/where-it-runs.md
CHANGED
|
@@ -65,7 +65,7 @@ Bun only, and the whole application ends up inside one file — engine, route
|
|
|
65
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
|
|
|
@@ -82,22 +82,12 @@ a binary one version behind the source with nothing to say so — and on a
|
|
|
82
82
|
project that has never been built, an `ENOENT` naming a path the app did not
|
|
83
83
|
write.
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
Partially prerendered pages are resumed inside the binary exactly as they are
|
|
86
|
+
by the built server. Compiling merges every module into one scope and renames
|
|
87
|
+
whatever collides — `--keep-names` does not prevent that — so the build pins
|
|
88
|
+
each component's name as a `displayName` the shell can rely on. No flag.
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
Expected the resume to render <J> in this slot but instead it rendered <J2>.
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
React then falls back to client rendering for every hole, and the page arrives
|
|
95
|
-
looking finished with nothing wired up. `--keep-names` preserves the names the
|
|
96
|
-
build recorded, and costs a few kilobytes. A build with no PPR route never
|
|
97
|
-
notices; one with them fails only on a cold load, in production, which is the
|
|
98
|
-
worst way to find out.
|
|
99
|
-
|
|
100
|
-
`--target bun` is the default. `--bytecode`
|
|
90
|
+
`--compile` implies `--production`; `--target bun` is the default. `--bytecode`
|
|
101
91
|
compiles the JavaScript ahead of time for a faster cold start at the cost
|
|
102
92
|
of a larger binary — a production port measured 275 ms → 72 ms to the first
|
|
103
93
|
healthy response for 81 MB → 117 MB, and left it out, since on a pod the
|
package/package.json
CHANGED