@rsc-kit/core 0.9.0 → 0.10.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/action.d.ts +21 -0
- package/dist/action.js +67 -36
- package/dist/action.js.map +1 -1
- package/dist/apiPrerender.d.ts +25 -0
- package/dist/apiPrerender.js +195 -0
- package/dist/apiPrerender.js.map +1 -0
- package/dist/host.d.ts +12 -0
- package/dist/host.js +140 -2
- package/dist/host.js.map +1 -1
- package/dist/js/RouteErrorBoundary.d.ts +36 -0
- package/dist/js/RouteErrorBoundary.js +43 -0
- package/dist/js/RouteErrorBoundary.js.map +1 -0
- package/dist/js/queryClient.js +21 -1
- package/dist/js/queryClient.js.map +1 -1
- package/dist/manifest.d.ts +33 -0
- package/dist/manifest.js.map +1 -1
- package/dist/notFound.d.ts +24 -0
- package/dist/notFound.js +107 -0
- package/dist/notFound.js.map +1 -0
- package/dist/prerender.d.ts +48 -4
- package/dist/prerender.js +79 -9
- package/dist/prerender.js.map +1 -1
- package/dist/query.d.ts +23 -0
- package/dist/query.js +45 -3
- package/dist/query.js.map +1 -1
- package/dist/redirect.d.ts +8 -0
- package/dist/redirect.js +11 -1
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +7 -0
- package/dist/request.js +31 -6
- package/dist/request.js.map +1 -1
- package/dist/routeSchema.d.ts +115 -0
- package/dist/routeSchema.js +182 -0
- package/dist/routeSchema.js.map +1 -0
- package/dist/routing.d.ts +20 -1
- package/dist/routing.js +30 -7
- package/dist/routing.js.map +1 -1
- package/dist/vite.js +560 -27
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
package/dist/host.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
//
|
|
14
14
|
// const rsc = createRscHandler({ engine, manifest, assets })
|
|
15
15
|
// Bun.serve({ fetch: (req) => rsc(req).then((r) => r ?? new Response('', { status: 404 })) })
|
|
16
|
-
import { matchIntercept, matchRoute, retentionKey, sharedDepth } from './routing.js';
|
|
16
|
+
import { allowFor, matchApiRoute, matchIntercept, matchRoute, retentionKey, sharedDepth } from './routing.js';
|
|
17
17
|
import { pathKey, patternKey } from './prerender.js';
|
|
18
|
+
import { apiKey } from './apiPrerender.js';
|
|
18
19
|
import { withRevalidation } from './revalidate.js';
|
|
19
20
|
export { revalidate } from './revalidate.js';
|
|
20
|
-
import { withRedirect } from './redirect.js';
|
|
21
|
+
import { currentNotFound, withRedirect } from './redirect.js';
|
|
21
22
|
import { withCache } from './cache.js';
|
|
22
23
|
import { withRequest, withResponseDraft } from './request.js';
|
|
23
24
|
export { redirect } from './redirect.js';
|
|
@@ -315,6 +316,22 @@ export function createRscHandler(options) {
|
|
|
315
316
|
}
|
|
316
317
|
return await handleAction(request, url);
|
|
317
318
|
}
|
|
319
|
+
// Api routes first. A url is one or the other, and a page that shares a
|
|
320
|
+
// path with a route.ts would otherwise win by accident of ordering.
|
|
321
|
+
const api = matchApiRoute(routes, url.pathname);
|
|
322
|
+
if (api && engine.handleApiRoute) {
|
|
323
|
+
// The guards above it run first, exactly as they would for a page in the
|
|
324
|
+
// same directory. A route.ts is colocated with the pages it belongs
|
|
325
|
+
// with, so adding one under a guarded path must not open a way around
|
|
326
|
+
// the guard.
|
|
327
|
+
const refused = await refuseApiUnlessAllowed(request, api);
|
|
328
|
+
if (refused)
|
|
329
|
+
return refused;
|
|
330
|
+
const stored = await frozenApi(request, url, api);
|
|
331
|
+
if (stored)
|
|
332
|
+
return stored;
|
|
333
|
+
return await engine.handleApiRoute(api.route.name, request, api.params, allowFor(api.route));
|
|
334
|
+
}
|
|
318
335
|
if (request.method === 'GET' && url.pathname === HEADER.queryPath) {
|
|
319
336
|
// Same check as an action, for a smaller reason: a cross-origin page
|
|
320
337
|
// cannot read this answer — CORS sees to that — but it can still cause
|
|
@@ -407,6 +424,14 @@ export function createRscHandler(options) {
|
|
|
407
424
|
const refused = taken();
|
|
408
425
|
if (refused)
|
|
409
426
|
return redirectResponse(refused, false);
|
|
427
|
+
// The page said this url names nothing. Null rather than a rendered
|
|
428
|
+
// 404: null is already how this host says "not mine", and the caller
|
|
429
|
+
// in front answers it with not-found.tsx and the right status. One
|
|
430
|
+
// path, so a page that calls notFound() and a url that matched no
|
|
431
|
+
// route are indistinguishable to whoever is asking — which is the
|
|
432
|
+
// point of a 404.
|
|
433
|
+
if (currentNotFound())
|
|
434
|
+
return null;
|
|
410
435
|
// A guard refusing is not a failed render. Without this a visitor
|
|
411
436
|
// who may not see the page gets a 500, which reads as the
|
|
412
437
|
// application being broken rather than them being turned away —
|
|
@@ -420,6 +445,12 @@ export function createRscHandler(options) {
|
|
|
420
445
|
const early = taken();
|
|
421
446
|
if (early)
|
|
422
447
|
return redirectResponse(early, false);
|
|
448
|
+
// Above every boundary, so the shell resolving means the page did not
|
|
449
|
+
// refuse itself. Deeper than that and the shell is already on the wire
|
|
450
|
+
// — the digest carries it to the boundary instead, and the status
|
|
451
|
+
// stays 200 because the status line has gone.
|
|
452
|
+
if (currentNotFound())
|
|
453
|
+
return null;
|
|
423
454
|
return new Response(appendLateRedirect(htmlStream, taken), {
|
|
424
455
|
headers: withVersion({
|
|
425
456
|
'Content-Type': HTML_TYPE,
|
|
@@ -447,6 +478,10 @@ export function createRscHandler(options) {
|
|
|
447
478
|
const refused = taken();
|
|
448
479
|
if (refused)
|
|
449
480
|
return redirectResponse(refused, true);
|
|
481
|
+
// Same answer the document path gives, so a client navigating to a
|
|
482
|
+
// url and a browser loading it fresh agree about whether it exists.
|
|
483
|
+
if (currentNotFound())
|
|
484
|
+
return null;
|
|
450
485
|
// A payload request is guarded exactly as the document is. Narrowing
|
|
451
486
|
// a request must never narrow what is checked.
|
|
452
487
|
const status = refusalStatus(error);
|
|
@@ -493,6 +528,50 @@ export function createRscHandler(options) {
|
|
|
493
528
|
* not cacheable by a shared cache at all, so handing one to an edge that
|
|
494
529
|
* exists to cache things is an invitation to a mistake nobody would see.
|
|
495
530
|
*/
|
|
531
|
+
/**
|
|
532
|
+
* The answer the build stored for this route, if it stored one.
|
|
533
|
+
*
|
|
534
|
+
* Three conditions, each closing a way the stored answer could be wrong:
|
|
535
|
+
*
|
|
536
|
+
* GET or HEAD, because a stored answer to a POST is a stored answer to
|
|
537
|
+
* something that was meant to happen once.
|
|
538
|
+
*
|
|
539
|
+
* No query string. The build answered the bare url, and a route that reads
|
|
540
|
+
* the query would answer differently for every one — so rather than trying
|
|
541
|
+
* to detect that during the probe, anything carrying a query goes to the
|
|
542
|
+
* route itself. A stored answer is for the url it was stored for.
|
|
543
|
+
*
|
|
544
|
+
* No middleware. A guarded route answers differently depending on who is
|
|
545
|
+
* asking, which is the point of the guard; one stored answer served to
|
|
546
|
+
* everyone is how a guard is quietly removed. The build refuses to store one
|
|
547
|
+
* for the same reason, so this is the second of two locks on the same door.
|
|
548
|
+
*/
|
|
549
|
+
async function frozenApi(request, url, api) {
|
|
550
|
+
if (!options.prerendered)
|
|
551
|
+
return null;
|
|
552
|
+
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
553
|
+
return null;
|
|
554
|
+
if (url.search)
|
|
555
|
+
return null;
|
|
556
|
+
if (api.route.middleware.length > 0)
|
|
557
|
+
return null;
|
|
558
|
+
const stored = await options.prerendered(apiKey(url.pathname));
|
|
559
|
+
if (stored === null)
|
|
560
|
+
return null;
|
|
561
|
+
let frozen;
|
|
562
|
+
try {
|
|
563
|
+
frozen = JSON.parse(stored);
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// A file this host wrote and cannot read back is a bug, not a request
|
|
567
|
+
// to answer badly. Falling through runs the route, which is correct.
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
return new Response(request.method === 'HEAD' ? null : frozen.body, {
|
|
571
|
+
status: frozen.status,
|
|
572
|
+
headers: withVersion(Object.fromEntries(frozen.headers)),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
496
575
|
async function servePprShell(request, url, read) {
|
|
497
576
|
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
498
577
|
return null;
|
|
@@ -868,6 +947,56 @@ export function createRscHandler(options) {
|
|
|
868
947
|
}),
|
|
869
948
|
});
|
|
870
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Run an api route's middleware, and answer instead of it if one refuses.
|
|
952
|
+
*
|
|
953
|
+
* Separate from the page version because the answer is different. A caller
|
|
954
|
+
* that is not a browser gets a status rather than a redirect to a login page
|
|
955
|
+
* it cannot render — a fetch would follow the 302 and hand back the login
|
|
956
|
+
* HTML as though it were the api's answer.
|
|
957
|
+
*/
|
|
958
|
+
async function refuseApiUnlessAllowed(request, api) {
|
|
959
|
+
if (!(api.route.middleware?.length ?? 0))
|
|
960
|
+
return null;
|
|
961
|
+
// A route that declares middleware and an engine that cannot run it is not
|
|
962
|
+
// "no middleware" — it is a check that silently does not happen.
|
|
963
|
+
if (!engine.runRouteMiddleware) {
|
|
964
|
+
return new Response('This route declares middleware, and the engine cannot run it. ' +
|
|
965
|
+
'Rebuild the app against the current @rsc-kit/core.', { status: 500 });
|
|
966
|
+
}
|
|
967
|
+
return await withRedirect(async (taken) => {
|
|
968
|
+
try {
|
|
969
|
+
await engine.runRouteMiddleware(api.route.name, api.params);
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
// A redirect is a refusal here. Where it was going is told rather than
|
|
973
|
+
// followed, so a client can decide for itself.
|
|
974
|
+
const redirected = taken();
|
|
975
|
+
if (redirected) {
|
|
976
|
+
return new Response('Unauthorized', {
|
|
977
|
+
status: 401,
|
|
978
|
+
headers: { 'X-RSC-Redirect': redirected.location },
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
// A visitor who may not use this endpoint has not caused a server
|
|
982
|
+
// error, and answering 500 makes a guarded route indistinguishable
|
|
983
|
+
// from a broken one. Null means the middleware threw something that is
|
|
984
|
+
// not a refusal, which is a real fault and says so.
|
|
985
|
+
const status = refusalStatus(error);
|
|
986
|
+
if (status === null)
|
|
987
|
+
throw error;
|
|
988
|
+
return new Response(status === 401 ? 'Unauthorized' : 'Forbidden', { status });
|
|
989
|
+
}
|
|
990
|
+
const redirected = taken();
|
|
991
|
+
if (redirected) {
|
|
992
|
+
return new Response('Unauthorized', {
|
|
993
|
+
status: 401,
|
|
994
|
+
headers: { 'X-RSC-Redirect': redirected.location },
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
});
|
|
999
|
+
}
|
|
871
1000
|
async function handleQuery(request, url) {
|
|
872
1001
|
if (!engine.handleQuery)
|
|
873
1002
|
return null;
|
|
@@ -894,6 +1023,15 @@ export function createRscHandler(options) {
|
|
|
894
1023
|
// Unknown id and registered-but-not-a-query are the same answer on purpose.
|
|
895
1024
|
if (!answered)
|
|
896
1025
|
return new Response('No such query', { status: 404 });
|
|
1026
|
+
// The read refused. Answered as a status with the message in the body, so
|
|
1027
|
+
// the fetcher rejects with something a person can read — a failure rendered
|
|
1028
|
+
// into a 200 would reach the browser as React's opaque error instead.
|
|
1029
|
+
if (!('stream' in answered)) {
|
|
1030
|
+
return new Response(JSON.stringify({ message: answered.message, errors: answered.errors }), {
|
|
1031
|
+
status: answered.status,
|
|
1032
|
+
headers: withVersion({ 'Content-Type': 'application/json', 'Cache-Control': PER_CLIENT }),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
897
1035
|
return new Response(answered.stream, {
|
|
898
1036
|
headers: withVersion({
|
|
899
1037
|
'Content-Type': FLIGHT_TYPE,
|