@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/src/rsc/handler.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createElement } from "react";
|
|
11
|
-
import {
|
|
11
|
+
import { isRouteNotFoundError } from "../errors.js";
|
|
12
12
|
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
13
13
|
import {
|
|
14
14
|
runWithRequestContext,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
interceptRedirectForPartial,
|
|
32
32
|
buildRouteMiddlewareEntries,
|
|
33
33
|
} from "./helpers.js";
|
|
34
|
+
import { guardOutgoingRedirect } from "./redirect-guard.js";
|
|
34
35
|
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
35
36
|
import {
|
|
36
37
|
handleResponseRoute,
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
getRouterTrie,
|
|
58
59
|
} from "../route-map-builder.js";
|
|
59
60
|
import type { HandlerContext } from "./handler-context.js";
|
|
61
|
+
import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
60
62
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
61
63
|
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
62
64
|
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
@@ -66,7 +68,10 @@ import {
|
|
|
66
68
|
type ActionContinuation,
|
|
67
69
|
} from "./server-action.js";
|
|
68
70
|
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
69
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
checkRequestOrigin,
|
|
73
|
+
ORIGIN_CHECK_PHASE_BY_MODE,
|
|
74
|
+
} from "./origin-guard.js";
|
|
70
75
|
import { handleRscRendering } from "./rsc-rendering.js";
|
|
71
76
|
import {
|
|
72
77
|
withTimeout,
|
|
@@ -83,6 +88,7 @@ import {
|
|
|
83
88
|
startSSRSetup,
|
|
84
89
|
getSSRSetup,
|
|
85
90
|
mayNeedSSR,
|
|
91
|
+
isRscRequest,
|
|
86
92
|
SSR_SETUP_VAR,
|
|
87
93
|
} from "./ssr-setup.js";
|
|
88
94
|
import {
|
|
@@ -124,12 +130,35 @@ import {
|
|
|
124
130
|
* });
|
|
125
131
|
* ```
|
|
126
132
|
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Response that tells the client to do a full document navigation. Shared by
|
|
136
|
+
* the terminal reload plans (version-mismatch and app-switch): an empty 200
|
|
137
|
+
* carrying X-RSC-Reload, which the client turns into window.location.href.
|
|
138
|
+
*/
|
|
139
|
+
function createReloadResponse(reloadUrl: string) {
|
|
140
|
+
return createResponseWithMergedHeaders(null, {
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: {
|
|
143
|
+
"X-RSC-Reload": reloadUrl,
|
|
144
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
127
149
|
export function createRSCHandler<
|
|
128
150
|
TEnv = unknown,
|
|
129
151
|
TRoutes extends Record<string, string> = Record<string, string>,
|
|
130
152
|
>(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
|
|
131
153
|
const { router, version = VERSION, nonce: nonceProvider } = options;
|
|
132
154
|
|
|
155
|
+
// Handler-owned registry of explicit per-scope stores from cache({ store }).
|
|
156
|
+
// Lives in the closure so it is scoped per handler (multi-router deployments
|
|
157
|
+
// get separate registries) and accumulates every explicit store this handler
|
|
158
|
+
// resolves across requests. updateTag()/revalidateTag() iterate it to reach
|
|
159
|
+
// stores not covered by the app-level ctx._cacheStore.
|
|
160
|
+
const explicitTaggedStores = new Set<SegmentCacheStore>();
|
|
161
|
+
|
|
133
162
|
// Use provided deps or default to @vitejs/plugin-rsc/rsc exports
|
|
134
163
|
const deps = options.deps ?? rscDeps;
|
|
135
164
|
const {
|
|
@@ -264,12 +293,13 @@ export function createRSCHandler<
|
|
|
264
293
|
function createRedirectFlightResponse(
|
|
265
294
|
redirectUrl: string,
|
|
266
295
|
locationState?: Record<string, unknown>,
|
|
296
|
+
external?: boolean,
|
|
267
297
|
): Response {
|
|
268
298
|
const redirectPayload: RscPayload = {
|
|
269
299
|
metadata: {
|
|
270
300
|
pathname: redirectUrl,
|
|
271
301
|
segments: [],
|
|
272
|
-
redirect: { url: redirectUrl },
|
|
302
|
+
redirect: { url: redirectUrl, ...(external && { external: true }) },
|
|
273
303
|
...(locationState && { locationState }),
|
|
274
304
|
},
|
|
275
305
|
};
|
|
@@ -421,9 +451,12 @@ export function createRSCHandler<
|
|
|
421
451
|
url,
|
|
422
452
|
variables,
|
|
423
453
|
cacheStore,
|
|
454
|
+
explicitTaggedStores,
|
|
424
455
|
cacheProfiles: router.cacheProfiles,
|
|
425
456
|
executionContext: executionCtx,
|
|
426
457
|
themeConfig: router.themeConfig,
|
|
458
|
+
stateCookieName: router.resolvedStateCookieName,
|
|
459
|
+
version,
|
|
427
460
|
});
|
|
428
461
|
if (earlyMetricsStore) {
|
|
429
462
|
requestContext._debugPerformance = true;
|
|
@@ -433,7 +466,7 @@ export function createRSCHandler<
|
|
|
433
466
|
// can surface non-fatal errors through the router's onError callback.
|
|
434
467
|
requestContext._reportBackgroundError = (
|
|
435
468
|
error: unknown,
|
|
436
|
-
category:
|
|
469
|
+
category: CacheErrorCategory,
|
|
437
470
|
) => {
|
|
438
471
|
callOnError(error, "cache", {
|
|
439
472
|
request,
|
|
@@ -539,7 +572,12 @@ export function createRSCHandler<
|
|
|
539
572
|
response.headers.set("Server-Timing", fullTiming);
|
|
540
573
|
}
|
|
541
574
|
|
|
542
|
-
|
|
575
|
+
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
576
|
+
// middleware short-circuit, response-route) funnels through here, so
|
|
577
|
+
// guarding browser-followed (3xx) redirects once covers them all and any
|
|
578
|
+
// future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
|
|
579
|
+
// through untouched (validated client-side instead).
|
|
580
|
+
return guardOutgoingRedirect(response, url.origin, router.basename);
|
|
543
581
|
});
|
|
544
582
|
};
|
|
545
583
|
|
|
@@ -597,10 +635,7 @@ export function createRSCHandler<
|
|
|
597
635
|
routerId: router.id,
|
|
598
636
|
});
|
|
599
637
|
} catch (error) {
|
|
600
|
-
if (
|
|
601
|
-
error instanceof RouteNotFoundError ||
|
|
602
|
-
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
603
|
-
) {
|
|
638
|
+
if (isRouteNotFoundError(error)) {
|
|
604
639
|
// Let the render path handle 404 — match()/matchPartial() will
|
|
605
640
|
// re-throw RouteNotFoundError and the catch block in
|
|
606
641
|
// executeRenderWithMiddleware renders the not-found page.
|
|
@@ -641,24 +676,18 @@ export function createRSCHandler<
|
|
|
641
676
|
console.log(
|
|
642
677
|
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
643
678
|
);
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
679
|
+
return createReloadResponse(plan.reloadUrl);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (plan.mode === "app-switch") {
|
|
683
|
+
// Cross-app SPA navigation crossed a host-router app boundary. Force a
|
|
684
|
+
// real document navigation so the target app's document is re-established
|
|
685
|
+
// (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
|
|
686
|
+
return createReloadResponse(plan.reloadUrl);
|
|
651
687
|
}
|
|
652
688
|
|
|
653
689
|
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
654
|
-
const originPhase
|
|
655
|
-
plan.mode === "action"
|
|
656
|
-
? "action"
|
|
657
|
-
: plan.mode === "loader"
|
|
658
|
-
? "loader"
|
|
659
|
-
: plan.mode === "pe-render"
|
|
660
|
-
? "pe-form"
|
|
661
|
-
: null;
|
|
690
|
+
const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
|
|
662
691
|
if (originPhase) {
|
|
663
692
|
const originResult = await checkRequestOrigin(
|
|
664
693
|
request,
|
|
@@ -925,47 +954,17 @@ export function createRSCHandler<
|
|
|
925
954
|
);
|
|
926
955
|
}
|
|
927
956
|
|
|
928
|
-
//
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
plan.
|
|
935
|
-
|
|
936
|
-
request,
|
|
937
|
-
env,
|
|
938
|
-
url,
|
|
939
|
-
variables,
|
|
940
|
-
nonce,
|
|
941
|
-
handleStore,
|
|
942
|
-
isPartial,
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// PE that fell through (handleProgressiveEnhancement returned null)
|
|
947
|
-
// falls back to full render
|
|
948
|
-
if (plan.mode === "pe-render") {
|
|
949
|
-
return executeRenderWithMiddleware(
|
|
950
|
-
plan.route.routeMiddleware,
|
|
951
|
-
false,
|
|
952
|
-
plan.route.routeKey,
|
|
953
|
-
routeReverse,
|
|
954
|
-
request,
|
|
955
|
-
env,
|
|
956
|
-
url,
|
|
957
|
-
variables,
|
|
958
|
-
nonce,
|
|
959
|
-
handleStore,
|
|
960
|
-
false,
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// Redirect plan that wasn't handled above (full-page redirect — let
|
|
965
|
-
// the pipeline handle it via match() which returns { redirect: url })
|
|
957
|
+
// Full render, partial render, fallen-through PE, and full-page redirect all
|
|
958
|
+
// render through the same middleware-wrapped path. Only full/partial-render
|
|
959
|
+
// carry negotiation + the partial flag; pe/redirect render plainly.
|
|
960
|
+
const isPartial = plan.mode === "partial-render";
|
|
961
|
+
const negotiated =
|
|
962
|
+
plan.mode === "full-render" || plan.mode === "partial-render"
|
|
963
|
+
? plan.negotiated
|
|
964
|
+
: false;
|
|
966
965
|
return executeRenderWithMiddleware(
|
|
967
966
|
plan.route.routeMiddleware,
|
|
968
|
-
|
|
967
|
+
negotiated,
|
|
969
968
|
plan.route.routeKey,
|
|
970
969
|
routeReverse,
|
|
971
970
|
request,
|
|
@@ -974,7 +973,7 @@ export function createRSCHandler<
|
|
|
974
973
|
variables,
|
|
975
974
|
nonce,
|
|
976
975
|
handleStore,
|
|
977
|
-
|
|
976
|
+
isPartial,
|
|
978
977
|
);
|
|
979
978
|
}
|
|
980
979
|
|
|
@@ -1025,10 +1024,19 @@ export function createRSCHandler<
|
|
|
1025
1024
|
} catch (error) {
|
|
1026
1025
|
// Check if middleware/handler returned Response
|
|
1027
1026
|
if (error instanceof Response) {
|
|
1027
|
+
// An action revalidation render is delivered to the client over the
|
|
1028
|
+
// same Flight-parsing path as a partial navigation, so a Response
|
|
1029
|
+
// thrown during it must be converted exactly like a partial one
|
|
1030
|
+
// (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
|
|
1031
|
+
// the no-middleware path returns the raw Response (the with-middleware
|
|
1032
|
+
// path is already covered by the isPartial || actionContinuation
|
|
1033
|
+
// guard below).
|
|
1034
|
+
const treatAsPartial = isPartial || actionContinuation != null;
|
|
1035
|
+
|
|
1028
1036
|
// During partial (client-side navigation), a 200 Response from a handler
|
|
1029
1037
|
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1030
1038
|
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1031
|
-
if (
|
|
1039
|
+
if (treatAsPartial && error.status === 200) {
|
|
1032
1040
|
console.warn(
|
|
1033
1041
|
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1034
1042
|
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
@@ -1042,7 +1050,7 @@ export function createRSCHandler<
|
|
|
1042
1050
|
});
|
|
1043
1051
|
}
|
|
1044
1052
|
|
|
1045
|
-
if (
|
|
1053
|
+
if (treatAsPartial) {
|
|
1046
1054
|
const intercepted = interceptRedirectForPartial(
|
|
1047
1055
|
error,
|
|
1048
1056
|
createRedirectFlightResponse,
|
|
@@ -1054,10 +1062,7 @@ export function createRSCHandler<
|
|
|
1054
1062
|
}
|
|
1055
1063
|
|
|
1056
1064
|
// Render 404 page for unmatched routes
|
|
1057
|
-
|
|
1058
|
-
error instanceof RouteNotFoundError ||
|
|
1059
|
-
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1060
|
-
if (isRouteNotFound) {
|
|
1065
|
+
if (isRouteNotFoundError(error)) {
|
|
1061
1066
|
callOnError(error, "routing", {
|
|
1062
1067
|
request,
|
|
1063
1068
|
url,
|
|
@@ -1092,6 +1097,7 @@ export function createRSCHandler<
|
|
|
1092
1097
|
rootLayout: router.rootLayout,
|
|
1093
1098
|
handles: handleStore.stream(),
|
|
1094
1099
|
version,
|
|
1100
|
+
stateCookieName: router.resolvedStateCookieName,
|
|
1095
1101
|
themeConfig: router.themeConfig,
|
|
1096
1102
|
warmupEnabled: router.warmupEnabled,
|
|
1097
1103
|
initialTheme: requireRequestContext().theme,
|
|
@@ -1104,16 +1110,15 @@ export function createRSCHandler<
|
|
|
1104
1110
|
},
|
|
1105
1111
|
});
|
|
1106
1112
|
|
|
1107
|
-
|
|
1108
|
-
isPartial ||
|
|
1109
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1110
|
-
!url.searchParams.has("__html")) ||
|
|
1111
|
-
url.searchParams.has("__rsc");
|
|
1112
|
-
|
|
1113
|
-
if (isRscRequest) {
|
|
1113
|
+
if (isRscRequest(request, url, isPartial)) {
|
|
1114
1114
|
return createResponseWithMergedHeaders(rscStream, {
|
|
1115
1115
|
status: 404,
|
|
1116
|
-
headers: {
|
|
1116
|
+
headers: {
|
|
1117
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
1118
|
+
// Router identity for the client's pre-decode integrity check; a
|
|
1119
|
+
// same-app 404 matches and applies in place. See response-adapter.
|
|
1120
|
+
"X-RSC-Router-Id": router.id,
|
|
1121
|
+
},
|
|
1117
1122
|
});
|
|
1118
1123
|
}
|
|
1119
1124
|
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -10,7 +10,32 @@ import {
|
|
|
10
10
|
} from "../server/request-context.js";
|
|
11
11
|
import type { RequestContext } from "../server/request-context.js";
|
|
12
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
14
|
+
import {
|
|
15
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
16
|
+
isExternalRedirect,
|
|
17
|
+
markExternalRedirect,
|
|
18
|
+
} from "../redirect-origin.js";
|
|
13
19
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
20
|
+
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
21
|
+
import type { RscPayload } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
25
|
+
* match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
|
|
26
|
+
* header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
|
|
27
|
+
* attached — output is byte-identical to the default. Header mutation failures
|
|
28
|
+
* are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
|
|
29
|
+
*/
|
|
30
|
+
function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
|
|
31
|
+
const signal = ctx._cacheSignal;
|
|
32
|
+
if (!signal || signal.length === 0) return;
|
|
33
|
+
try {
|
|
34
|
+
target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
|
|
35
|
+
} catch {
|
|
36
|
+
// Headers immutable — skip.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
14
39
|
|
|
15
40
|
/**
|
|
16
41
|
* Copy stub headers from the request context onto a target Headers instance:
|
|
@@ -21,6 +46,10 @@ import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
|
21
46
|
function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
22
47
|
stub.forEach((value, name) => {
|
|
23
48
|
try {
|
|
49
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
50
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
51
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
52
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
24
53
|
if (name.toLowerCase() === "set-cookie") {
|
|
25
54
|
target.append(name, value);
|
|
26
55
|
} else if (!target.has(name)) {
|
|
@@ -44,10 +73,19 @@ function drainOnResponseCallbacks(
|
|
|
44
73
|
const callbacks = ctx._onResponseCallbacks;
|
|
45
74
|
if (callbacks.length === 0) return response;
|
|
46
75
|
ctx._onResponseCallbacks = [];
|
|
76
|
+
// An onResponse callback may return a NEW Response (e.g. to add a header),
|
|
77
|
+
// which drops the out-of-band external-redirect brand (brand is keyed on
|
|
78
|
+
// Response object identity). Preserve a redirect(url, { external: true })
|
|
79
|
+
// opt-in across that rebuild so a callback can't silently neutralize the
|
|
80
|
+
// off-host redirect at the guard chokepoint.
|
|
81
|
+
const wasExternal = isExternalRedirect(response);
|
|
47
82
|
let result = response;
|
|
48
83
|
for (const callback of callbacks) {
|
|
49
84
|
result = callback(result) ?? result;
|
|
50
85
|
}
|
|
86
|
+
if (wasExternal && !isExternalRedirect(result)) {
|
|
87
|
+
markExternalRedirect(result);
|
|
88
|
+
}
|
|
51
89
|
return result;
|
|
52
90
|
}
|
|
53
91
|
|
|
@@ -84,6 +122,7 @@ export function createResponseWithMergedHeaders(
|
|
|
84
122
|
const mergedHeaders = new Headers(init.headers);
|
|
85
123
|
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
86
124
|
ctx.res.headers.delete("set-cookie");
|
|
125
|
+
applyCacheSignalHeader(mergedHeaders, ctx);
|
|
87
126
|
|
|
88
127
|
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
89
128
|
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
@@ -114,8 +153,20 @@ export function createSimpleRedirectResponse(redirectUrl: string): Response {
|
|
|
114
153
|
|
|
115
154
|
/**
|
|
116
155
|
* Carry over headers from a source redirect Response to a wrapper Response.
|
|
117
|
-
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
|
|
118
|
-
*
|
|
156
|
+
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper) and
|
|
157
|
+
* appends Set-Cookie to avoid clobbering multiple cookie headers.
|
|
158
|
+
*
|
|
159
|
+
* This is a GENERIC copier used by every redirect-rebuild path (PE
|
|
160
|
+
* extractRedirectResponse, the SPA intercept below, the guard's neutralize
|
|
161
|
+
* rebuild), so it has two redirect-specific jobs:
|
|
162
|
+
*
|
|
163
|
+
* 1. NEVER copy the reserved external-redirect header: it is no longer a trust
|
|
164
|
+
* signal (the opt-in is the out-of-band brand), and a forged value from a
|
|
165
|
+
* proxied upstream must not ride a rebuilt response to the browser.
|
|
166
|
+
* 2. Transfer the out-of-band external brand: a rebuilt document-native redirect
|
|
167
|
+
* has to carry the opt-in to the guard chokepoint, which reads and clears it.
|
|
168
|
+
* Without this transfer, redirect(url, { external: true }) would be silently
|
|
169
|
+
* neutralized on any rebuild path (fail-closed, but a feature regression).
|
|
119
170
|
*/
|
|
120
171
|
export function carryOverRedirectHeaders(
|
|
121
172
|
source: Response,
|
|
@@ -124,12 +175,16 @@ export function carryOverRedirectHeaders(
|
|
|
124
175
|
source.headers.forEach((value, name) => {
|
|
125
176
|
const lower = name.toLowerCase();
|
|
126
177
|
if (lower === "location" || lower === "x-rsc-redirect") return;
|
|
178
|
+
if (lower === EXTERNAL_REDIRECT_MARKER) return;
|
|
127
179
|
if (lower === "set-cookie") {
|
|
128
180
|
target.headers.append(name, value);
|
|
129
181
|
} else if (!target.headers.has(name)) {
|
|
130
182
|
target.headers.set(name, value);
|
|
131
183
|
}
|
|
132
184
|
});
|
|
185
|
+
if (isExternalRedirect(source)) {
|
|
186
|
+
markExternalRedirect(target);
|
|
187
|
+
}
|
|
133
188
|
}
|
|
134
189
|
|
|
135
190
|
/**
|
|
@@ -143,28 +198,62 @@ export function interceptRedirectForPartial(
|
|
|
143
198
|
createRedirectFlightResponse: (
|
|
144
199
|
redirectUrl: string,
|
|
145
200
|
locationState?: Record<string, unknown>,
|
|
201
|
+
external?: boolean,
|
|
146
202
|
) => Response,
|
|
147
203
|
): Response | null {
|
|
148
|
-
|
|
149
|
-
if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
|
|
204
|
+
if (!isRedirectResponse(response)) {
|
|
150
205
|
return null;
|
|
151
206
|
}
|
|
207
|
+
const redirectUrl = response.headers.get("Location")!;
|
|
208
|
+
// redirect(url, { external: true }) marks an explicit off-host redirect via
|
|
209
|
+
// the out-of-band brand (not a wire header). On the SPA/action channel the
|
|
210
|
+
// intent must travel as a Flight payload (metadata.redirect.external) so the
|
|
211
|
+
// client does a scheme-validated hard navigation (location.assign) rather than
|
|
212
|
+
// a partial fetch. The client re-validates the scheme; see partial-update.ts.
|
|
213
|
+
const external = isExternalRedirect(response);
|
|
152
214
|
const locationState = getLocationState();
|
|
153
215
|
let intercepted: Response;
|
|
154
216
|
if (locationState) {
|
|
155
217
|
intercepted = createRedirectFlightResponse(
|
|
156
218
|
redirectUrl,
|
|
157
219
|
resolveLocationStateEntries(locationState),
|
|
220
|
+
external,
|
|
158
221
|
);
|
|
222
|
+
} else if (external) {
|
|
223
|
+
intercepted = createRedirectFlightResponse(redirectUrl, undefined, true);
|
|
159
224
|
} else {
|
|
160
225
|
intercepted = createSimpleRedirectResponse(redirectUrl);
|
|
161
226
|
}
|
|
162
227
|
|
|
163
228
|
carryOverRedirectHeaders(response, intercepted);
|
|
229
|
+
// Defense-in-depth at the SPA browser-facing exit: carryOverRedirectHeaders
|
|
230
|
+
// already refuses to copy the reserved marker, but strip any value that might
|
|
231
|
+
// exist on `intercepted` so a forged header can never ride the 200/204 to the
|
|
232
|
+
// browser. The external intent travels in metadata.redirect.external (Flight),
|
|
233
|
+
// where the client re-validates the scheme.
|
|
234
|
+
try {
|
|
235
|
+
intercepted.headers.delete(EXTERNAL_REDIRECT_MARKER);
|
|
236
|
+
} catch {
|
|
237
|
+
// Immutable headers: the marker was never copied here, so this is inert.
|
|
238
|
+
}
|
|
164
239
|
|
|
165
240
|
return intercepted;
|
|
166
241
|
}
|
|
167
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Attach location state set during a request to a payload's metadata.
|
|
245
|
+
* No-op if no location state was set. Callers must ensure payload.metadata
|
|
246
|
+
* is populated (the non-null assertion holds for the partial/action payloads
|
|
247
|
+
* that reach this helper).
|
|
248
|
+
*/
|
|
249
|
+
export function attachLocationStateIfPresent(payload: RscPayload): void {
|
|
250
|
+
const locationState = getLocationState();
|
|
251
|
+
if (locationState) {
|
|
252
|
+
payload.metadata!.locationState =
|
|
253
|
+
resolveLocationStateEntries(locationState);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
168
257
|
/**
|
|
169
258
|
* Only cache successful responses. Non-200 statuses (errors, redirects) are
|
|
170
259
|
* not cached -- notFound() produces 500 in response routes, and explicit
|
|
@@ -191,7 +280,6 @@ export function buildRouteMiddlewareEntries<TEnv>(
|
|
|
191
280
|
regex: null,
|
|
192
281
|
paramNames: [],
|
|
193
282
|
handler: mw.handler,
|
|
194
|
-
mountPrefix: null,
|
|
195
283
|
} as MiddlewareEntry<TEnv>,
|
|
196
284
|
params: mw.params,
|
|
197
285
|
}));
|
package/src/rsc/index.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared serialization for `json()` response-route results.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own lightweight module (depends only on `errors.js`) so the
|
|
5
|
+
* `dispatch()` testing primitive can import it WITHOUT dragging in
|
|
6
|
+
* `response-route-handler.ts`'s heavy runtime graph, which transitively reaches
|
|
7
|
+
* a Vite virtual module and breaks a plain (non-Vite) vitest import.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RouterError } from "../errors.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serialize a `json()` response-route result, rejecting a nested unresolved
|
|
14
|
+
* Promise (the forgotten-await footgun: `() => ({ data: fetchSomething() })`).
|
|
15
|
+
* `JSON.stringify` would silently emit `{}` for a Promise, shipping empty data;
|
|
16
|
+
* the RSC pipeline awaits nested promises but this path does not. Throwing
|
|
17
|
+
* `RESPONSE_NOT_SERIALIZABLE` makes the failure loud.
|
|
18
|
+
*
|
|
19
|
+
* Shared by the production response-route handler and the `dispatch()` testing
|
|
20
|
+
* primitive so a `dispatch` json test of a forgotten await fails exactly where
|
|
21
|
+
* production 500s, instead of going green.
|
|
22
|
+
*/
|
|
23
|
+
export function stringifyJsonRouteResult(result: unknown): string {
|
|
24
|
+
return JSON.stringify(result, (_key, value) => {
|
|
25
|
+
if (
|
|
26
|
+
value != null &&
|
|
27
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
28
|
+
) {
|
|
29
|
+
throw new RouterError(
|
|
30
|
+
"RESPONSE_NOT_SERIALIZABLE",
|
|
31
|
+
"A json() response route returned a Promise (likely a forgotten " +
|
|
32
|
+
"await). Await async values before returning so they serialize, " +
|
|
33
|
+
"instead of emitting an empty {}.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRouteTrie,
|
|
14
14
|
setRouterManifest,
|
|
15
15
|
setRouterTrie,
|
|
16
|
+
setRouterPrecomputedEntries,
|
|
16
17
|
} from "../route-map-builder.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -36,47 +37,13 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
36
37
|
undefined,
|
|
37
38
|
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
39
|
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const routeToStaticPrefix: Record<string, string> = {};
|
|
47
|
-
for (const name of Object.keys(generated.routeManifest)) {
|
|
48
|
-
routeToStaticPrefix[name] = "";
|
|
49
|
-
}
|
|
50
|
-
// Override with prefix from include() entries so the trie
|
|
51
|
-
// returns the correct sp for lazy entry lookup in findMatch.
|
|
52
|
-
// Walk recursively to include routes in nested includes.
|
|
53
|
-
if (generated.prefixTree) {
|
|
54
|
-
const visitPrefixNode = (node: any): void => {
|
|
55
|
-
const sp = node.staticPrefix || "";
|
|
56
|
-
for (const route of node.routes || []) {
|
|
57
|
-
routeToStaticPrefix[route] = sp;
|
|
58
|
-
}
|
|
59
|
-
for (const child of Object.values(node.children || {})) {
|
|
60
|
-
visitPrefixNode(child);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
for (const node of Object.values(generated.prefixTree)) {
|
|
64
|
-
visitPrefixNode(node);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const trie = buildRouteTrie(
|
|
68
|
-
generated.routeManifest,
|
|
69
|
-
generated._routeAncestry,
|
|
70
|
-
routeToStaticPrefix,
|
|
71
|
-
generated.routeTrailingSlash,
|
|
72
|
-
generated.prerenderRoutes
|
|
73
|
-
? new Set(generated.prerenderRoutes)
|
|
74
|
-
: undefined,
|
|
75
|
-
generated.passthroughRoutes
|
|
76
|
-
? new Set(generated.passthroughRoutes)
|
|
77
|
-
: undefined,
|
|
78
|
-
generated.responseTypeRoutes,
|
|
79
|
-
);
|
|
40
|
+
// Build the trie through the SAME shared helper the production discovery uses
|
|
41
|
+
// (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
|
|
42
|
+
// serialized trie cannot drift. buildPerRouterTrie returns null when there
|
|
43
|
+
// are no routes.
|
|
44
|
+
const { buildPerRouterTrie } = await import("../build/route-trie.js");
|
|
45
|
+
const trie = buildPerRouterTrie(generated);
|
|
46
|
+
if (trie) {
|
|
80
47
|
setRouterTrie(router.id, trie);
|
|
81
48
|
// Set global trie only if not already set by another router
|
|
82
49
|
if (!getRouteTrie()) {
|
|
@@ -84,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
84
51
|
}
|
|
85
52
|
}
|
|
86
53
|
setRouterManifest(router.id, generated.routeManifest);
|
|
54
|
+
|
|
55
|
+
// Match the production discovery path: precompute leaf-include entries so the
|
|
56
|
+
// match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
|
|
57
|
+
// Without this, dev re-runs each matched leaf include's handler at match time
|
|
58
|
+
// (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
|
|
59
|
+
// match-time run is skipped and the handler runs once per first request.
|
|
60
|
+
// Identical route ownership to the handler path (the shortcut is guarded by
|
|
61
|
+
// the same prefixIsShared and #506 checks production uses).
|
|
62
|
+
const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
|
|
63
|
+
const precomputed: Array<{
|
|
64
|
+
staticPrefix: string;
|
|
65
|
+
routes: Record<string, string>;
|
|
66
|
+
}> = [];
|
|
67
|
+
flattenLeafEntries(
|
|
68
|
+
generated.prefixTree,
|
|
69
|
+
generated.routeManifest,
|
|
70
|
+
precomputed,
|
|
71
|
+
);
|
|
72
|
+
setRouterPrecomputedEntries(router.id, precomputed);
|
|
73
|
+
|
|
87
74
|
// Merge into global manifest (needed for reverse/href across routers)
|
|
88
75
|
const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
|
|
89
76
|
setCachedManifest({ ...existing, ...generated.routeManifest });
|