@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/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
- Every icon is marked `any maskable`, because without it Android crops a square
54
- icon into a circle and takes the corners off whatever was in them.
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
@@ -29,8 +29,8 @@ export const env = createEnv({
29
29
  },
30
30
  clientPrefix: 'PUBLIC_',
31
31
  client: {},
32
- // No bare process.env: a "use client" file importing this for a PUBLIC_
33
- // value has no process, and the spread would throw before the first render.
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.
@@ -24,13 +24,13 @@ import { createPost } from '../actions';
24
24
  export function PostForm() {
25
25
  return (
26
26
  <Form action={createPost}>
27
- {({ errors, pending }) => (
27
+ {({ error, pending }) => (
28
28
  <>
29
29
  <input name="title" />
30
- {errors.title?.map((message) => <p key={message} className="error">{message}</p>)}
30
+ {error('title') && <p className="error">{error('title')}</p>}
31
31
 
32
32
  <textarea name="body" />
33
- {errors.body?.map((message) => <p key={message} className="error">{message}</p>)}
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
- {({ errors, pending }) => (
94
+ {({ error, pending }) => (
95
95
  <>
96
96
  <input name="title" />
97
- {errors.title?.map((m) => <p key={m} className="error">{m}</p>)}
97
+ {error('title') && <p className="error">{error('title')}</p>}
98
98
 
99
99
  <textarea name="body" />
100
- {errors.body?.map((m) => <p key={m} className="error">{m}</p>)}
100
+ {error('body') && <p className="error">{error('body')}</p>}
101
101
 
102
102
  <button disabled={pending}>Create</button>
103
103
  </>
@@ -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 --keep-names .output/server/compile.mjs
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
- **`--keep-names` is not optional.** `--compile` implies `--production`, which
86
- minifies — and minifying renames every function. A partially prerendered page
87
- is finished by replaying a tree the build recorded by component *name*, so a
88
- binary that renamed them finds the wrong name in every slot:
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
- ```text
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/mcp",
3
- "version": "0.20.12",
3
+ "version": "0.21.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",