@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad
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/AGENTS.md +17 -0
- package/README.md +291 -61
- package/dist/bin/rango.js +544 -143
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3744 -1329
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +67 -13
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +312 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +322 -19
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +282 -60
- package/skills/host-router/SKILL.md +278 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +297 -31
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +155 -111
- package/skills/rango/SKILL.md +251 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +104 -9
- package/skills/router-setup/SKILL.md +124 -11
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +125 -222
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -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 +123 -0
- package/skills/typesafety/SKILL.md +357 -52
- package/skills/use-cache/SKILL.md +46 -14
- 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 +67 -40
- package/src/bin/rango.ts +18 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +200 -30
- package/src/browser/navigation-client.ts +217 -58
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +312 -30
- package/src/browser/prefetch/fetch.ts +344 -47
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +125 -18
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -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 +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +80 -5
- package/src/browser/rsc-router.tsx +226 -75
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +619 -442
- package/src/browser/types.ts +115 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +7 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +182 -37
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +392 -106
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +45 -21
- package/src/client.tsx +120 -336
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +95 -0
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -5
- package/src/host/pattern-matcher.ts +23 -52
- 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 +79 -29
- package/src/index.ts +149 -65
- package/src/internal-debug.ts +11 -10
- 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/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +63 -26
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +110 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -49
- package/src/router/metrics.ts +217 -26
- package/src/router/middleware-types.ts +24 -110
- package/src/router/middleware.ts +384 -197
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +148 -91
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +199 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +144 -74
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +129 -36
- package/src/router/router-options.ts +185 -23
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +281 -76
- package/src/router/segment-resolution/helpers.ts +116 -31
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +493 -391
- 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 +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +180 -58
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +182 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +702 -460
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +98 -19
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +126 -106
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +269 -114
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +34 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +49 -41
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +317 -63
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +425 -177
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +25 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -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 +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -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 +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +3 -19
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +236 -88
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +10 -45
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -19
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +64 -91
- package/src/vite/discovery/discover-routers.ts +147 -88
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +247 -145
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +61 -13
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +10 -3
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +241 -287
- package/src/vite/router-discovery.ts +956 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -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 +20 -52
- package/src/vite/utils/prerender-utils.ts +141 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/CLAUDE.md +0 -5
- 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 -6
- package/src/network-error-thrower.tsx +0 -23
- package/src/route-definition/route-function.ts +0 -119
- package/src/router/middleware-cookies.ts +0 -55
package/src/rsc/server-action.ts
CHANGED
|
@@ -16,36 +16,35 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
getRequestContext,
|
|
20
20
|
setRequestContextParams,
|
|
21
|
-
getLocationState,
|
|
22
21
|
} from "../server/request-context.js";
|
|
23
|
-
import {
|
|
22
|
+
import { appendMetric } from "../router/metrics.js";
|
|
23
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
24
24
|
import type { RscPayload } from "./types.js";
|
|
25
25
|
import {
|
|
26
26
|
hasBodyContent,
|
|
27
27
|
createResponseWithMergedHeaders,
|
|
28
28
|
createSimpleRedirectResponse,
|
|
29
|
-
|
|
29
|
+
interceptRedirectForPartial,
|
|
30
|
+
attachLocationStateIfPresent,
|
|
30
31
|
} from "./helpers.js";
|
|
32
|
+
import { warnNonRedirectActionResponse } from "./runtime-warnings.js";
|
|
31
33
|
import type { HandlerContext } from "./handler-context.js";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Attach location state set during the action to a payload's metadata.
|
|
35
|
-
* No-op if no location state was set.
|
|
36
|
-
*/
|
|
37
|
-
function attachLocationState(payload: RscPayload): void {
|
|
38
|
-
const locationState = getLocationState();
|
|
39
|
-
if (locationState) {
|
|
40
|
-
payload.metadata!.locationState =
|
|
41
|
-
resolveLocationStateEntries(locationState);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
34
|
+
import type { MatchResult } from "../types.js";
|
|
44
35
|
|
|
45
36
|
/**
|
|
46
37
|
* Data flowing from action execution to the revalidation phase.
|
|
47
|
-
* When the action completes without redirect
|
|
48
|
-
*
|
|
38
|
+
* When the action completes without redirect, the handler passes this to route
|
|
39
|
+
* middleware → revalidateAfterAction.
|
|
40
|
+
*
|
|
41
|
+
* `errorBoundary` carries the matched error-boundary result when the action
|
|
42
|
+
* threw and a boundary matched. The error-boundary render is then performed in
|
|
43
|
+
* the revalidation phase so it runs INSIDE the same route-middleware wrapper as
|
|
44
|
+
* a successful revalidation — route middleware (context vars, headers, cookies)
|
|
45
|
+
* must apply to the error render too, matching the module doc's "identical to a
|
|
46
|
+
* normal render". When `errorBoundary` is set, `actionContext` is unused (the
|
|
47
|
+
* boundary is already matched; no matchPartial is run).
|
|
49
48
|
*/
|
|
50
49
|
export interface ActionContinuation {
|
|
51
50
|
returnValue: { ok: boolean; data: unknown };
|
|
@@ -59,6 +58,7 @@ export interface ActionContinuation {
|
|
|
59
58
|
actionResult: unknown;
|
|
60
59
|
formData?: FormData;
|
|
61
60
|
};
|
|
61
|
+
errorBoundary?: MatchResult;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
@@ -74,7 +74,7 @@ export async function executeServerAction<TEnv>(
|
|
|
74
74
|
env: TEnv,
|
|
75
75
|
url: URL,
|
|
76
76
|
actionId: string,
|
|
77
|
-
handleStore: ReturnType<typeof
|
|
77
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
78
78
|
): Promise<Response | ActionContinuation> {
|
|
79
79
|
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
80
80
|
|
|
@@ -88,17 +88,82 @@ export async function executeServerAction<TEnv>(
|
|
|
88
88
|
? await request.formData()
|
|
89
89
|
: await request.text();
|
|
90
90
|
|
|
91
|
-
if (body instanceof FormData) {
|
|
92
|
-
actionFormData = body;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
91
|
if (hasBodyContent(body)) {
|
|
96
92
|
args = await ctx.decodeReply(body, { temporaryReferences });
|
|
97
93
|
}
|
|
94
|
+
|
|
95
|
+
// Surface the action's FormData to shouldRevalidate({ formData }) for a JS
|
|
96
|
+
// server action, matching the PE path (progressive-enhancement.ts populates
|
|
97
|
+
// formData from request.formData()). A form-driven action is invoked as
|
|
98
|
+
// action(formData) (direct) or action(prevState, formData) (useActionState),
|
|
99
|
+
// so the FormData arrives INSIDE the decoded args. Use the LAST FormData arg:
|
|
100
|
+
// for useActionState the submitted form is the final arg, and a prior state
|
|
101
|
+
// that is itself a FormData would otherwise be picked first.
|
|
102
|
+
//
|
|
103
|
+
// The raw request body is NOT usable here: encodeReply wraps a FormData arg
|
|
104
|
+
// in a multipart envelope whose keys are Flight-encoded (e.g. `_1_name`,
|
|
105
|
+
// `0`), so request.formData() would hand shouldRevalidate a FormData with
|
|
106
|
+
// internal keys instead of the consumer's `name`. The decoded arg has the
|
|
107
|
+
// original keys.
|
|
108
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
109
|
+
if (args[i] instanceof FormData) {
|
|
110
|
+
actionFormData = args[i] as FormData;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
98
114
|
} catch (error) {
|
|
99
|
-
|
|
115
|
+
// Keep the original error as `cause` for server-side logging, but do not
|
|
116
|
+
// interpolate it into the message: that string can surface to the client
|
|
117
|
+
// and may leak decode internals.
|
|
118
|
+
const decodeError = new Error("Failed to decode action arguments", {
|
|
100
119
|
cause: error,
|
|
101
120
|
});
|
|
121
|
+
|
|
122
|
+
// Produce a router-controlled response instead of re-throwing into the
|
|
123
|
+
// host (which would surface as an opaque 500). This mirrors the no-JS PE
|
|
124
|
+
// path, where a malformed form body renders the route error boundary or
|
|
125
|
+
// returns an explicit 400 (progressive-enhancement.ts). Attempt boundary
|
|
126
|
+
// rendering first; if a boundary matches, defer the render to the
|
|
127
|
+
// revalidation phase (errorBoundary continuation) so it runs inside route
|
|
128
|
+
// middleware, identical to the action-threw path below. Otherwise return a
|
|
129
|
+
// plain 400 — the JS and no-JS paths now converge on the same outcome.
|
|
130
|
+
let decodeBoundary: MatchResult | undefined;
|
|
131
|
+
try {
|
|
132
|
+
decodeBoundary =
|
|
133
|
+
(await ctx.router.matchError(request, { env }, decodeError, "route")) ??
|
|
134
|
+
undefined;
|
|
135
|
+
} catch {
|
|
136
|
+
// matchError itself failed — fall through to the plain 400 below.
|
|
137
|
+
decodeBoundary = undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ctx.callOnError(decodeError, "action", {
|
|
141
|
+
request,
|
|
142
|
+
url,
|
|
143
|
+
env,
|
|
144
|
+
actionId,
|
|
145
|
+
handledByBoundary: !!decodeBoundary,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (decodeBoundary) {
|
|
149
|
+
return {
|
|
150
|
+
returnValue: { ok: false, data: decodeError },
|
|
151
|
+
// 400: malformed action request, matching the PE explicit-400 status
|
|
152
|
+
// class (the action-threw path uses 500; a decode failure is a bad
|
|
153
|
+
// request, not an action runtime error).
|
|
154
|
+
actionStatus: 400,
|
|
155
|
+
temporaryReferences,
|
|
156
|
+
actionContext: {
|
|
157
|
+
actionId,
|
|
158
|
+
actionUrl: new URL(url),
|
|
159
|
+
actionResult: decodeError,
|
|
160
|
+
formData: actionFormData,
|
|
161
|
+
},
|
|
162
|
+
errorBoundary: decodeBoundary,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return createResponseWithMergedHeaders(null, { status: 400 });
|
|
102
167
|
}
|
|
103
168
|
|
|
104
169
|
// Execute the server action
|
|
@@ -108,51 +173,37 @@ export async function executeServerAction<TEnv>(
|
|
|
108
173
|
|
|
109
174
|
try {
|
|
110
175
|
loadedAction = await ctx.loadServerAction(actionId);
|
|
111
|
-
|
|
176
|
+
let data = await loadedAction!.apply(null, args);
|
|
112
177
|
|
|
113
|
-
// Intercept redirect
|
|
114
|
-
//
|
|
115
|
-
// and the revalidation step would run unnecessarily.
|
|
178
|
+
// Intercept redirect Responses: serializing one as the action returnValue
|
|
179
|
+
// would fail, and revalidation would run needlessly.
|
|
116
180
|
if (data instanceof Response) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
129
|
-
}
|
|
130
|
-
carryOverRedirectHeaders(data, redirect);
|
|
131
|
-
return redirect;
|
|
181
|
+
const intercepted = interceptRedirectForPartial(
|
|
182
|
+
data,
|
|
183
|
+
ctx.createRedirectFlightResponse,
|
|
184
|
+
);
|
|
185
|
+
if (intercepted) return intercepted;
|
|
186
|
+
|
|
187
|
+
// Non-redirect Response returned (not thrown): a raw Response cannot be
|
|
188
|
+
// serialized into Flight. Discard it and re-render — mirroring the PE
|
|
189
|
+
// path (progressive-enhancement.ts) so JS and no-JS behave identically.
|
|
190
|
+
if (process.env.NODE_ENV !== "production") {
|
|
191
|
+
warnNonRedirectActionResponse(actionId);
|
|
132
192
|
}
|
|
193
|
+
data = undefined;
|
|
133
194
|
}
|
|
134
195
|
|
|
135
196
|
returnValue = { ok: true, data };
|
|
136
197
|
} catch (error) {
|
|
198
|
+
let actionResultData: unknown = error;
|
|
199
|
+
|
|
137
200
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
138
201
|
if (error instanceof Response) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
let redirect: Response;
|
|
145
|
-
if (locationState) {
|
|
146
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
147
|
-
redirectUrl,
|
|
148
|
-
resolveLocationStateEntries(locationState),
|
|
149
|
-
);
|
|
150
|
-
} else {
|
|
151
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
152
|
-
}
|
|
153
|
-
carryOverRedirectHeaders(error, redirect);
|
|
154
|
-
return redirect;
|
|
155
|
-
}
|
|
202
|
+
const intercepted = interceptRedirectForPartial(
|
|
203
|
+
error,
|
|
204
|
+
ctx.createRedirectFlightResponse,
|
|
205
|
+
);
|
|
206
|
+
if (intercepted) return intercepted;
|
|
156
207
|
|
|
157
208
|
// Non-redirect Response thrown from action — this will be treated
|
|
158
209
|
// as a regular error and routed to the error boundary. Warn in dev
|
|
@@ -165,9 +216,18 @@ export async function executeServerAction<TEnv>(
|
|
|
165
216
|
`Use \`throw redirect('/path')\` for redirects.`,
|
|
166
217
|
);
|
|
167
218
|
}
|
|
219
|
+
|
|
220
|
+
// A raw Response cannot be serialized into Flight; storing it as the
|
|
221
|
+
// action returnValue.data would make the error payload serialization
|
|
222
|
+
// throw and mask the boundary render. Replace it with a serializable
|
|
223
|
+
// error (mirrors the discard of a returned non-redirect Response above).
|
|
224
|
+
// matchError/onError still receive the original Response.
|
|
225
|
+
actionResultData = new Error(
|
|
226
|
+
`Server action "${actionId}" threw a non-redirect Response (status ${error.status})`,
|
|
227
|
+
);
|
|
168
228
|
}
|
|
169
229
|
|
|
170
|
-
returnValue = { ok: false, data:
|
|
230
|
+
returnValue = { ok: false, data: actionResultData };
|
|
171
231
|
actionStatus = 500;
|
|
172
232
|
|
|
173
233
|
// Try to render error boundary.
|
|
@@ -202,50 +262,46 @@ export async function executeServerAction<TEnv>(
|
|
|
202
262
|
});
|
|
203
263
|
|
|
204
264
|
if (errorResult) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
isError: true,
|
|
215
|
-
handles: handleStore.stream(),
|
|
216
|
-
version: ctx.version,
|
|
217
|
-
},
|
|
265
|
+
// Defer the error-boundary render to the revalidation phase so it runs
|
|
266
|
+
// inside the same route-middleware wrapper as a successful revalidation
|
|
267
|
+
// (handler.ts executeRenderWithMiddleware). Building + returning the
|
|
268
|
+
// Response here would bypass route middleware: context vars, headers, and
|
|
269
|
+
// cookies set by route middleware would NOT apply to the error render,
|
|
270
|
+
// diverging from the success path and from the module doc's "identical to
|
|
271
|
+
// a normal render". The boundary is already matched; the render in
|
|
272
|
+
// revalidateAfterAction uses errorResult directly (no matchPartial).
|
|
273
|
+
return {
|
|
218
274
|
returnValue,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// Intentionally omit attachLocationState for error payloads:
|
|
222
|
-
// location state is a success-only semantic. Error boundary responses
|
|
223
|
-
// update the error UI but should not mutate browser history state.
|
|
224
|
-
|
|
225
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
275
|
+
actionStatus,
|
|
226
276
|
temporaryReferences,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
277
|
+
// actionContext is unused on the errorBoundary path (no matchPartial).
|
|
278
|
+
actionContext: {
|
|
279
|
+
actionId,
|
|
280
|
+
actionUrl: new URL(url),
|
|
281
|
+
actionResult: returnValue.data,
|
|
282
|
+
formData: actionFormData,
|
|
283
|
+
},
|
|
284
|
+
errorBoundary: errorResult,
|
|
285
|
+
};
|
|
233
286
|
}
|
|
234
287
|
}
|
|
235
288
|
|
|
236
289
|
// Build continuation for the revalidation phase
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
290
|
+
const actionMeta = loadedAction as
|
|
291
|
+
| { $id?: string; $$id?: string }
|
|
292
|
+
| undefined;
|
|
293
|
+
const resolvedActionId = actionMeta?.$id ?? actionMeta?.$$id ?? actionId;
|
|
241
294
|
|
|
242
295
|
return {
|
|
243
296
|
returnValue,
|
|
244
297
|
actionStatus,
|
|
245
298
|
temporaryReferences,
|
|
246
299
|
actionContext: {
|
|
300
|
+
// Defensive copy of the already-parsed url (avoids re-parsing
|
|
301
|
+
// request.url). actionUrl is persisted into the continuation and later
|
|
302
|
+
// flows into matchPartial, so it must not alias the handler's live url.
|
|
247
303
|
actionId: resolvedActionId,
|
|
248
|
-
actionUrl: new URL(
|
|
304
|
+
actionUrl: new URL(url),
|
|
249
305
|
actionResult: returnValue.data,
|
|
250
306
|
formData: actionFormData,
|
|
251
307
|
},
|
|
@@ -264,16 +320,103 @@ export async function executeServerAction<TEnv>(
|
|
|
264
320
|
* provide. Redirects are the only non-partial outcome and are handled via
|
|
265
321
|
* X-RSC-Redirect headers before Flight deserialization.
|
|
266
322
|
*/
|
|
267
|
-
export
|
|
323
|
+
export function revalidateAfterAction<TEnv>(
|
|
268
324
|
ctx: HandlerContext<TEnv>,
|
|
269
325
|
request: Request,
|
|
270
326
|
env: TEnv,
|
|
271
327
|
url: URL,
|
|
272
|
-
handleStore: ReturnType<typeof
|
|
328
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
273
329
|
continuation: ActionContinuation,
|
|
274
330
|
): Promise<Response> {
|
|
275
|
-
|
|
276
|
-
|
|
331
|
+
// Instrument the action-revalidation render through the unified phase API,
|
|
332
|
+
// exactly like a normal navigation render (handleRscRendering). It records
|
|
333
|
+
// "render:total" AND opens "rango.render" from one boundary covering
|
|
334
|
+
// matchPartial -> serialize, so the revalidation loaders' rango.loader spans
|
|
335
|
+
// nest under a rango.render parent instead of dangling at the request root.
|
|
336
|
+
return observePhase(PHASES.render, () =>
|
|
337
|
+
revalidateAfterActionInner(
|
|
338
|
+
ctx,
|
|
339
|
+
request,
|
|
340
|
+
env,
|
|
341
|
+
url,
|
|
342
|
+
handleStore,
|
|
343
|
+
continuation,
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function revalidateAfterActionInner<TEnv>(
|
|
349
|
+
ctx: HandlerContext<TEnv>,
|
|
350
|
+
request: Request,
|
|
351
|
+
env: TEnv,
|
|
352
|
+
url: URL,
|
|
353
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
354
|
+
continuation: ActionContinuation,
|
|
355
|
+
): Promise<Response> {
|
|
356
|
+
const {
|
|
357
|
+
returnValue,
|
|
358
|
+
actionStatus,
|
|
359
|
+
temporaryReferences,
|
|
360
|
+
actionContext,
|
|
361
|
+
errorBoundary,
|
|
362
|
+
} = continuation;
|
|
363
|
+
const reqCtx = getRequestContext();
|
|
364
|
+
const metricsStore = reqCtx._metricsStore;
|
|
365
|
+
|
|
366
|
+
// Action threw and a boundary matched: render the (already-matched) error
|
|
367
|
+
// boundary here so it runs inside the route-middleware wrapper, exactly like
|
|
368
|
+
// the success branch below. setRequestContextParams + the payload mirror the
|
|
369
|
+
// pre-deferral render that executeServerAction used to do inline.
|
|
370
|
+
if (errorBoundary) {
|
|
371
|
+
setRequestContextParams(errorBoundary.params, errorBoundary.routeName);
|
|
372
|
+
|
|
373
|
+
const errorPayload: RscPayload = {
|
|
374
|
+
metadata: {
|
|
375
|
+
pathname: url.pathname,
|
|
376
|
+
// routerId exposed for the frontend (current app identity); see
|
|
377
|
+
// rsc-rendering.ts partial branch.
|
|
378
|
+
routerId: ctx.router.id,
|
|
379
|
+
segments: errorBoundary.segments,
|
|
380
|
+
isPartial: true,
|
|
381
|
+
matched: errorBoundary.matched,
|
|
382
|
+
diff: errorBoundary.diff,
|
|
383
|
+
resolvedIds: errorBoundary.resolvedIds,
|
|
384
|
+
params: errorBoundary.params,
|
|
385
|
+
isError: true,
|
|
386
|
+
handles: handleStore.stream(),
|
|
387
|
+
version: ctx.version,
|
|
388
|
+
},
|
|
389
|
+
returnValue,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Intentionally omit attachLocationState for error payloads: location state
|
|
393
|
+
// is a success-only semantic. Error boundary responses update the error UI
|
|
394
|
+
// but should not mutate browser history state.
|
|
395
|
+
|
|
396
|
+
const errorStart = performance.now();
|
|
397
|
+
const errorStream = ctx.renderToReadableStream<RscPayload>(errorPayload, {
|
|
398
|
+
temporaryReferences,
|
|
399
|
+
onError: (error: unknown) => {
|
|
400
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
appendMetric(
|
|
404
|
+
metricsStore,
|
|
405
|
+
"rsc-serialize",
|
|
406
|
+
errorStart,
|
|
407
|
+
performance.now() - errorStart,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return createResponseWithMergedHeaders(errorStream, {
|
|
411
|
+
status: actionStatus,
|
|
412
|
+
headers: {
|
|
413
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
414
|
+
// Router identity for the client's pre-decode integrity check (the
|
|
415
|
+
// action apply path has no post-decode guard). See response-adapter.
|
|
416
|
+
"X-RSC-Router-Id": ctx.router.id,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
277
420
|
|
|
278
421
|
const matchResult = await ctx.router.matchPartial(
|
|
279
422
|
request,
|
|
@@ -282,8 +425,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
282
425
|
);
|
|
283
426
|
|
|
284
427
|
if (!matchResult) {
|
|
285
|
-
// matchPartial returns null when the route is a redirect or
|
|
286
|
-
//
|
|
428
|
+
// matchPartial returns null when the route is a redirect or no previous-URL
|
|
429
|
+
// context could be resolved. Check for redirect first.
|
|
287
430
|
const fullMatch = await ctx.router.match(request, { env });
|
|
288
431
|
setRequestContextParams(fullMatch.params, fullMatch.routeName);
|
|
289
432
|
|
|
@@ -294,29 +437,35 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
294
437
|
return createSimpleRedirectResponse(fullMatch.redirect);
|
|
295
438
|
}
|
|
296
439
|
|
|
297
|
-
// Non-redirect: this branch is only reachable when
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
440
|
+
// Non-redirect: this branch is only reachable when no previous URL could
|
|
441
|
+
// be resolved (neither X-RSC-Router-Client-Path nor a usable Referer), or
|
|
442
|
+
// the previous URL was unparseable (defensive). The client requires
|
|
443
|
+
// isPartial for action responses, so producing a full payload here would
|
|
444
|
+
// be rejected. Return 500 instead.
|
|
301
445
|
throw new Error(
|
|
302
446
|
`[RSC] matchPartial returned null for a non-redirect route ` +
|
|
303
447
|
`during action revalidation (${url.pathname}). This indicates ` +
|
|
304
|
-
`a malformed action request
|
|
448
|
+
`a malformed action request: no previous-URL context could be ` +
|
|
449
|
+
`resolved (neither X-RSC-Router-Client-Path nor a usable Referer), ` +
|
|
450
|
+
`or the previous URL was unparseable.`,
|
|
305
451
|
);
|
|
306
452
|
}
|
|
307
453
|
|
|
308
454
|
// Return updated segments
|
|
309
455
|
setRequestContextParams(matchResult.params, matchResult.routeName);
|
|
310
456
|
|
|
311
|
-
const serverTiming = matchResult.serverTiming;
|
|
312
|
-
|
|
313
457
|
const payload: RscPayload = {
|
|
314
458
|
metadata: {
|
|
315
459
|
pathname: url.pathname,
|
|
460
|
+
// routerId exposed for the frontend (current app identity); see
|
|
461
|
+
// rsc-rendering.ts partial branch.
|
|
462
|
+
routerId: ctx.router.id,
|
|
316
463
|
segments: matchResult.segments,
|
|
317
464
|
isPartial: true,
|
|
318
465
|
matched: matchResult.matched,
|
|
319
466
|
diff: matchResult.diff,
|
|
467
|
+
resolvedIds: matchResult.resolvedIds,
|
|
468
|
+
params: matchResult.params,
|
|
320
469
|
slots: matchResult.slots,
|
|
321
470
|
handles: handleStore.stream(),
|
|
322
471
|
version: ctx.version,
|
|
@@ -324,21 +473,27 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
324
473
|
returnValue,
|
|
325
474
|
};
|
|
326
475
|
|
|
327
|
-
|
|
476
|
+
attachLocationStateIfPresent(payload);
|
|
328
477
|
|
|
478
|
+
const renderStart = performance.now();
|
|
329
479
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
330
480
|
temporaryReferences,
|
|
481
|
+
onError: (error: unknown) => {
|
|
482
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
483
|
+
},
|
|
331
484
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (serverTiming) {
|
|
337
|
-
actionHeaders["Server-Timing"] = serverTiming;
|
|
338
|
-
}
|
|
485
|
+
const rscSerializeDur = performance.now() - renderStart;
|
|
486
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
487
|
+
// render:total is recorded by the observePhase wrapper in revalidateAfterAction.
|
|
488
|
+
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
339
489
|
|
|
340
490
|
return createResponseWithMergedHeaders(rscStream, {
|
|
341
491
|
status: actionStatus,
|
|
342
|
-
headers:
|
|
492
|
+
headers: {
|
|
493
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
494
|
+
// Router identity for the client's pre-decode integrity check (the action
|
|
495
|
+
// apply path has no post-decode guard). See response-adapter.
|
|
496
|
+
"X-RSC-Router-Id": ctx.router.id,
|
|
497
|
+
},
|
|
343
498
|
});
|
|
344
499
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR Setup Utilities
|
|
3
|
+
*
|
|
4
|
+
* Manages early kickoff and retrieval of SSR module loading and stream mode
|
|
5
|
+
* resolution. Both operations are request-scoped but independent of route
|
|
6
|
+
* matching, so they can run in parallel with segment resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
10
|
+
import type { SSRModule } from "./types.js";
|
|
11
|
+
import type { SSRStreamMode } from "../router/router-options.js";
|
|
12
|
+
import type { MetricsStore } from "../server/context.js";
|
|
13
|
+
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
15
|
+
|
|
16
|
+
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Key used to stash the early SSR setup promise on request variables.
|
|
20
|
+
* Read back via `getSSRSetup`.
|
|
21
|
+
*/
|
|
22
|
+
export const SSR_SETUP_VAR = "__ssrSetup";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start loading the SSR module and resolving the stream mode in parallel.
|
|
26
|
+
* When a `getMetricsStore` getter is provided, records individual
|
|
27
|
+
* `ssr:module-load` and `ssr:stream-mode` metrics (the getter is called
|
|
28
|
+
* lazily so stores created after kickoff are still captured). Without a
|
|
29
|
+
* getter the promises run bare — no `.then()` microtasks, no
|
|
30
|
+
* `performance.now()` calls — keeping the non-debug hot path lean.
|
|
31
|
+
*/
|
|
32
|
+
export function startSSRSetup<TEnv>(
|
|
33
|
+
ctx: HandlerContext<TEnv>,
|
|
34
|
+
request: Request,
|
|
35
|
+
env: TEnv,
|
|
36
|
+
url: URL,
|
|
37
|
+
getMetricsStore?: () => MetricsStore | undefined,
|
|
38
|
+
): Promise<SSRSetup> {
|
|
39
|
+
if (!getMetricsStore) {
|
|
40
|
+
return Promise.all([
|
|
41
|
+
ctx.loadSSRModule(),
|
|
42
|
+
ctx.resolveStreamMode(request, env, url),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
return Promise.all([
|
|
47
|
+
ctx.loadSSRModule().then((mod) => {
|
|
48
|
+
appendMetric(
|
|
49
|
+
getMetricsStore(),
|
|
50
|
+
"ssr:module-load",
|
|
51
|
+
start,
|
|
52
|
+
performance.now() - start,
|
|
53
|
+
);
|
|
54
|
+
return mod;
|
|
55
|
+
}),
|
|
56
|
+
ctx.resolveStreamMode(request, env, url).then((mode) => {
|
|
57
|
+
appendMetric(
|
|
58
|
+
getMetricsStore(),
|
|
59
|
+
"ssr:stream-mode",
|
|
60
|
+
start,
|
|
61
|
+
performance.now() - start,
|
|
62
|
+
);
|
|
63
|
+
return mode;
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve the SSR setup result. Returns the early-kicked-off promise
|
|
70
|
+
* when available (stashed on request variables), otherwise starts a
|
|
71
|
+
* fresh setup.
|
|
72
|
+
*/
|
|
73
|
+
export function getSSRSetup<TEnv>(
|
|
74
|
+
ctx: HandlerContext<TEnv>,
|
|
75
|
+
request: Request,
|
|
76
|
+
env: TEnv,
|
|
77
|
+
url: URL,
|
|
78
|
+
metricsStore: MetricsStore | undefined,
|
|
79
|
+
): Promise<SSRSetup> {
|
|
80
|
+
const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
|
|
81
|
+
| Promise<SSRSetup>
|
|
82
|
+
| undefined;
|
|
83
|
+
if (early) return early;
|
|
84
|
+
return startSSRSetup(
|
|
85
|
+
ctx,
|
|
86
|
+
request,
|
|
87
|
+
env,
|
|
88
|
+
url,
|
|
89
|
+
metricsStore ? () => metricsStore : undefined,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify whether a request may require SSR (HTML rendering).
|
|
95
|
+
*
|
|
96
|
+
* Returns false for requests that are definitively RSC-only, loader fetches,
|
|
97
|
+
* prerender collection, or Accept-based RSC (no text/html). This mirrors
|
|
98
|
+
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
|
+
*
|
|
100
|
+
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
|
+
*/
|
|
103
|
+
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
|
+
if (
|
|
105
|
+
url.searchParams.has("_rsc_partial") ||
|
|
106
|
+
url.searchParams.has("_rsc_action") ||
|
|
107
|
+
request.headers.has("rsc-action") ||
|
|
108
|
+
url.searchParams.has("_rsc_loader") ||
|
|
109
|
+
url.searchParams.has("__rsc") ||
|
|
110
|
+
url.searchParams.has("__prerender_collect")
|
|
111
|
+
) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Mirror the Accept-based RSC decision from rsc-rendering.ts:
|
|
116
|
+
// if Accept is present and does not include text/html (and no __html override),
|
|
117
|
+
// the response will be RSC, not HTML.
|
|
118
|
+
const accept = request.headers.get("accept");
|
|
119
|
+
if (
|
|
120
|
+
accept &&
|
|
121
|
+
!accept.includes("text/html") &&
|
|
122
|
+
!url.searchParams.has("__html")
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
|
|
131
|
+
// from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
|
|
132
|
+
// Accept header as needing SSR; this treats it as RSC).
|
|
133
|
+
export function isRscRequest(
|
|
134
|
+
request: Request,
|
|
135
|
+
url: URL,
|
|
136
|
+
isPartial: boolean,
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
isPartial ||
|
|
140
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
141
|
+
!url.searchParams.has("__html")) ||
|
|
142
|
+
url.searchParams.has("__rsc")
|
|
143
|
+
);
|
|
144
|
+
}
|