@rangojs/router 0.22.1 → 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/bin/rango.js +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/src/rsc/handler.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
createRequestContext,
|
|
18
18
|
captureRequestEntryContext,
|
|
19
19
|
} from "../server/request-context.js";
|
|
20
|
+
import { REWRITE_HEADER, runRewritePasses } from "./rewrite-passes.js";
|
|
21
|
+
import { renderedLocation } from "./full-payload.js";
|
|
20
22
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
|
|
21
23
|
import type {
|
|
22
24
|
RscPayload,
|
|
@@ -25,6 +27,7 @@ import type {
|
|
|
25
27
|
SSRModule,
|
|
26
28
|
} from "./types.js";
|
|
27
29
|
import {
|
|
30
|
+
createReloadResponse,
|
|
28
31
|
createResponseWithMergedHeaders,
|
|
29
32
|
finalizeResponse,
|
|
30
33
|
interceptRedirectForPartial,
|
|
@@ -41,6 +44,7 @@ import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
|
41
44
|
import {
|
|
42
45
|
isWebSocketUpgradeResponse,
|
|
43
46
|
appendVaryAccept,
|
|
47
|
+
trySetHeader,
|
|
44
48
|
} from "../response-utils.js";
|
|
45
49
|
import {
|
|
46
50
|
handleResponseRoute,
|
|
@@ -57,10 +61,7 @@ import {
|
|
|
57
61
|
invokeOnError,
|
|
58
62
|
resolveDefaultNotFound,
|
|
59
63
|
} from "../router/error-handling.js";
|
|
60
|
-
import {
|
|
61
|
-
createReverseFunction,
|
|
62
|
-
stripInternalParams,
|
|
63
|
-
} from "../router/handler-context.js";
|
|
64
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
64
65
|
import { contextSet } from "../context-var.js";
|
|
65
66
|
import {
|
|
66
67
|
hasCachedManifest,
|
|
@@ -82,7 +83,7 @@ import {
|
|
|
82
83
|
revalidateAfterAction,
|
|
83
84
|
type ActionContinuation,
|
|
84
85
|
} from "./server-action.js";
|
|
85
|
-
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
86
|
+
import { handleLoaderFetch, renderLoaderError } from "./loader-fetch.js";
|
|
86
87
|
import { applyStreamIdleTimeout } from "./stream-idle.js";
|
|
87
88
|
import {
|
|
88
89
|
checkRequestOrigin,
|
|
@@ -113,6 +114,7 @@ import {
|
|
|
113
114
|
mayNeedSSR,
|
|
114
115
|
isRscRequest,
|
|
115
116
|
SSR_SETUP_VAR,
|
|
117
|
+
type SSRSetup,
|
|
116
118
|
} from "./ssr-setup.js";
|
|
117
119
|
import {
|
|
118
120
|
classifyRequest,
|
|
@@ -155,21 +157,6 @@ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
|
155
157
|
* ```
|
|
156
158
|
*/
|
|
157
159
|
|
|
158
|
-
/**
|
|
159
|
-
* Response that tells the client to do a full document navigation. Shared by
|
|
160
|
-
* the terminal reload plans (version-mismatch and app-switch): an empty 200
|
|
161
|
-
* carrying X-RSC-Reload, which the client turns into window.location.href.
|
|
162
|
-
*/
|
|
163
|
-
function createReloadResponse(reloadUrl: string) {
|
|
164
|
-
return createResponseWithMergedHeaders(null, {
|
|
165
|
-
status: 200,
|
|
166
|
-
headers: {
|
|
167
|
-
"X-RSC-Reload": reloadUrl,
|
|
168
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
160
|
export function createRSCHandler<
|
|
174
161
|
TEnv = unknown,
|
|
175
162
|
TRoutes extends Record<string, string> = Record<string, string>,
|
|
@@ -703,24 +690,70 @@ export function createRSCHandler<
|
|
|
703
690
|
// latter is reserved for the low-cardinality template (OTel convention).
|
|
704
691
|
span.setAttribute("url.path", url.pathname);
|
|
705
692
|
|
|
706
|
-
//
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const downstream =
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
693
|
+
// One pass per URL: the global middleware chain matched on the pass's
|
|
694
|
+
// path, wrapping the core handler. A `throw rewrite()` from global or
|
|
695
|
+
// route middleware unwinds the pass, and the next one runs the same
|
|
696
|
+
// request as the target from the top, as a direct visit would
|
|
697
|
+
// (rsc/rewrite-passes.ts). The response is finalized below, inside the
|
|
698
|
+
// response span.
|
|
699
|
+
let passIndex = 0;
|
|
700
|
+
let hasMiddleware = false;
|
|
701
|
+
const { response: downstream, rewrittenTo } = await runRewritePasses({
|
|
702
|
+
reqCtx: requestContext,
|
|
703
|
+
request,
|
|
704
|
+
url,
|
|
705
|
+
routeMap: getRequiredRouteMap,
|
|
706
|
+
basename: router.basename,
|
|
707
|
+
runPass: (passRequest, passUrl) => {
|
|
708
|
+
const passMiddleware =
|
|
709
|
+
passIndex++ === 0
|
|
710
|
+
? matchedMiddleware
|
|
711
|
+
: matchMiddleware(
|
|
712
|
+
requestContext.url.pathname,
|
|
713
|
+
router.middleware,
|
|
714
|
+
);
|
|
715
|
+
hasMiddleware = passMiddleware.length > 0;
|
|
716
|
+
const coreHandler = (): Promise<Response> =>
|
|
717
|
+
coreRequestHandler(passRequest, env, passUrl, variables, nonce);
|
|
718
|
+
return hasMiddleware
|
|
719
|
+
? executeMiddleware(
|
|
720
|
+
passMiddleware,
|
|
721
|
+
passRequest,
|
|
722
|
+
env,
|
|
723
|
+
variables,
|
|
724
|
+
coreHandler,
|
|
725
|
+
createReverseFunction(getRequiredRouteMap()),
|
|
726
|
+
)
|
|
727
|
+
: coreHandler();
|
|
728
|
+
},
|
|
729
|
+
onHop: (pass) => {
|
|
730
|
+
span.setAttribute("rango.rewrite.target", pass.target.pathname);
|
|
731
|
+
span.setAttribute("rango.rewrite.hops", pass.hops);
|
|
732
|
+
},
|
|
733
|
+
report: (error, at) => {
|
|
734
|
+
callOnError(error, "routing", {
|
|
735
|
+
request: at.request,
|
|
736
|
+
url: at.url,
|
|
718
737
|
env,
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
:
|
|
738
|
+
handledByBoundary: false,
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
// A loader fetch answers with the error `load()` rejects with.
|
|
742
|
+
refuse: (error) =>
|
|
743
|
+
url.searchParams.has("_rsc_loader")
|
|
744
|
+
? renderLoaderError(
|
|
745
|
+
handlerCtx,
|
|
746
|
+
{ message: (error as Error).message, name: "Error" },
|
|
747
|
+
500,
|
|
748
|
+
{
|
|
749
|
+
request,
|
|
750
|
+
url,
|
|
751
|
+
env,
|
|
752
|
+
loaderId: url.searchParams.get("_rsc_loader") ?? "",
|
|
753
|
+
},
|
|
754
|
+
)
|
|
755
|
+
: undefined,
|
|
756
|
+
});
|
|
724
757
|
|
|
725
758
|
// Final response construction + host handoff, wrapped in rango.response
|
|
726
759
|
// — the explicit stream-handoff marker. The callback is synchronous, so
|
|
@@ -787,14 +820,9 @@ export function createRSCHandler<
|
|
|
787
820
|
}
|
|
788
821
|
|
|
789
822
|
const fullTiming = timingParts.join(", ");
|
|
790
|
-
if (fullTiming
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
} catch {
|
|
794
|
-
// Immutable headers (e.g. a passed-through platform Response) — drop
|
|
795
|
-
// the timing header, never the response. Instrumentation must not
|
|
796
|
-
// 500 a request.
|
|
797
|
-
}
|
|
823
|
+
if (fullTiming) trySetHeader(response, "Server-Timing", fullTiming);
|
|
824
|
+
if (rewrittenTo !== undefined) {
|
|
825
|
+
trySetHeader(response, REWRITE_HEADER, rewrittenTo);
|
|
798
826
|
}
|
|
799
827
|
|
|
800
828
|
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
@@ -956,18 +984,19 @@ export function createRSCHandler<
|
|
|
956
984
|
// which returns { redirect: url }. Fall through to full-render.
|
|
957
985
|
}
|
|
958
986
|
|
|
987
|
+
// A reload points at the visitor's URL, never a rewrite target's.
|
|
959
988
|
if (plan.mode === "version-mismatch") {
|
|
960
989
|
console.log(
|
|
961
990
|
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
962
991
|
);
|
|
963
|
-
return createReloadResponse(plan.reloadUrl);
|
|
992
|
+
return createReloadResponse(getRequestContext(), plan.reloadUrl);
|
|
964
993
|
}
|
|
965
994
|
|
|
966
995
|
if (plan.mode === "app-switch") {
|
|
967
996
|
// Cross-app SPA navigation crossed a host-router app boundary. Force a
|
|
968
997
|
// real document navigation so the target app's document is re-established
|
|
969
998
|
// (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
|
|
970
|
-
return createReloadResponse(plan.reloadUrl);
|
|
999
|
+
return createReloadResponse(getRequestContext(), plan.reloadUrl);
|
|
971
1000
|
}
|
|
972
1001
|
|
|
973
1002
|
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
@@ -1137,6 +1166,9 @@ export function createRSCHandler<
|
|
|
1137
1166
|
(mayNeedSSR(request, url) &&
|
|
1138
1167
|
!isRscRequest(request, url, plan.mode === "partial-render"));
|
|
1139
1168
|
if (plan.mode !== "loader" && willRenderHtml) {
|
|
1169
|
+
// A rewritten pass keeps the SSR module the earlier pass started
|
|
1170
|
+
// loading; the stream mode is resolved for the request it renders.
|
|
1171
|
+
const earlier = variables[SSR_SETUP_VAR] as Promise<SSRSetup> | undefined;
|
|
1140
1172
|
const ssrSetup = startSSRSetup(
|
|
1141
1173
|
handlerCtx,
|
|
1142
1174
|
request,
|
|
@@ -1145,6 +1177,7 @@ export function createRSCHandler<
|
|
|
1145
1177
|
router.debugPerformance
|
|
1146
1178
|
? () => getRequestContext()._metricsStore
|
|
1147
1179
|
: undefined,
|
|
1180
|
+
earlier?.then(([ssrModule]) => ssrModule),
|
|
1148
1181
|
);
|
|
1149
1182
|
variables[SSR_SETUP_VAR] = ssrSetup;
|
|
1150
1183
|
|
|
@@ -1348,13 +1381,7 @@ export function createRSCHandler<
|
|
|
1348
1381
|
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1349
1382
|
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1350
1383
|
);
|
|
1351
|
-
return
|
|
1352
|
-
status: 200,
|
|
1353
|
-
headers: {
|
|
1354
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
1355
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
1356
|
-
},
|
|
1357
|
-
});
|
|
1384
|
+
return createReloadResponse(getRequestContext(), url);
|
|
1358
1385
|
}
|
|
1359
1386
|
|
|
1360
1387
|
if (treatAsPartial) {
|
|
@@ -1396,7 +1423,7 @@ export function createRSCHandler<
|
|
|
1396
1423
|
|
|
1397
1424
|
const payload: RscPayload = {
|
|
1398
1425
|
metadata: {
|
|
1399
|
-
pathname: url.pathname,
|
|
1426
|
+
pathname: renderedLocation(getRequestContext(), url).pathname,
|
|
1400
1427
|
routerId: router.id,
|
|
1401
1428
|
basename: router.basename,
|
|
1402
1429
|
segments: [notFoundSegment],
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
25
25
|
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
26
26
|
import type { RscPayload } from "./types.js";
|
|
27
|
+
import { rewriteTrail, visitorUrl } from "../server/rewrite-trail.js";
|
|
27
28
|
import type { HandlerContext } from "./handler-context.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -157,6 +158,29 @@ export function createResponseWithMergedHeaders(
|
|
|
157
158
|
return drainOnResponseCallbacks(ctx, response);
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* An empty 200 carrying X-RSC-Reload: the client turns it into a document
|
|
163
|
+
* navigation (window.location.href). It points at the visitor's URL: the
|
|
164
|
+
* first URL of a rewritten request (server/rewrite-trail.ts visitorUrl),
|
|
165
|
+
* else `url` with the internal `_rsc*` params stripped, or `url` as given
|
|
166
|
+
* when it is a string the caller resolved (an action's referer).
|
|
167
|
+
*/
|
|
168
|
+
export function createReloadResponse(
|
|
169
|
+
reqCtx: RequestContext<any> | undefined,
|
|
170
|
+
url: URL | string,
|
|
171
|
+
): Response {
|
|
172
|
+
const target =
|
|
173
|
+
rewriteTrail(reqCtx)?.[0] ??
|
|
174
|
+
(typeof url === "string" ? url : visitorUrl(undefined, url).href);
|
|
175
|
+
return createResponseWithMergedHeaders(null, {
|
|
176
|
+
status: 200,
|
|
177
|
+
headers: {
|
|
178
|
+
"X-RSC-Reload": target,
|
|
179
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
160
184
|
/**
|
|
161
185
|
* Rebuild a Response returned or thrown by a response-route handler so request
|
|
162
186
|
* effects merge consistently in production and the public dispatch primitive.
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { requestHeaders } from "../server/request-headers.js";
|
|
15
|
+
import { FORBIDDEN_KEYS } from "../context-var.js";
|
|
15
16
|
import { getLoaderLazy } from "../server/loader-registry.js";
|
|
16
17
|
import { isDataNotFoundError } from "../errors.js";
|
|
17
18
|
import { executeLoaderMiddleware } from "../router/middleware.js";
|
|
@@ -33,18 +34,57 @@ import {
|
|
|
33
34
|
} from "./helpers.js";
|
|
34
35
|
import type { HandlerContext } from "./handler-context.js";
|
|
35
36
|
|
|
37
|
+
export interface LoaderErrorMeta<TEnv> {
|
|
38
|
+
request: Request;
|
|
39
|
+
url: URL;
|
|
40
|
+
env: TEnv;
|
|
41
|
+
loaderId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A text/x-component Response carrying `{ loaderResult: null, loaderError }`,
|
|
46
|
+
* the payload `load()` turns into a rejection with `loaderError.message`. A
|
|
47
|
+
* plain-text body would reject with React's opaque "Connection closed.".
|
|
48
|
+
* Render failures report through onError("rendering").
|
|
49
|
+
*/
|
|
50
|
+
export function renderLoaderError<TEnv>(
|
|
51
|
+
ctx: HandlerContext<TEnv>,
|
|
52
|
+
loaderError: { message: string; name: string },
|
|
53
|
+
status: number,
|
|
54
|
+
meta: LoaderErrorMeta<TEnv>,
|
|
55
|
+
): Response {
|
|
56
|
+
const { request, url, env, loaderId } = meta;
|
|
57
|
+
const rscStream = ctx.renderToReadableStream(
|
|
58
|
+
{ loaderResult: null, loaderError },
|
|
59
|
+
{
|
|
60
|
+
onError: (renderError: unknown) => {
|
|
61
|
+
ctx.callOnError(renderError, "rendering", {
|
|
62
|
+
request,
|
|
63
|
+
url,
|
|
64
|
+
env,
|
|
65
|
+
loaderName: loaderId,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
72
|
+
status,
|
|
73
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
/**
|
|
37
78
|
* Build the 500 RSC error Response for a failed fetchable loader. Shared by the
|
|
38
79
|
* module-load-error catch (the import itself threw) and the loader-execution
|
|
39
|
-
* error catch — both call onError("loader")
|
|
40
|
-
* payload
|
|
41
|
-
*
|
|
42
|
-
* per-site difference is the console.error label, passed in.
|
|
80
|
+
* error catch — both call onError("loader") and serialize the same dev-gated
|
|
81
|
+
* error payload. The only per-site difference is the console.error label,
|
|
82
|
+
* passed in.
|
|
43
83
|
*/
|
|
44
84
|
function buildLoaderErrorResponse<TEnv>(
|
|
45
85
|
ctx: HandlerContext<TEnv>,
|
|
46
86
|
error: unknown,
|
|
47
|
-
meta:
|
|
87
|
+
meta: LoaderErrorMeta<TEnv>,
|
|
48
88
|
logLabel: string,
|
|
49
89
|
): Response {
|
|
50
90
|
const { request, url, env, loaderId } = meta;
|
|
@@ -61,9 +101,9 @@ function buildLoaderErrorResponse<TEnv>(
|
|
|
61
101
|
});
|
|
62
102
|
|
|
63
103
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
return renderLoaderError(
|
|
105
|
+
ctx,
|
|
106
|
+
{
|
|
67
107
|
message: isDev ? err.message : "An error occurred",
|
|
68
108
|
// Gate err.name to dev. In production it leaks the consumer's error class
|
|
69
109
|
// name (e.g. AuthError, PrismaClientKnownRequestError) to the client; the
|
|
@@ -71,22 +111,9 @@ function buildLoaderErrorResponse<TEnv>(
|
|
|
71
111
|
// Matches sanitizeError's dev-only name contract.
|
|
72
112
|
name: isDev ? err.name : "Error",
|
|
73
113
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ctx.callOnError(renderError, "rendering", {
|
|
78
|
-
request,
|
|
79
|
-
url,
|
|
80
|
-
env,
|
|
81
|
-
loaderName: loaderId,
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
87
|
-
status: 500,
|
|
88
|
-
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
89
|
-
});
|
|
114
|
+
500,
|
|
115
|
+
meta,
|
|
116
|
+
);
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
export async function handleLoaderFetch<TEnv>(
|
|
@@ -183,6 +210,21 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
183
210
|
}
|
|
184
211
|
}
|
|
185
212
|
|
|
213
|
+
// The params become ctx.params, so they hold what a route param can: string
|
|
214
|
+
// keys and string values, never a reserved key (#1109). The message is the
|
|
215
|
+
// router's own, so production sends it too.
|
|
216
|
+
if (!isLoaderParams(loaderParams)) {
|
|
217
|
+
return renderLoaderError(
|
|
218
|
+
ctx,
|
|
219
|
+
{
|
|
220
|
+
message: `Invalid loader params: expected an object of string values without the reserved keys ${[...FORBIDDEN_KEYS].join(", ")}`,
|
|
221
|
+
name: "Error",
|
|
222
|
+
},
|
|
223
|
+
400,
|
|
224
|
+
{ request, url, env, loaderId },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
186
228
|
// Execute the loader with middleware.
|
|
187
229
|
// finalizeResponse drains onResponse callbacks that middleware short-circuits
|
|
188
230
|
// may leave behind (executeLoaderMiddleware does not finalize them itself).
|
|
@@ -304,3 +346,16 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
304
346
|
);
|
|
305
347
|
}
|
|
306
348
|
}
|
|
349
|
+
|
|
350
|
+
function isLoaderParams(value: unknown): value is Record<string, string> {
|
|
351
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
for (const key of Object.keys(value)) {
|
|
355
|
+
if (FORBIDDEN_KEYS.has(key)) return false;
|
|
356
|
+
if (typeof (value as Record<string, unknown>)[key] !== "string") {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* type-only import (erased at build).
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { isRewrite } from "../route-definition/rewrite.js";
|
|
22
23
|
import type { CacheScope } from "../cache/cache-scope.js";
|
|
23
24
|
import {
|
|
24
25
|
_getRequestContext,
|
|
@@ -159,10 +160,13 @@ export async function serveResponseRouteWithCache(
|
|
|
159
160
|
// Finding #3 as document-cache.ts shouldCacheResponse.
|
|
160
161
|
// Only store GET: HEAD may branch on method; sharing a write slot with GET
|
|
161
162
|
// can poison subsequent GET bodies. HEAD still reads the GET key below.
|
|
163
|
+
// Nor a body a rewritten request built from the visitor's URL
|
|
164
|
+
// (ctx.rewrittenFrom, server/rewrite-trail.ts): the key is the target's.
|
|
162
165
|
const canStore = (response: Response): boolean =>
|
|
163
166
|
method === "GET" &&
|
|
164
167
|
isCacheableStatus(response.status) &&
|
|
165
|
-
!hasPerClientSignal(response.headers)
|
|
168
|
+
!hasPerClientSignal(response.headers) &&
|
|
169
|
+
!reqCtx._payloadVisitorLocation;
|
|
166
170
|
|
|
167
171
|
// Reject unsafe persisted entries on read (pre-upgrade poison / store bugs).
|
|
168
172
|
const canServeCached = (response: Response): boolean =>
|
|
@@ -206,6 +210,8 @@ export async function serveResponseRouteWithCache(
|
|
|
206
210
|
const fresh = finalizeResponse(await executeHandler());
|
|
207
211
|
if (canStore(fresh)) await putFresh(store, fresh, start);
|
|
208
212
|
} catch (error) {
|
|
213
|
+
// The route now rewrites: nothing to refresh this entry with.
|
|
214
|
+
if (isRewrite(error)) return;
|
|
209
215
|
reportCacheError(
|
|
210
216
|
error,
|
|
211
217
|
"stale-revalidation",
|
|
@@ -227,7 +233,17 @@ export async function serveResponseRouteWithCache(
|
|
|
227
233
|
|
|
228
234
|
// Cache miss: execute the handler and cache the result when storeable.
|
|
229
235
|
const start = executionStart();
|
|
230
|
-
|
|
236
|
+
let response: Response;
|
|
237
|
+
try {
|
|
238
|
+
response = finalizeResponse(await executeHandler());
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Route middleware rewrote: the request runs again as the target, whose
|
|
241
|
+
// response runs these pre-handler callbacks.
|
|
242
|
+
if (isRewrite(error)) {
|
|
243
|
+
reqCtx._onResponseCallbacks.unshift(...savedCallbacks);
|
|
244
|
+
}
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
231
247
|
if (canStore(response)) {
|
|
232
248
|
// Clone SYNCHRONOUSLY here, before returning. The original `response` is
|
|
233
249
|
// handed back to the middleware chain, where mergeResponse rebuilds it as
|
|
@@ -10,6 +10,7 @@ import { RouterError } from "../errors.js";
|
|
|
10
10
|
import { getRequestContext } from "../server/request-context.js";
|
|
11
11
|
import { contextGet, isNonCacheable } from "../context-var.js";
|
|
12
12
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
13
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
13
14
|
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
14
15
|
import { createCacheScope } from "../cache/cache-scope.js";
|
|
15
16
|
import { serveResponseRouteWithCache } from "./response-cache-serve.js";
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
import type { HandlerContext } from "./handler-context.js";
|
|
27
28
|
import { createProblemDetails } from "./response-error.js";
|
|
28
29
|
import {
|
|
30
|
+
createReloadResponse,
|
|
29
31
|
createResponseWithMergedHeaders,
|
|
30
32
|
finalizeResponse,
|
|
31
33
|
isCacheableStatus,
|
|
@@ -67,13 +69,7 @@ export async function handleResponseRoute<TEnv>(
|
|
|
67
69
|
// Partial requests (client-side navigation) to response routes
|
|
68
70
|
// get X-RSC-Reload to trigger hard navigation in the browser
|
|
69
71
|
if (isPartial) {
|
|
70
|
-
return
|
|
71
|
-
status: 200,
|
|
72
|
-
headers: {
|
|
73
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
74
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
75
|
-
},
|
|
76
|
-
});
|
|
72
|
+
return createReloadResponse(getRequestContext(), url);
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
// Build lightweight context for response handler
|
|
@@ -101,6 +97,7 @@ export async function handleResponseRoute<TEnv>(
|
|
|
101
97
|
};
|
|
102
98
|
// Brand with taint symbol so "use cache" detects it as request-scoped
|
|
103
99
|
// and extracts route-identifying properties (params, pathname, _responseType)
|
|
100
|
+
defineRewrittenFrom(responseHandlerCtx, reqCtx);
|
|
104
101
|
(responseHandlerCtx as any)[NOCACHE_SYMBOL] = true;
|
|
105
102
|
|
|
106
103
|
// Call handler directly, wrapped by route middleware if present
|