@rsc-kit/mcp 0.20.13 → 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 CHANGED
@@ -15,28 +15,37 @@
15
15
  const RECIPES = [
16
16
  {
17
17
  topic: 'forms',
18
- summary: 'Submitting to a server action, with pending state and field errors. Uncontrolled by default - no useState per field',
18
+ summary: 'Submitting to a server action, with pending state and field errors. Uncontrolled by default; read with error() and field()',
19
19
  body: `THE RULE: forms are UNCONTROLLED. Inputs keep their value in the DOM,
20
20
  an initial value is defaultValue, the action reads FormData. Do NOT write
21
21
  useState + value/onChange per input, and do NOT reach for TanStack Form.
22
- Control ONE field only when the UI must react as the user types (a character
23
- count, a live preview, a dependent select) - bind it with useField, which
24
- scopes the re-render to that field. Everything else stays uncontrolled.
22
+
23
+ ONE WAY TO READ A FORM - two words, the same everywhere:
24
+ error('title') the field's error, or undefined. !!error('title') is invalid.
25
+ Nested fields are paths: error('address.city'), error('items[0].name').
26
+ field('title') a binding to spread - ONLY for a field whose value the UI must
27
+ hold (a character count, a live preview, a control with no
28
+ native element). Everything else stays uncontrolled.
29
+ Beside them: pending, dirty, succeeded, recentlySucceeded, reset(), clearErrors(),
30
+ and data (what is being submitted).
31
+
32
+ There is nothing else. No errors map, no fieldState, no useFormValues, no
33
+ useFormStatus - those were removed; if you see them in old code, convert them.
25
34
 
26
35
  Use <Form>. It takes the server action itself, not a url.
27
36
 
28
37
  \`\`\`tsx
29
38
  'use client'
30
- import Form from '@rsc-kit/core/Form'
39
+ import Form from '@rsc-kit/core/form'
31
40
  import { createPost } from '../actions'
32
41
 
33
42
  export function NewPost() {
34
43
  return (
35
44
  <Form action={createPost} schema={schema}>
36
- {({ pending, errors }) => (
45
+ {({ pending, error }) => (
37
46
  <>
38
47
  <input name="title" />
39
- {errors.title?.[0] && <p>{errors.title[0]}</p>}
48
+ {error('title') && <p>{error('title')}</p>}
40
49
  <button disabled={pending}>Save</button>
41
50
  </>
42
51
  )}
@@ -45,6 +54,20 @@ export function NewPost() {
45
54
  }
46
55
  \`\`\`
47
56
 
57
+ In a child component, useForm() returns EXACTLY what the render prop gets, so
58
+ the code is the same:
59
+
60
+ \`\`\`tsx
61
+ import { useForm } from '@rsc-kit/core/form'
62
+
63
+ function SubmitButton() {
64
+ const { pending } = useForm()
65
+ return <button disabled={pending}>Save</button>
66
+ }
67
+ \`\`\`
68
+
69
+ (Not React's useFormStatus - a different hook with a different answer.)
70
+
48
71
  Passing \`schema\` validates in the browser BEFORE the action is called, so a
49
72
  mistake costs no round trip. It is a courtesy, never a control: the action is a
50
73
  public endpoint reachable without your form, so the server must check too.
@@ -52,6 +75,10 @@ public endpoint reachable without your form, so the server must check too.
52
75
  A schema on the server (\`client.input(schema)\`) does NOT give you client-side
53
76
  validation. Pass it to the form as well — the same schema is fine.
54
77
 
78
+ A field is checked when it is LEFT, not as it is typed (the form listens for
79
+ focusout, so uncontrolled fields are covered), and on submit. So error() has
80
+ nothing for a field nobody reached - there is no "touched" to check first.
81
+
55
82
  Values are uncontrolled, so an initial one is React's own \`defaultValue\`. A
56
83
  refused submit keeps what was typed, because the DOM kept it.
57
84
 
@@ -65,85 +92,65 @@ z.array() accepts - so for anything that is a list by nature end the name in
65
92
 
66
93
  Names that describe a shape build it: \`address.city\` nests, and
67
94
  \`items[0].name\` (or \`items[0][name]\`) makes an array of objects. That is
68
- the shape the schema was written against, and errors come back keyed the same
69
- way because Standard Schema issue paths join with dots too.
95
+ the shape the schema was written against, and error() takes the same path.
70
96
 
71
- For a control with no native element behind it - a rich editor, a Radix select -
72
- or a value read as it is typed, bind it with \`field()\`. It is the same four
73
- props react-hook-form's Controller gives:
97
+ Binding a field - a rich editor, a Radix select, a value read as it is typed:
74
98
 
75
99
  \`\`\`tsx
76
100
  <Form action={save} defaultValues={{ body: '' }}>
77
- {({ field }) => (
101
+ {({ field, error }) => (
78
102
  <>
79
103
  <Editor {...field('body')} />
80
104
  <span>{field('body').value.length}/100</span>
105
+ {error('body') && <p>{error('body')}</p>}
81
106
  </>
82
107
  )}
83
108
  </Form>
84
109
  \`\`\`
85
110
 
86
- onChange takes a DOM event OR a bare value, so native inputs and Radix
87
- components both work. A bound field is still an ordinary named input, so it
88
- arrives in FormData with the rest - nothing merges.
111
+ field() gives { name, value, onChange, onBlur } - react-hook-form Controller's
112
+ four. onChange takes a DOM event OR a bare value. A bound field is still an
113
+ ordinary named input, so it arrives in FormData with the rest.
89
114
 
90
- \`fieldState(name)\` is the other half: { touched, invalid, errors }. Two
91
- objects rather than one because touched and invalid are not DOM attributes and
92
- spreading them would warn on every field.
115
+ shadcn Field components, with the two words:
93
116
 
94
117
  \`\`\`tsx
95
- const title = fieldState('title')
96
- <Field data-invalid={title.invalid}>
97
- <Input {...field('title')} aria-invalid={title.invalid} />
98
- <FieldError errors={title.errors.map((message) => ({ message }))} />
118
+ <Field data-invalid={!!error('title')}>
119
+ <FieldLabel htmlFor="title">Title</FieldLabel>
120
+ <Input id="title" name="title" aria-invalid={!!error('title')} />
121
+ <FieldError>{error('title')}</FieldError>
99
122
  </Field>
100
123
  \`\`\`
101
124
 
102
- A field is checked when it is LEFT, not as it is typed, and it works on
103
- uncontrolled fields too - the form listens for focusout rather than each field
104
- listening for blur.
105
-
106
- There is no per-field render prop component here, and that is deliberate.
107
- TanStack Form is controlled-first, so it needs one - without per-field
108
- subscriptions a keystroke re-renders every field. react-hook-form is
109
- uncontrolled-first like this, and its Controller scopes the re-render of a
110
- controlled field to itself.
111
-
112
- field() is a function call instead, which keeps the markup flat and means a
113
- bound field re-renders the form rather than only itself. Right for the one or
114
- two controlled fields a form usually has.
125
+ FieldError renders its children and renders nothing when there are none.
115
126
 
116
- When it is not, put the field in its own component and use \`useField\` there -
117
- it re-renders that component and nothing else, which is what Controller achieves
118
- with a render prop:
127
+ A bound field re-renders the form. For a LARGE controlled form, put the field in
128
+ its own component and use useField - the same two words for one name, and it
129
+ re-renders that component and nothing else:
119
130
 
120
131
  \`\`\`tsx
121
132
  function Title() {
122
- const { invalid, errors, ...bound } = useField('title')
123
-
124
- return <Input {...bound} aria-invalid={invalid} />
133
+ const { field, error } = useField('title')
134
+ return <Input {...field} aria-invalid={!!error} />
125
135
  }
126
136
  \`\`\`
127
137
 
128
- \`useFormValues()\` reads every bound value from anywhere inside the form - a
129
- preview, a summary. Only BOUND values: an uncontrolled input's value is the
130
- DOM's and nothing can know it changed.
138
+ A preview or summary reads a bound value the same way:
139
+ \`const { field } = useField('title'); field.value\`. Only BOUND values can be
140
+ read - an uncontrolled input's value is the DOM's.
131
141
 
132
- Both read a context, so they work below <Form>. For something that is NOT a
133
- descendant - a top bar, a sidebar preview - create the store above both and
134
- hand it in:
142
+ For something that is NOT a descendant - a top bar - create the store above both
143
+ and hand it in; useField(name, store) reads it:
135
144
 
136
145
  \`\`\`tsx
137
146
  const store = useFormStore({ title: '' })
138
147
 
139
- <TopBar store={store} /> // outside the form
148
+ <TopBar store={store} /> // outside the form: useField('title', store)
140
149
  <Form action={save} store={store}>…</Form>
141
150
  \`\`\`
142
151
 
143
- useFormStore is the values and nothing else - no submit, no errors. Creating it
144
- does not subscribe to it, so the holder does not re-render per keystroke and
145
- take the subtree with it. useField(name, store) and useFormValues(store) take
146
- one explicitly; without one they read the context.
152
+ Creating the store does not subscribe to it, so the holder does not re-render
153
+ per keystroke.
147
154
 
148
155
  A submit from outside the form is html, not a second api:
149
156
 
@@ -152,21 +159,14 @@ A submit from outside the form is html, not a second api:
152
159
  <Button type="submit" form="bug-report">Submit</Button>
153
160
  \`\`\`
154
161
 
155
- There is no useForm hook. <Form> is the whole surface.
156
-
157
- Fields are real \`name\` attributes rather than controlled state, so the form
158
- reads a native FormData and any component rendering a real control works.
159
-
160
162
  It works before hydration. The action is on the form element as well as in the
161
- submit handler, so the markup is submittable on its own - the handler calls
162
- preventDefault() first and React does not run a form action for a cancelled
163
- submit, so exactly one path runs.
163
+ submit handler, so the markup is submittable on its own; a refusal shows on its
164
+ fields through error() even with no javascript.
164
165
 
165
166
  **shadcn/ui works as-is.** Input, Textarea, Button and Label are styled native
166
167
  elements, so \`name\` does what it always does. Select, Checkbox, Switch and
167
168
  RadioGroup are Radix underneath and render a hidden native control whenever
168
- given a \`name\` - omit it and they are invisible to the form, which is the
169
- only thing to remember.
169
+ given a \`name\` - omit it and they are invisible to the form.
170
170
 
171
171
  Do NOT use shadcn's own Form/FormField/FormControl with this. Those wrap
172
172
  react-hook-form, a different system for the same job. One or the other.`,
@@ -246,7 +246,13 @@ payload and every held page is dropped - they are from before the write - so
246
246
  visit() to a list after creating a row fetches the list with the row in it.
247
247
  What an action re-rendered goes under the page it was invoked on: a tap that
248
248
  left the page while the action was in flight is answered by fetching the page
249
- now showing again, whole, never by putting the old page under the new url.`,
249
+ now showing again, whole, never by putting the old page under the new url.
250
+
251
+ The client components a prefetched page renders are loaded (code only) as its
252
+ payload lands - the first eight per page, bounded per document - so the first
253
+ visit to a page does not wait on a chunk after the page arrived. Nothing to
254
+ configure. Serve over HTTP/2 (Cloudflare does by default): every client
255
+ component is its own small chunk.`,
250
256
  },
251
257
  {
252
258
  topic: 'validation',
@@ -516,7 +522,13 @@ not found" - it is the package's, and fixed.`,
516
522
  {
517
523
  topic: 'pwa',
518
524
  summary: 'Making the app installable',
519
- body: `A manifest file beside the routes:
525
+ body: `A NEW app: bun create rsc-kit@latest my-app --pwa. It turns on offline and
526
+ writes src/app/manifest.ts, a static src/app/offline/page.tsx and starter icons
527
+ (icon-192.png, icon-512.png, icon-maskable-512.png, apple-icon.png) - Chrome
528
+ reports it installable with nothing missing. Replace the icons, keep the names.
529
+ Nothing in rsc-kit processes images.
530
+
531
+ An EXISTING app - a manifest file beside the routes:
520
532
 
521
533
  \`\`\`ts title="src/app/manifest.ts"
522
534
  import type { WebManifest } from '@rsc-kit/core/manifest-file'
@@ -537,11 +549,37 @@ imported from elsewhere.
537
549
  favicon.ico served at /favicon.ico
538
550
  icon-192.png <link rel="icon">, and the manifest's icons
539
551
  icon-512.png
552
+ icon-maskable-512.png manifest only, purpose "maskable" (safe-zone padded)
540
553
  apple-icon.png <link rel="apple-touch-icon">
554
+ apple-splash-1179x2556.png iOS launch screen for that device, portrait
541
555
  opengraph-image.png <meta property="og:image">
542
556
  twitter-image.png <meta name="twitter:image">
543
557
 
544
- Sizes are read from the filename. The build says whether it worked:
558
+ screenshot-wide-1280x720.png manifest screenshots, form_factor wide (desktop)
559
+ screenshot-narrow-750x1334.png form_factor narrow (phone) - Chrome's richer install sheet
560
+
561
+ Sizes are read from the filename; no image is ever opened or resized.
562
+
563
+ Set id: '/' in the manifest and never change it - without it the app's identity
564
+ is its startUrl, and changing that orphans every install (the build warns).
565
+ scope, orientation, categories and shortcuts are typed too.
566
+
567
+ An install button: useInstall() from '@rsc-kit/core/install' ->
568
+ { canInstall, install, installed, ios }. The offer (beforeinstallprompt) is
569
+ caught by the page's inline bootstrap, so a button that mounts late still gets
570
+ it - do NOT add your own beforeinstallprompt listener in an effect, it can miss
571
+ the event. install() resolves 'accepted' | 'dismissed' | 'unavailable'. On iOS
572
+ there is no offer; when ios is true, tell the visitor Share -> Add to Home Screen.
573
+
574
+ **Splash screens.** Android draws its own from the manifest: name, the 512px
575
+ icon and backgroundColor - set all three. iOS needs one image per device and
576
+ orientation: name each apple-splash-WIDTHxHEIGHT.png at the device's pixel
577
+ size (landscape swaps them) and the build writes the media query from a device
578
+ table; a size no device has is named at build and linked nowhere. A Next port
579
+ can keep metadata.appleWebApp ({ capable, title, statusBarStyle, startupImage })
580
+ unchanged. read_guide({ slug: 'pwa' }) has the size table.
581
+
582
+ The build says whether it worked:
545
583
 
546
584
  [rsc-kit] manifest: Orders is installable
547
585
  [rsc-kit] manifest: no icons, so no browser will offer to install this.
@@ -598,7 +636,10 @@ still works: its flight payload is written with the wrappers.
598
636
 
599
637
  A page without the runtime also gets its stylesheet inlined when small
600
638
  (rscKit({ inlineStylesheets }) to change), and still registers the service
601
- worker with one inlined line.
639
+ worker with one inlined line. So does every other stored page, shells
640
+ included; one that hydrates keeps its link as media="print" so React finds
641
+ it rather than fetching it again. Do not "fix" that link - it is not render
642
+ blocking, and removing it makes React download the sheet after hydration.
602
643
 
603
644
  Do NOT restructure an app to chase this, and do NOT look for export const
604
645
  clientJs - it does not exist. The size column says what each route costs; a
@@ -1296,9 +1337,11 @@ auth, policies, jobs - answering one private endpoint, and it keeps every
1296
1337
  route of its own. The whole model, and how to build for it:
1297
1338
  read_guide({ slug: 'backend-answered-pages' }).
1298
1339
 
1299
- Go: in a Go module, rsc-kit init sees go.mod, writes the JS half and .env
1300
- (RSC_BACKEND + a generated secret) and prints the Go wiring; go get
1301
- github.com/rsc-kit/go. A new app: bun create rsc-kit --backend=<url>.
1340
+ Go: in a Go module, rsc-kit init sees go.mod, writes the JS half - the
1341
+ project's first package.json if it has none - and .env (RSC_BACKEND + a
1342
+ generated secret) and prints the Go wiring; go get github.com/rsc-kit/go.
1343
+ A new app: bun create rsc-kit --backend=<url>, or rsc-kit init in an empty
1344
+ directory, which starts one there.
1302
1345
 
1303
1346
  A backend in another language answers that ONE endpoint, POST /__rsc/host-call,
1304
1347
  and the renderer wires itself from two variables in .env: RSC_BACKEND (a
@@ -1558,8 +1601,8 @@ export const env = createEnv({
1558
1601
  },
1559
1602
  clientPrefix: 'PUBLIC_',
1560
1603
  client: { PUBLIC_SITE_URL: z.url() },
1561
- // No bare process.env: a "use client" file importing this for a PUBLIC_
1562
- // value has no process, and the spread throws before the first render.
1604
+ // No bare process.env: import.meta.env carries the PUBLIC_ values, and the
1605
+ // file does not throw in the browser if a client component imports it.
1563
1606
  runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },
1564
1607
  emptyStringAsUndefined: true,
1565
1608
  skipValidation: typeof process !== 'undefined' && !!process.env.SKIP_ENV_VALIDATION,
@@ -1573,6 +1616,12 @@ a browser-readable one MUST start with PUBLIC_ and is read from import.meta.env
1573
1616
  (Vite; the engine registers PUBLIC_ beside VITE_ as a client prefix, nothing
1574
1617
  to configure), which is why runtimeEnv merges both. Commit .env.example, not .env.
1575
1618
 
1619
+ CLIENT COMPONENTS DO NOT IMPORT src/env.ts. A "use client" file (or a lib file
1620
+ one imports) reads import.meta.env.PUBLIC_X directly, declared in
1621
+ ImportMetaEnv. Importing env there ships the validation library to the
1622
+ browser - measured: 20 kB gzipped of Zod + env-core in a chunk that needed one
1623
+ url. The value is still validated: the server imports env at startup.
1624
+
1576
1625
  Next: NEXT_PUBLIC_* becomes PUBLIC_*; @t3-oss/env-nextjs becomes
1577
1626
  @t3-oss/env-core with runtimeEnv as above (env-nextjs's experimental__runtimeEnv
1578
1627
  is not needed).`,