@rsc-kit/mcp 0.22.9 → 0.24.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
@@ -23,6 +23,10 @@ useState + value/onChange per input, and do NOT reach for TanStack Form.
23
23
  ONE WAY TO READ A FORM - two words, the same everywhere:
24
24
  error('title') the field's error, or undefined. !!error('title') is invalid.
25
25
  Nested fields are paths: error('address.city'), error('items[0].name').
26
+ formError the refusal NOT about a field (serverError - a 402/409 - or a
27
+ form-level validation message), or undefined. Render it as
28
+ {formError && <p role="alert">{formError}</p>}; do not keep it
29
+ from onError in your own state.
26
30
  field('title') a binding to spread - ONLY for a field whose value the UI must
27
31
  hold (a character count, a live preview, a control with no
28
32
  native element). Everything else stays uncontrolled.
@@ -163,6 +167,12 @@ It works before hydration. The action is on the form element as well as in the
163
167
  submit handler, so the markup is submittable on its own; a refusal shows on its
164
168
  fields through error() even with no javascript.
165
169
 
170
+ **components.json needs "rsc": true.** shadcn's init sees a plain Vite
171
+ project and writes false, and then a dialog or dropdown it adds has no
172
+ "use client". create-rsc-kit --shadcn and rsc-kit init --shadcn set it; after
173
+ a bare \`shadcn init\`, set it by hand. \`import { cn } from "cn"\` in its
174
+ components is correct: cn is shadcn's own package (lib/utils re-exports it).
175
+
166
176
  **shadcn/ui works as-is.** Input, Textarea, Button and Label are styled native
167
177
  elements, so \`name\` does what it always does. Select, Checkbox, Switch and
168
178
  RadioGroup are Radix underneath and render a hidden native control whenever
@@ -473,8 +483,41 @@ stored at all.
473
483
  </Suspense>
474
484
  \`\`\`
475
485
 
476
- Or a \`loading.tsx\` beside the page, which is the same thing for the whole
477
- route.
486
+ THE DEFAULT PATTERN - write pages this way: the page is a SYNCHRONOUS
487
+ component; fixed copy, titles and frames go straight in it (they are the
488
+ stored shell); each read is its own async "slot" component under its own
489
+ <Suspense>, with a skeleton the shape of what it replaces:
490
+
491
+ \`\`\`tsx
492
+ export default function ShopifyPage() { // not async
493
+ return (
494
+ <Card>
495
+ <CardTitle>Shopify</CardTitle> {/* in the shell */}
496
+ <Suspense><ConnectButtonSlot /></Suspense> {/* nothing until known */}
497
+ <Suspense fallback={<ConnectionsSkeleton rows={1} />}>
498
+ <ConnectionSlot /> {/* async: its own read + check */}
499
+ </Suspense>
500
+ </Card>
501
+ )
502
+ }
503
+ \`\`\`
504
+
505
+ Each slot does its own read and its own permission check, so a fast part is
506
+ not held back by a slow one, and a slot can move or be revalidated as a
507
+ section on its own.
508
+
509
+ loading.tsx wraps the WHOLE page in one boundary. Use it only for a page that
510
+ is one read and nothing else (a detail page whose skeleton is the page). Do
511
+ NOT reach for it by default:
512
+ - the heading/copy/frames the page already knows wait with the data;
513
+ - the slowest read holds back every other part;
514
+ - notFound()/redirect() or a backend 404 decided under it may already have a
515
+ 200 on the wire - a check deciding whether the page exists goes in
516
+ middleware.ts, which runs before anything is sent;
517
+ - it sits BELOW its layout: a layout that awaits (params, session) needs its
518
+ own <Suspense> inside the layout;
519
+ - a ROOT loading.tsx hides pages waiting in the wrong place (the build only
520
+ warns "nothing painted without the root loading.tsx").
478
521
 
479
522
  The build renders every page. Whatever has not resolved when the budget expires
480
523
  becomes the hole; everything above it is stored and served instantly. So a page
@@ -485,6 +528,9 @@ so:
485
528
  blocks before anything can paint. Add a loading.tsx beside it, or put a
486
529
  <Suspense> above the waiting, and it has a skeleton to store.
487
530
 
531
+ Fix that by moving the read into a slot under <Suspense>, not by adding a
532
+ loading.tsx.
533
+
488
534
  A painted fallback stays up at least 300 ms - React's rule: content that
489
535
  arrives within 300 ms of the fallback painting waits until then. A stored
490
536
  shell's holes arrive a moment after the shell, so fast data shows its
@@ -1323,7 +1369,13 @@ The visitor's url is untouched; only the match changes. Only a top-level
1323
1369
  [domain] or [host] directory binds the host; app/[slug] or app/[collection] at
1324
1370
  the top is a path parameter, as in Next. A top-level [domain]
1325
1371
  binds ONLY from a host, never from a path: example.com/nope is a 404, not a
1326
- tenant called "nope". Otherwise [domain] is an ordinary dynamic segment: params.domain in every page/layout under it, typed
1372
+ tenant called "nope". A layout gets params too (its own segments' and those above, as a promise),
1373
+ and is rendered again when its own value changes. A route listing no urls
1374
+ stores one shell for all values, so a layout awaiting params above every
1375
+ boundary fails the build: read them under <Suspense> in the layout, or list
1376
+ the urls with generateStaticParams. A tenant check that should 404 goes in
1377
+ [domain]/middleware.ts - its argument is the params, and notFound() there is
1378
+ a real 404. Otherwise [domain] is an ordinary dynamic segment: params.domain in every page/layout under it, typed
1327
1379
  route('/[domain]/settings', { domain }), loading/error files as usual. A
1328
1380
  directory named for a host (app/admin/) wins over [domain].
1329
1381
 
@@ -1390,6 +1442,28 @@ generated secret) and prints the Go wiring; go get github.com/rsc-kit/go.
1390
1442
  A new app: bun create rsc-kit --backend=<url>, or rsc-kit init in an empty
1391
1443
  directory, which starts one there.
1392
1444
 
1445
+ The backend writes rsc-host.json at the project root (beside vite.config.ts):
1446
+ { "actions": { jsName: "Function.name" }, "functions": ["Function.name", ...] }.
1447
+ actions become "use server" stubs in src/server-actions.generated.ts;
1448
+ functions type rpc()'s first argument, so a misspelt name fails tsc. rpc is a
1449
+ GLOBAL the build declares in .rsc-kit/rsc-env.d.ts - never import it. Have
1450
+ Vite write the manifest so it cannot go stale:
1451
+ rscKit({ hostManifest: { command: ['go', 'run', '.', '-manifest', '../rsc-host.json'], cwd: 'backend' } })
1452
+ (Go: reg.WriteManifest(path); Laravel: php artisan rsc:host-manifest).
1453
+ Go TYPED functions - prefer these: reg.Handle("Orders.recent", func(ctx context.Context, limit int) ([]Order, error) {...})
1454
+ and reg.HandleAction(jsName, name, fn). Any positional params (structs, slices,
1455
+ maps...), a trailing pointer is optional, variadic takes the rest. Their types
1456
+ go into rsc-host.json, so rpc('Orders.recent', 5) is Order[] with no <T>, and
1457
+ the action stub is typed and also accepts a FormData (fields -> first param).
1458
+ reg.Register(name, func(ctx, args rsckit.Args) (any, error)) stays untyped.
1459
+ Laravel: rsc:host-manifest types each app/Rsc method from its PHP signature -
1460
+ int/float/string/bool params (nullable, defaults optional, variadic), a
1461
+ FormRequest first param from its rules(), and a result that is a scalar, a
1462
+ backed enum, or a plain class with public typed properties (a data object ->
1463
+ an interface). array/Model/Collection/JsonSerializable stay unknown: give
1464
+ those rpc<T>(). Return a data object, not an array, to get a typed result. Tests
1465
+ answer the backend with createTestApp({ host }) - how_to testing.
1466
+
1393
1467
  A backend in another language answers that ONE endpoint, POST /__rsc/host-call,
1394
1468
  and the renderer wires itself from two variables in .env: RSC_BACKEND (a
1395
1469
  Laravel app's APP_URL counts) and RSC_HOST_CALL_SECRET. Both or neither.
@@ -1427,15 +1501,15 @@ Make one: php artisan make:rsc-action Orders --method=cancel --auth
1427
1501
  --can=update,Order --middleware=throttle:60,1 --revalidate=orders (--rpc for
1428
1502
  an rpc() class under app/Rsc; no --method = invokable; a slash nests,
1429
1503
  Billing/Invoices). Do NOT hand-write the attributes from memory; the command
1430
- writes the ones the registry reads, and it writes the map
1431
- (rsc-host-actions.json) too - a running dev server restarts on its own when
1432
- the map changes, so the export is importable when the command returns.
1504
+ writes the ones the registry reads, and it writes the manifest
1505
+ (rsc-host.json) too - a running dev server restarts on its own when it
1506
+ changes, so the export is importable when the command returns.
1433
1507
  Server actions are classes in app/Rsc/Actions, found by reflection in PHP;
1434
- \`php artisan rsc:action-manifest\` writes the map and MUST run before
1435
- Vite - the dev and build scripts rsc:install wrote do (\`php artisan
1436
- rsc:action-manifest && vite\`), so a class written by hand is picked up by
1437
- the next \`npm run dev\` or \`npm run build\`, never by Vite alone. The
1438
- build writes the "use server" stubs from the map - import ordersCancel from
1508
+ \`php artisan rsc:host-manifest\` writes rsc-host.json - { actions,
1509
+ functions } - and vite.config.ts runs it as dev and every build start:
1510
+ rscKit({ hostManifest: { command: ['php', 'artisan', 'rsc:host-manifest'] } }).
1511
+ functions types rpc()'s first argument, so a misspelt name fails tsc. The
1512
+ build writes the "use server" stubs from actions - import ordersCancel from
1439
1513
  the generated actions module in a client component. Rsc::revalidate('orders')
1440
1514
  in the action returns the re-rendered region with the answer.
1441
1515
 
@@ -1867,6 +1941,30 @@ It builds when the source is newer than the last build - the first run pays,
1867
1941
  the rest do not. This is where a guard that never ran or a 404 that came back
1868
1942
  200 shows up.
1869
1943
 
1944
+ **A Go or Laravel backend, answered in the test (nothing running):**
1945
+
1946
+ \`\`\`ts
1947
+ import { createTestApp, hostReply, HOST_MIDDLEWARE } from '@rsc-kit/core/testing'
1948
+ const app = await createTestApp({
1949
+ host: {
1950
+ 'Apps.list': ({ args, headers }) => [{ id: 1 }], // rpc('Apps.list', ...)
1951
+ 'Apps.create': () => hostReply.invalid({ name: ['Taken.'] }),
1952
+ [HOST_MIDDLEWARE]: ({ args, headers }) => // middleware.ts guards; args[0] = names
1953
+ headers.get('cookie')?.includes('session=') ? true : hostReply.unauthenticated(),
1954
+ },
1955
+ })
1956
+ \`\`\`
1957
+
1958
+ Answers go through the real host-call client, so they behave as the backend's
1959
+ would: hostReply.unauthenticated() -> 401, unauthorized() -> 403,
1960
+ redirect(to), refuse(status, message), invalid(errors) -> validationErrors,
1961
+ revalidating(result, ...regions). A name with no handler fails the call,
1962
+ naming it. RSC_BACKEND in .env is not called.
1963
+
1964
+ A url the app forwards to the backend (a Go route, /login) is a 502 in a test
1965
+ unless createTestApp({ backend: (request) => new Response(...) }) answers it;
1966
+ it gets the request as the backend would.
1967
+
1870
1968
  **What still needs a browser:** a server action called OVER THE WIRE (the id is
1871
1969
  React's and private), hydration, navigation. Playwright against vite preview.
1872
1970
  That limit is narrower than Next's: the action's logic is a unit test here.