@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
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 +8 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- 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 +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -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/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- 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 +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- 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 +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -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 +2170 -377
- 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 +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- 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 +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- 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 +35 -15
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -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 +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +357 -0
- package/src/testing/render-route.tsx +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- 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 +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/rsc/helpers.ts
CHANGED
|
@@ -11,8 +11,14 @@ import {
|
|
|
11
11
|
import type { RequestContext } from "../server/request-context.js";
|
|
12
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
13
|
import { isRedirectResponse } from "../response-utils.js";
|
|
14
|
+
import {
|
|
15
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
16
|
+
isExternalRedirect,
|
|
17
|
+
markExternalRedirect,
|
|
18
|
+
} from "../redirect-origin.js";
|
|
14
19
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
15
20
|
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
21
|
+
import type { RscPayload } from "./types.js";
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
@@ -40,6 +46,10 @@ function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
|
|
|
40
46
|
function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
41
47
|
stub.forEach((value, name) => {
|
|
42
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;
|
|
43
53
|
if (name.toLowerCase() === "set-cookie") {
|
|
44
54
|
target.append(name, value);
|
|
45
55
|
} else if (!target.has(name)) {
|
|
@@ -55,18 +65,33 @@ function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
|
55
65
|
* Drain ctx._onResponseCallbacks onto a response. Swapping the array before
|
|
56
66
|
* iteration prevents re-entrant registrations from double-firing and matches
|
|
57
67
|
* the contract that each callback runs at most once per request.
|
|
68
|
+
*
|
|
69
|
+
* Exported so the testing primitives' buildRunResponse can reuse the exact
|
|
70
|
+
* production drain (swap-before-iterate + external-redirect brand preservation)
|
|
71
|
+
* rather than maintain a hand-synced copy. This module is plugin-rsc-free on its
|
|
72
|
+
* eager graph (dispatch.ts already statically imports from here in the same
|
|
73
|
+
* testing barrel), so importing it adds nothing to the unit runner's graph.
|
|
58
74
|
*/
|
|
59
|
-
function drainOnResponseCallbacks(
|
|
75
|
+
export function drainOnResponseCallbacks(
|
|
60
76
|
ctx: RequestContext,
|
|
61
77
|
response: Response,
|
|
62
78
|
): Response {
|
|
63
79
|
const callbacks = ctx._onResponseCallbacks;
|
|
64
80
|
if (callbacks.length === 0) return response;
|
|
65
81
|
ctx._onResponseCallbacks = [];
|
|
82
|
+
// An onResponse callback may return a NEW Response (e.g. to add a header),
|
|
83
|
+
// which drops the out-of-band external-redirect brand (brand is keyed on
|
|
84
|
+
// Response object identity). Preserve a redirect(url, { external: true })
|
|
85
|
+
// opt-in across that rebuild so a callback can't silently neutralize the
|
|
86
|
+
// off-host redirect at the guard chokepoint.
|
|
87
|
+
const wasExternal = isExternalRedirect(response);
|
|
66
88
|
let result = response;
|
|
67
89
|
for (const callback of callbacks) {
|
|
68
90
|
result = callback(result) ?? result;
|
|
69
91
|
}
|
|
92
|
+
if (wasExternal && !isExternalRedirect(result)) {
|
|
93
|
+
markExternalRedirect(result);
|
|
94
|
+
}
|
|
70
95
|
return result;
|
|
71
96
|
}
|
|
72
97
|
|
|
@@ -134,8 +159,20 @@ export function createSimpleRedirectResponse(redirectUrl: string): Response {
|
|
|
134
159
|
|
|
135
160
|
/**
|
|
136
161
|
* Carry over headers from a source redirect Response to a wrapper Response.
|
|
137
|
-
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
|
|
138
|
-
*
|
|
162
|
+
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper) and
|
|
163
|
+
* appends Set-Cookie to avoid clobbering multiple cookie headers.
|
|
164
|
+
*
|
|
165
|
+
* This is a GENERIC copier used by every redirect-rebuild path (PE
|
|
166
|
+
* extractRedirectResponse, the SPA intercept below, the guard's neutralize
|
|
167
|
+
* rebuild), so it has two redirect-specific jobs:
|
|
168
|
+
*
|
|
169
|
+
* 1. NEVER copy the reserved external-redirect header: it is no longer a trust
|
|
170
|
+
* signal (the opt-in is the out-of-band brand), and a forged value from a
|
|
171
|
+
* proxied upstream must not ride a rebuilt response to the browser.
|
|
172
|
+
* 2. Transfer the out-of-band external brand: a rebuilt document-native redirect
|
|
173
|
+
* has to carry the opt-in to the guard chokepoint, which reads and clears it.
|
|
174
|
+
* Without this transfer, redirect(url, { external: true }) would be silently
|
|
175
|
+
* neutralized on any rebuild path (fail-closed, but a feature regression).
|
|
139
176
|
*/
|
|
140
177
|
export function carryOverRedirectHeaders(
|
|
141
178
|
source: Response,
|
|
@@ -144,12 +181,16 @@ export function carryOverRedirectHeaders(
|
|
|
144
181
|
source.headers.forEach((value, name) => {
|
|
145
182
|
const lower = name.toLowerCase();
|
|
146
183
|
if (lower === "location" || lower === "x-rsc-redirect") return;
|
|
184
|
+
if (lower === EXTERNAL_REDIRECT_MARKER) return;
|
|
147
185
|
if (lower === "set-cookie") {
|
|
148
186
|
target.headers.append(name, value);
|
|
149
187
|
} else if (!target.headers.has(name)) {
|
|
150
188
|
target.headers.set(name, value);
|
|
151
189
|
}
|
|
152
190
|
});
|
|
191
|
+
if (isExternalRedirect(source)) {
|
|
192
|
+
markExternalRedirect(target);
|
|
193
|
+
}
|
|
153
194
|
}
|
|
154
195
|
|
|
155
196
|
/**
|
|
@@ -163,28 +204,62 @@ export function interceptRedirectForPartial(
|
|
|
163
204
|
createRedirectFlightResponse: (
|
|
164
205
|
redirectUrl: string,
|
|
165
206
|
locationState?: Record<string, unknown>,
|
|
207
|
+
external?: boolean,
|
|
166
208
|
) => Response,
|
|
167
209
|
): Response | null {
|
|
168
210
|
if (!isRedirectResponse(response)) {
|
|
169
211
|
return null;
|
|
170
212
|
}
|
|
171
213
|
const redirectUrl = response.headers.get("Location")!;
|
|
214
|
+
// redirect(url, { external: true }) marks an explicit off-host redirect via
|
|
215
|
+
// the out-of-band brand (not a wire header). On the SPA/action channel the
|
|
216
|
+
// intent must travel as a Flight payload (metadata.redirect.external) so the
|
|
217
|
+
// client does a scheme-validated hard navigation (location.assign) rather than
|
|
218
|
+
// a partial fetch. The client re-validates the scheme; see partial-update.ts.
|
|
219
|
+
const external = isExternalRedirect(response);
|
|
172
220
|
const locationState = getLocationState();
|
|
173
221
|
let intercepted: Response;
|
|
174
222
|
if (locationState) {
|
|
175
223
|
intercepted = createRedirectFlightResponse(
|
|
176
224
|
redirectUrl,
|
|
177
225
|
resolveLocationStateEntries(locationState),
|
|
226
|
+
external,
|
|
178
227
|
);
|
|
228
|
+
} else if (external) {
|
|
229
|
+
intercepted = createRedirectFlightResponse(redirectUrl, undefined, true);
|
|
179
230
|
} else {
|
|
180
231
|
intercepted = createSimpleRedirectResponse(redirectUrl);
|
|
181
232
|
}
|
|
182
233
|
|
|
183
234
|
carryOverRedirectHeaders(response, intercepted);
|
|
235
|
+
// Defense-in-depth at the SPA browser-facing exit: carryOverRedirectHeaders
|
|
236
|
+
// already refuses to copy the reserved marker, but strip any value that might
|
|
237
|
+
// exist on `intercepted` so a forged header can never ride the 200/204 to the
|
|
238
|
+
// browser. The external intent travels in metadata.redirect.external (Flight),
|
|
239
|
+
// where the client re-validates the scheme.
|
|
240
|
+
try {
|
|
241
|
+
intercepted.headers.delete(EXTERNAL_REDIRECT_MARKER);
|
|
242
|
+
} catch {
|
|
243
|
+
// Immutable headers: the marker was never copied here, so this is inert.
|
|
244
|
+
}
|
|
184
245
|
|
|
185
246
|
return intercepted;
|
|
186
247
|
}
|
|
187
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Attach location state set during a request to a payload's metadata.
|
|
251
|
+
* No-op if no location state was set. Callers must ensure payload.metadata
|
|
252
|
+
* is populated (the non-null assertion holds for the partial/action payloads
|
|
253
|
+
* that reach this helper).
|
|
254
|
+
*/
|
|
255
|
+
export function attachLocationStateIfPresent(payload: RscPayload): void {
|
|
256
|
+
const locationState = getLocationState();
|
|
257
|
+
if (locationState) {
|
|
258
|
+
payload.metadata!.locationState =
|
|
259
|
+
resolveLocationStateEntries(locationState);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
188
263
|
/**
|
|
189
264
|
* Only cache successful responses. Non-200 statuses (errors, redirects) are
|
|
190
265
|
* not cached -- notFound() produces 500 in response routes, and explicit
|
|
@@ -211,7 +286,6 @@ export function buildRouteMiddlewareEntries<TEnv>(
|
|
|
211
286
|
regex: null,
|
|
212
287
|
paramNames: [],
|
|
213
288
|
handler: mw.handler,
|
|
214
|
-
mountPrefix: null,
|
|
215
289
|
} as MiddlewareEntry<TEnv>,
|
|
216
290
|
params: mw.params,
|
|
217
291
|
}));
|
package/src/rsc/index.ts
CHANGED
|
@@ -30,7 +30,4 @@ export type {
|
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
|
|
32
32
|
// Re-export request context utilities for server-side access to env/request/params
|
|
33
|
-
export {
|
|
34
|
-
getRequestContext,
|
|
35
|
-
requireRequestContext,
|
|
36
|
-
} from "../server/request-context.js";
|
|
33
|
+
export { getRequestContext } from "../server/request-context.js";
|
|
@@ -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/loader-fetch.ts
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getLoaderLazy } from "../server/loader-registry.js";
|
|
15
|
+
import { DataNotFoundError } from "../errors.js";
|
|
15
16
|
import { executeLoaderMiddleware } from "../router/middleware.js";
|
|
16
|
-
import {
|
|
17
|
+
import { getRequestContext } from "../server/request-context.js";
|
|
18
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
17
19
|
import {
|
|
18
20
|
createReverseFunction,
|
|
19
21
|
stripInternalParams,
|
|
@@ -30,6 +32,62 @@ import {
|
|
|
30
32
|
} from "./helpers.js";
|
|
31
33
|
import type { HandlerContext } from "./handler-context.js";
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Build the 500 RSC error Response for a failed fetchable loader. Shared by the
|
|
37
|
+
* module-load-error catch (the import itself threw) and the loader-execution
|
|
38
|
+
* error catch — both call onError("loader"), serialize the same dev-gated error
|
|
39
|
+
* payload via renderToReadableStream (reporting render failures through
|
|
40
|
+
* onError("rendering")), and return a 500 text/x-component Response. The only
|
|
41
|
+
* per-site difference is the console.error label, passed in.
|
|
42
|
+
*/
|
|
43
|
+
function buildLoaderErrorResponse<TEnv>(
|
|
44
|
+
ctx: HandlerContext<TEnv>,
|
|
45
|
+
error: unknown,
|
|
46
|
+
meta: { request: Request; url: URL; env: TEnv; loaderId: string },
|
|
47
|
+
logLabel: string,
|
|
48
|
+
): Response {
|
|
49
|
+
const { request, url, env, loaderId } = meta;
|
|
50
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
51
|
+
|
|
52
|
+
console.error(logLabel, error);
|
|
53
|
+
|
|
54
|
+
ctx.callOnError(error, "loader", {
|
|
55
|
+
request,
|
|
56
|
+
url,
|
|
57
|
+
env,
|
|
58
|
+
loaderName: loaderId,
|
|
59
|
+
handledByBoundary: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
63
|
+
const errorPayload = {
|
|
64
|
+
loaderResult: null,
|
|
65
|
+
loaderError: {
|
|
66
|
+
message: isDev ? err.message : "An error occurred",
|
|
67
|
+
// Gate err.name to dev. In production it leaks the consumer's error class
|
|
68
|
+
// name (e.g. AuthError, PrismaClientKnownRequestError) to the client; the
|
|
69
|
+
// client only ever reads `message`, so the field is dead data outside dev.
|
|
70
|
+
// Matches sanitizeError's dev-only name contract.
|
|
71
|
+
name: isDev ? err.name : "Error",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
75
|
+
onError: (renderError: unknown) => {
|
|
76
|
+
ctx.callOnError(renderError, "rendering", {
|
|
77
|
+
request,
|
|
78
|
+
url,
|
|
79
|
+
env,
|
|
80
|
+
loaderName: loaderId,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
86
|
+
status: 500,
|
|
87
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
33
91
|
export async function handleLoaderFetch<TEnv>(
|
|
34
92
|
ctx: HandlerContext<TEnv>,
|
|
35
93
|
request: Request,
|
|
@@ -46,8 +104,22 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
46
104
|
});
|
|
47
105
|
}
|
|
48
106
|
|
|
49
|
-
// Look up loader lazily
|
|
50
|
-
|
|
107
|
+
// Look up loader lazily. getLoaderLazy returns undefined only when the id was
|
|
108
|
+
// never registered (genuine 404). A thrown error means the loader module
|
|
109
|
+
// EXISTS but its import failed (broken transitive import, syntax error, throw
|
|
110
|
+
// in top-level code) — a real server breakage that must surface as a 500 and
|
|
111
|
+
// fire onError, not be collapsed into a misleading "not found".
|
|
112
|
+
let registeredLoader;
|
|
113
|
+
try {
|
|
114
|
+
registeredLoader = await getLoaderLazy(loaderId);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return buildLoaderErrorResponse(
|
|
117
|
+
ctx,
|
|
118
|
+
error,
|
|
119
|
+
{ request, url, env, loaderId },
|
|
120
|
+
`[RSC] Loader module load failed for "${loaderId}":`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
51
123
|
if (!registeredLoader) {
|
|
52
124
|
return createResponseWithMergedHeaders(
|
|
53
125
|
`Loader "${loaderId}" not found in registry`,
|
|
@@ -124,7 +196,7 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
124
196
|
loaderParams,
|
|
125
197
|
variables,
|
|
126
198
|
async () => {
|
|
127
|
-
const reqCtx =
|
|
199
|
+
const reqCtx = getRequestContext();
|
|
128
200
|
// Merge route params (from previewMatch) with explicit loader params.
|
|
129
201
|
// Explicit params take precedence over route-matched params.
|
|
130
202
|
const resolvedRouteParams = routeParams ?? {};
|
|
@@ -162,7 +234,12 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
162
234
|
...(loaderFormData ? { formData: loaderFormData } : {}),
|
|
163
235
|
};
|
|
164
236
|
|
|
165
|
-
|
|
237
|
+
// Meter the fetchable-loader execution via observePhase, the sole
|
|
238
|
+
// funnel for this path (fn is called directly, not via ctx.use).
|
|
239
|
+
// depth:1 — a fetchable request has no render-phase parent.
|
|
240
|
+
const result = await observePhase(PHASES.loader(loaderId, 1), () =>
|
|
241
|
+
fn(loaderCtx),
|
|
242
|
+
);
|
|
166
243
|
|
|
167
244
|
interface LoaderPayload {
|
|
168
245
|
loaderResult: unknown;
|
|
@@ -190,40 +267,39 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
190
267
|
),
|
|
191
268
|
);
|
|
192
269
|
} catch (error) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
270
|
+
// A thrown Response is control flow, not an error: `throw redirect('/x')`
|
|
271
|
+
// throws a real Response (a 3xx). The with-middleware path already converts
|
|
272
|
+
// this to a returned Response (middleware.ts: `if (error instanceof Response)
|
|
273
|
+
// result = error`), but a fetchable loader with NO middleware reaches this
|
|
274
|
+
// catch directly, where the generic Error coercion below would turn it into a
|
|
275
|
+
// 500. Honor it the same way: re-wrap through createResponseWithMergedHeaders
|
|
276
|
+
// so the request context's stub cookies/headers merge, exactly like the
|
|
277
|
+
// returned-Response path. Mirrors rsc/handler.ts's `error instanceof Response`
|
|
278
|
+
// special-case.
|
|
279
|
+
if (error instanceof Response) {
|
|
280
|
+
return finalizeResponse(
|
|
281
|
+
createResponseWithMergedHeaders(error.body, {
|
|
282
|
+
status: error.status,
|
|
283
|
+
headers: error.headers,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
205
287
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
ctx.callOnError(error, "rendering", {
|
|
216
|
-
request,
|
|
217
|
-
url,
|
|
218
|
-
env,
|
|
219
|
-
loaderName: loaderId,
|
|
220
|
-
});
|
|
221
|
-
},
|
|
222
|
-
});
|
|
288
|
+
// notFound() throws a DataNotFoundError (an Error subclass, NOT a Response),
|
|
289
|
+
// so it does not match the branch above. Map it to a 404 before the generic
|
|
290
|
+
// 500 coercion so a no-middleware fetchable loader's notFound() is honored
|
|
291
|
+
// (the with-middleware path resolves it through the notFoundBoundary).
|
|
292
|
+
if (error instanceof DataNotFoundError) {
|
|
293
|
+
return finalizeResponse(
|
|
294
|
+
createResponseWithMergedHeaders(null, { status: 404 }),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
223
297
|
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
298
|
+
return buildLoaderErrorResponse(
|
|
299
|
+
ctx,
|
|
300
|
+
error,
|
|
301
|
+
{ request, url, env, loaderId },
|
|
302
|
+
"[RSC] Loader error:",
|
|
303
|
+
);
|
|
228
304
|
}
|
|
229
305
|
}
|
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
|
/**
|
|
@@ -31,52 +32,18 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
32
|
): Promise<void> {
|
|
32
33
|
const { generateManifestFull } =
|
|
33
34
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
35
|
+
const generated = await generateManifestFull(
|
|
35
36
|
router.urlpatterns,
|
|
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 });
|
package/src/rsc/nonce.ts
CHANGED
|
@@ -8,11 +8,20 @@ import { createVar } from "../context-var.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* Typed ContextVar token for CSP nonce.
|
|
10
10
|
*
|
|
11
|
-
* Use this to
|
|
11
|
+
* Use this to READ the nonce in middleware or handlers:
|
|
12
12
|
* ```ts
|
|
13
13
|
* import { nonce } from "@rangojs/router";
|
|
14
14
|
* const value = ctx.get(nonce); // string | undefined
|
|
15
15
|
* ```
|
|
16
|
+
*
|
|
17
|
+
* Supply the nonce via the `createRouter({ nonce })` provider, not by writing
|
|
18
|
+
* this token yourself. The provider value is threaded into the router's SSR
|
|
19
|
+
* machinery (NonceContext/useNonce, Scripts/MetaTags attributes, the inlined
|
|
20
|
+
* Flight payload scripts) AND sets this token. A direct `ctx.set(nonce, value)`
|
|
21
|
+
* in middleware runs AFTER the SSR nonce is resolved: the value is readable via
|
|
22
|
+
* `ctx.get(nonce)` and gates PPR shell capture (a per-request nonce must never
|
|
23
|
+
* bake into a shared shell), but the router will not apply it to its own
|
|
24
|
+
* scripts — useNonce() stays undefined for that request.
|
|
16
25
|
*/
|
|
17
26
|
export const nonce: ContextVar<string> = createVar<string>();
|
|
18
27
|
|
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
|
|
|
30
30
|
response: null,
|
|
31
31
|
redirect: null,
|
|
32
32
|
"version-mismatch": null,
|
|
33
|
+
// Terminal: handled before the origin guard (emits X-RSC-Reload, no execution).
|
|
34
|
+
"app-switch": null,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -67,11 +69,8 @@ export type OriginCheckConfig<TEnv = any> =
|
|
|
67
69
|
* Returns true to allow, false to reject.
|
|
68
70
|
*/
|
|
69
71
|
export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
70
|
-
// 1. Read Origin header (present on all cross-origin requests and
|
|
71
|
-
// same-origin POST/PUT/PATCH/DELETE in modern browsers)
|
|
72
72
|
let requestOrigin = request.headers.get("origin");
|
|
73
73
|
|
|
74
|
-
// 2. Fallback to Referer if Origin is absent (some proxies strip it)
|
|
75
74
|
if (!requestOrigin) {
|
|
76
75
|
const referer = request.headers.get("referer");
|
|
77
76
|
if (referer) {
|
|
@@ -83,23 +82,20 @@ export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
// 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
|
|
87
85
|
if (!requestOrigin) return true;
|
|
88
86
|
|
|
89
|
-
// "null" origin comes from privacy-sensitive contexts (data: URLs,
|
|
90
|
-
// sandboxed iframes, cross-origin redirects). Reject it.
|
|
91
87
|
if (requestOrigin === "null") return false;
|
|
92
88
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
const
|
|
89
|
+
// An Origin/Referer is present, so this is a browser request worth checking.
|
|
90
|
+
// Establish the expected origin from the Host header only -- browsers always
|
|
91
|
+
// send Host alongside Origin (runtimes synthesize it from the HTTP/2
|
|
92
|
+
// :authority), so a missing Host here is anomalous. Fail closed rather than
|
|
93
|
+
// fall back to url.host (derived from the request line) when the trusted Host
|
|
94
|
+
// cannot be established.
|
|
95
|
+
const expectedHost = request.headers.get("host");
|
|
96
|
+
if (!expectedHost) return false;
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
|
|
98
|
+
const expectedOrigin = `${url.protocol}//${expectedHost}`;
|
|
103
99
|
|
|
104
100
|
return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
|
|
105
101
|
}
|