@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 +109 -11
- package/dist/recipes.js.map +1 -1
- package/guides/backend-answered-pages.md +68 -1
- package/guides/domains.md +38 -5
- package/guides/forms.md +5 -0
- package/guides/go.md +54 -15
- package/guides/installation.md +32 -1
- package/guides/laravel.md +46 -15
- package/guides/ppr.md +81 -2
- package/guides/sections.md +10 -0
- package/guides/url-validation.md +5 -3
- package/guides/your-own-backend.md +24 -9
- package/package.json +1 -1
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
|
-
|
|
477
|
-
|
|
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".
|
|
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
|
|
1431
|
-
(rsc-host
|
|
1432
|
-
|
|
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:
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
build writes the "use server" stubs 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.
|