@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/testing/dispatch.ts
CHANGED
|
@@ -7,25 +7,62 @@
|
|
|
7
7
|
* and middleware short-circuits behave exactly as in production. It deliberately
|
|
8
8
|
* does NOT render React Server Components: there is no Flight stream, no SSR,
|
|
9
9
|
* and no DOM. Hit an RSC (component) route and dispatch throws a clear error
|
|
10
|
-
* directing you to
|
|
10
|
+
* directing you to renderToFlightString/renderServerTree/renderHandler or an e2e test.
|
|
11
11
|
*
|
|
12
12
|
* What dispatch DOES support:
|
|
13
13
|
* - Trailing-slash and other findMatch() redirects -> 308 with Location
|
|
14
14
|
* - Unmatched paths -> 404 Response
|
|
15
|
+
* Both the 308 and the 404 are produced INSIDE the global middleware chain
|
|
16
|
+
* (mirroring production, where coreHandler runs wrapped by executeMiddleware),
|
|
17
|
+
* so a global auth middleware can 401/redirect them and middleware-set
|
|
18
|
+
* cookies/headers merge onto the 308/404 the way createResponseWithMergedHeaders
|
|
19
|
+
* merges them in production.
|
|
15
20
|
* - Response routes (non-RSC) -> serialized Response
|
|
16
|
-
* - json: JSON.stringify(
|
|
21
|
+
* - json: JSON.stringify(result) (bare value) with application/json
|
|
17
22
|
* - text/html/xml/md: String(result) with the mapped MIME type
|
|
18
|
-
* - handler returning a Response:
|
|
23
|
+
* - handler returning a Response: re-wrapped like
|
|
24
|
+
* handleResponseRoute (stub headers/cookies merged, Set-Cookie preserved,
|
|
25
|
+
* WebSocket upgrade passed through without reconstruction)
|
|
19
26
|
* - handler throwing an error: typed 500 / RouterError
|
|
20
|
-
* status, matching handleResponseRoute (
|
|
21
|
-
* routes, text/plain message otherwise)
|
|
27
|
+
* status, matching handleResponseRoute (RFC 9457 problem+json body with
|
|
28
|
+
* application/problem+json for json routes, text/plain message otherwise)
|
|
22
29
|
* - content-negotiated route: Vary: Accept appended
|
|
30
|
+
* - cached response route (cache({...})): getResponse/putResponse
|
|
31
|
+
* hit/SWR/tag write, resolved from the matched entry tree exactly as
|
|
32
|
+
* handleResponseRoute does. The write is scheduled via ctx.waitUntil
|
|
33
|
+
* (a microtask without an executionContext), so a HIT-asserting test must
|
|
34
|
+
* flush microtasks between the seeding dispatch and the asserting one.
|
|
23
35
|
* - Global middleware (router.use(...)) AND route-level middleware, with full
|
|
24
36
|
* next()/short-circuit/throw-Response/header+cookie-merge fidelity.
|
|
25
37
|
* - Partial (client-navigation) requests to a RESPONSE route (?_rsc_partial):
|
|
26
38
|
* global middleware runs first (so an auth gate can still 401/redirect),
|
|
27
39
|
* then — if it passes through — an X-RSC-Reload is returned. Route-level
|
|
28
40
|
* middleware is skipped on a partial, exactly as production skips it.
|
|
41
|
+
* - A middleware redirect (3xx + Location) on a partial/action request
|
|
42
|
+
* (?_rsc_partial / ?_rsc_action): converted to a 204 + X-RSC-Redirect via the
|
|
43
|
+
* real interceptRedirectForPartial, so fetch() does not auto-follow the 3xx —
|
|
44
|
+
* identical to production's no-location-state path.
|
|
45
|
+
* - The open-redirect guard (rsc/redirect-guard.ts) on full (browser-followed)
|
|
46
|
+
* redirects: a cross-origin Location is rewritten to the basename root unless
|
|
47
|
+
* redirect(url, { external: true }) opted out, mirroring production's single
|
|
48
|
+
* handler chokepoint. Soft partial/action redirects are 204 and pass through.
|
|
49
|
+
* - createRouter({ onError }) for CACHE / background-error degradation. dispatch
|
|
50
|
+
* wires the request context's _reportBackgroundError to the router's onError the
|
|
51
|
+
* same way the production RSC handler does, so a cache-read/cache-write/
|
|
52
|
+
* stale-revalidation failure on a cached response route (a throwing route
|
|
53
|
+
* cache({ key })/cache({ tags }), or a custom store whose keyGenerator/
|
|
54
|
+
* getResponse/putResponse throws) fires onError with phase "cache" and
|
|
55
|
+
* metadata.category, while the request still degrades-to-miss exactly as before.
|
|
56
|
+
* (A thrown response-route HANDLER error is the one onError path NOT covered —
|
|
57
|
+
* see "DOES NOT support" below.)
|
|
58
|
+
* - createRouter({ telemetry }) match-transaction lifecycle: request.start opens
|
|
59
|
+
* the transaction before the global middleware chain, request.end closes it
|
|
60
|
+
* after finalizeResponse (segmentCount 0 / cacheHit false — dispatch renders no
|
|
61
|
+
* RSC segments and holds no match-cache state), and a thrown non-Response error
|
|
62
|
+
* emits request.error with phase "routing". All three carry the same requestId
|
|
63
|
+
* (getRequestId). Emission is gated entirely on a configured sink; with none,
|
|
64
|
+
* dispatch does zero new work and stays byte-identical for existing callers.
|
|
65
|
+
* Lets a consumer unit-test their sink wiring in-process instead of only at e2e.
|
|
29
66
|
*
|
|
30
67
|
* What dispatch DOES NOT support (and why):
|
|
31
68
|
* - RSC component routes — rendering requires the Flight serializer + React
|
|
@@ -33,10 +70,27 @@
|
|
|
33
70
|
* This includes partial requests that resolve to a component route.
|
|
34
71
|
* - Server actions (?_rsc_action) — RSC protocol concerns handled by
|
|
35
72
|
* router.fetch().
|
|
36
|
-
* -
|
|
73
|
+
* - createRouter({ onError }) on a thrown response-route HANDLER error: the
|
|
37
74
|
* error is serialized into the same typed 500 / RouterError Response as
|
|
38
|
-
* production, but
|
|
39
|
-
* onError side effects with an e2e test.
|
|
75
|
+
* production, but onError is NOT invoked for that path here. Cover handler-error
|
|
76
|
+
* onError side effects with an e2e test. (This is the unchanged boundary; cache
|
|
77
|
+
* and other background errors DO route through onError — see below.)
|
|
78
|
+
* - Location-state-carrying redirects on a partial/action request: production
|
|
79
|
+
* embeds a Flight payload (createRedirectFlightResponse) so the client can
|
|
80
|
+
* restore location state across the redirect. dispatch is RSC-free, so it
|
|
81
|
+
* cannot emit that Flight stream. It falls back to the no-state behavior — a
|
|
82
|
+
* 204 + X-RSC-Redirect via createSimpleRedirectResponse — dropping the
|
|
83
|
+
* embedded location state. The 204 status, the X-RSC-Redirect header, and the
|
|
84
|
+
* merged cookies/headers all match production; only the Flight-embedded
|
|
85
|
+
* location-state entries are absent. Cover location-state restoration across a
|
|
86
|
+
* partial redirect with an e2e test.
|
|
87
|
+
* - Telemetry cache.decision / loader.* / handler.error events: these fire from
|
|
88
|
+
* the real match()/matchPartial() + RSC render + loader pipeline (match-
|
|
89
|
+
* handlers.ts, loader-resolution.ts, segment-resolution), none of which
|
|
90
|
+
* dispatch runs. Synthesizing them here would be faking (the events would not
|
|
91
|
+
* reflect a real cache lookup or loader run), so dispatch emits only the
|
|
92
|
+
* request.start/end/error lifecycle above; cover cache/loader telemetry with an
|
|
93
|
+
* e2e test driving a real RSC request.
|
|
40
94
|
*
|
|
41
95
|
* dispatch reuses router.previewMatch(), which itself runs content negotiation
|
|
42
96
|
* and resolves route middleware from the matched entry tree, so dispatch's
|
|
@@ -60,13 +114,36 @@ import {
|
|
|
60
114
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
61
115
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
62
116
|
import type { CacheProfile } from "../cache/profile-registry.js";
|
|
117
|
+
// cache-scope is loaded LAZILY inside the response-route cache path (below):
|
|
118
|
+
// its module graph pulls @vitejs/plugin-rsc/rsc (via segment-codec), which the
|
|
119
|
+
// non-Vite unit-test runner cannot resolve. A static import here would drag that
|
|
120
|
+
// onto the whole testing barrel's eager graph and break every consumer suite
|
|
121
|
+
// that imports `@rangojs/router/testing` without mocking plugin-rsc.
|
|
122
|
+
import type { EntryData } from "../server/context.js";
|
|
63
123
|
import { setRouterManifest } from "../route-map-builder.js";
|
|
64
124
|
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
65
125
|
import { RouterError } from "../errors.js";
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
126
|
+
import { createProblemDetails } from "../rsc/response-error.js";
|
|
127
|
+
import {
|
|
128
|
+
createResponseWithMergedHeaders,
|
|
129
|
+
createSimpleRedirectResponse,
|
|
130
|
+
finalizeResponse,
|
|
131
|
+
interceptRedirectForPartial,
|
|
132
|
+
mergeStubHeadersAndFinalize,
|
|
133
|
+
} from "../rsc/helpers.js";
|
|
134
|
+
import { guardOutgoingRedirect } from "../rsc/redirect-guard.js";
|
|
135
|
+
import { stringifyJsonRouteResult } from "../rsc/json-route-result.js";
|
|
136
|
+
import {
|
|
137
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
138
|
+
isExternalRedirect,
|
|
139
|
+
markExternalRedirect,
|
|
140
|
+
} from "../redirect-origin.js";
|
|
68
141
|
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
142
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
143
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
69
144
|
import type { Rango } from "../router/router-interfaces.js";
|
|
145
|
+
import { getRequestId, resolveSink, safeEmit } from "../router/telemetry.js";
|
|
146
|
+
import type { TelemetrySink } from "../router/telemetry.js";
|
|
70
147
|
|
|
71
148
|
/**
|
|
72
149
|
* The internal subset of the router surface dispatch depends on. The public
|
|
@@ -79,11 +156,19 @@ interface DispatchableRouter<TEnv> {
|
|
|
79
156
|
routerId?: string;
|
|
80
157
|
routeMap: Record<string, unknown>;
|
|
81
158
|
middleware: MiddlewareEntry<TEnv>[];
|
|
82
|
-
|
|
159
|
+
onError?: OnErrorCallback<TEnv>;
|
|
160
|
+
/**
|
|
161
|
+
* Optional telemetry sink from createRouter({ telemetry }) (RangoInternal
|
|
162
|
+
* field). dispatch emits the match-transaction lifecycle events onto it so a
|
|
163
|
+
* consumer can unit-test their sink wiring in-process; undefined keeps dispatch
|
|
164
|
+
* byte-identical for every existing caller.
|
|
165
|
+
*/
|
|
166
|
+
telemetry?: TelemetrySink;
|
|
167
|
+
findMatch(pathname: string): Promise<{
|
|
83
168
|
redirectTo?: string;
|
|
84
169
|
routeKey?: string;
|
|
85
170
|
params?: Record<string, string>;
|
|
86
|
-
} | null
|
|
171
|
+
} | null>;
|
|
87
172
|
previewMatch(
|
|
88
173
|
request: Request,
|
|
89
174
|
input?: { env?: TEnv },
|
|
@@ -97,6 +182,7 @@ interface DispatchableRouter<TEnv> {
|
|
|
97
182
|
params?: Record<string, string>;
|
|
98
183
|
routeKey?: string;
|
|
99
184
|
negotiated?: boolean;
|
|
185
|
+
manifestEntry?: EntryData;
|
|
100
186
|
} | null>;
|
|
101
187
|
basename?: string;
|
|
102
188
|
cache?:
|
|
@@ -112,6 +198,8 @@ interface DispatchableRouter<TEnv> {
|
|
|
112
198
|
* Options for dispatch.
|
|
113
199
|
*/
|
|
114
200
|
export interface DispatchOptions<TEnv = any> {
|
|
201
|
+
/** The request to dispatch: a `Request`, or a URL string (absolute or path). */
|
|
202
|
+
request: Request | string;
|
|
115
203
|
/** Environment bindings forwarded to matching and middleware. */
|
|
116
204
|
env?: TEnv;
|
|
117
205
|
}
|
|
@@ -124,20 +212,28 @@ function toRequest(request: Request | string): Request {
|
|
|
124
212
|
}
|
|
125
213
|
|
|
126
214
|
/**
|
|
127
|
-
* Serialize a response-route handler result, mirroring the
|
|
128
|
-
* handleResponseRoute() contract:
|
|
129
|
-
* -
|
|
130
|
-
*
|
|
215
|
+
* Serialize a NON-Response response-route handler result, mirroring the
|
|
216
|
+
* router's handleResponseRoute() contract:
|
|
217
|
+
* - "json" serializes the value (bare) with application/json, rejecting a nested
|
|
218
|
+
* unresolved Promise via the shared stringifyJsonRouteResult guard,
|
|
131
219
|
* - text/html/xml/md stringify with the mapped MIME type.
|
|
220
|
+
*
|
|
221
|
+
* A handler-returned Response is NOT routed here — callHandler re-wraps it via
|
|
222
|
+
* rewrapHandlerResponse (mirroring handleResponseRoute's rewrapResponse) so the
|
|
223
|
+
* WebSocket-upgrade bypass and Set-Cookie-preserving header merge match
|
|
224
|
+
* production.
|
|
132
225
|
*/
|
|
133
226
|
function serializeResponseRouteResult(
|
|
134
227
|
result: unknown,
|
|
135
228
|
responseType: string,
|
|
136
229
|
): Response {
|
|
137
|
-
if (result instanceof Response) return result;
|
|
138
|
-
|
|
139
230
|
if (responseType === "json") {
|
|
140
|
-
|
|
231
|
+
// Serialize through the SAME guard production uses: a nested unresolved
|
|
232
|
+
// Promise (forgotten await) throws RESPONSE_NOT_SERIALIZABLE here, caught by
|
|
233
|
+
// callHandler's catch and mapped to the identical typed 500 production
|
|
234
|
+
// returns -- so a dispatch json test fails exactly where production would,
|
|
235
|
+
// instead of silently emitting {} and passing.
|
|
236
|
+
return new Response(stringifyJsonRouteResult(result), {
|
|
141
237
|
status: 200,
|
|
142
238
|
headers: { "content-type": "application/json;charset=utf-8" },
|
|
143
239
|
});
|
|
@@ -162,27 +258,28 @@ function serializeResponseRouteResult(
|
|
|
162
258
|
/**
|
|
163
259
|
* Serialize a thrown handler error into the same typed Response the router's
|
|
164
260
|
* handleResponseRoute() catch block produces:
|
|
165
|
-
* -
|
|
166
|
-
* - "json" routes return { error: <createResponseErrorPayload> } as JSON,
|
|
261
|
+
* - "json" routes return an RFC 9457 problem+json body (application/problem+json),
|
|
167
262
|
* - all other types return a text/plain body (the RouterError message verbatim,
|
|
168
263
|
* the Error message in dev, else "Internal Server Error").
|
|
169
264
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
265
|
+
* `status` is the effective HTTP status resolved by the caller (RouterError.status
|
|
266
|
+
* or 500, overridden by ctx.setStatus()); it governs both the HTTP status and the
|
|
267
|
+
* problem body's `status`/`title` members. Reuses the production
|
|
268
|
+
* createProblemDetails so the error body is byte-identical rather than re-derived.
|
|
172
269
|
*/
|
|
173
270
|
function serializeResponseRouteError(
|
|
174
271
|
error: unknown,
|
|
175
272
|
responseType: string,
|
|
273
|
+
status: number,
|
|
176
274
|
): Response {
|
|
177
275
|
const isDev = process.env.NODE_ENV !== "production";
|
|
178
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
179
276
|
|
|
180
277
|
if (responseType === "json") {
|
|
181
278
|
return new Response(
|
|
182
|
-
JSON.stringify(
|
|
279
|
+
JSON.stringify(createProblemDetails(error, status, isDev)),
|
|
183
280
|
{
|
|
184
281
|
status,
|
|
185
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
282
|
+
headers: { "content-type": "application/problem+json;charset=utf-8" },
|
|
186
283
|
},
|
|
187
284
|
);
|
|
188
285
|
}
|
|
@@ -199,6 +296,51 @@ function serializeResponseRouteError(
|
|
|
199
296
|
});
|
|
200
297
|
}
|
|
201
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Re-wrap a handler-returned Response, byte-identical to handleResponseRoute's
|
|
301
|
+
* rewrapResponse:
|
|
302
|
+
* - A WebSocket upgrade (status 101 or a `webSocket` property) is returned via
|
|
303
|
+
* mergeStubHeadersAndFinalize WITHOUT reconstruction — the Response
|
|
304
|
+
* constructor rejects status 101, and an upgrade response's headers/socket
|
|
305
|
+
* must not be rebuilt.
|
|
306
|
+
* - Otherwise headers are copied into a fresh Headers (Set-Cookie appended to
|
|
307
|
+
* preserve duplicates, others set) and the Response is rebuilt through
|
|
308
|
+
* createResponseWithMergedHeaders so stub headers/cookies, the ctx.setStatus
|
|
309
|
+
* override, and onResponse callbacks merge exactly as in production. statusText
|
|
310
|
+
* is intentionally dropped (production does not carry it across the re-wrap).
|
|
311
|
+
*
|
|
312
|
+
* Must run inside runWithRequestContext (reads the ambient request context via
|
|
313
|
+
* the helpers), which callHandler guarantees.
|
|
314
|
+
*/
|
|
315
|
+
function rewrapHandlerResponse(result: Response): Response {
|
|
316
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
317
|
+
return mergeStubHeadersAndFinalize(result);
|
|
318
|
+
}
|
|
319
|
+
const headers = new Headers();
|
|
320
|
+
result.headers.forEach((value, key) => {
|
|
321
|
+
// Mirror production: never copy the reserved external-redirect marker off a
|
|
322
|
+
// handler result (it is not a trust signal; the opt-in is the out-of-band
|
|
323
|
+
// brand transferred below).
|
|
324
|
+
if (key.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
325
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
326
|
+
headers.append(key, value);
|
|
327
|
+
} else {
|
|
328
|
+
headers.set(key, value);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
const rewrapped = createResponseWithMergedHeaders(result.body, {
|
|
332
|
+
status: result.status,
|
|
333
|
+
headers,
|
|
334
|
+
});
|
|
335
|
+
// Mirror production's rewrapResponse: transfer the out-of-band external brand
|
|
336
|
+
// only from a genuinely branded result (a real redirect(url, { external:
|
|
337
|
+
// true })), never from a proxied upstream's forged header.
|
|
338
|
+
if (isExternalRedirect(result)) {
|
|
339
|
+
markExternalRedirect(rewrapped);
|
|
340
|
+
}
|
|
341
|
+
return rewrapped;
|
|
342
|
+
}
|
|
343
|
+
|
|
202
344
|
/**
|
|
203
345
|
* Run a request through the router in-process and return the Response.
|
|
204
346
|
*
|
|
@@ -208,21 +350,20 @@ function serializeResponseRouteError(
|
|
|
208
350
|
* path.json("/api/health", () => ({ ok: true }), { name: "health" }),
|
|
209
351
|
* ]));
|
|
210
352
|
*
|
|
211
|
-
* const res = await dispatch(router, "/api/health");
|
|
353
|
+
* const res = await dispatch(router, { request: "/api/health" });
|
|
212
354
|
* expect(res.status).toBe(200);
|
|
213
|
-
* expect(await res.json()).toEqual({
|
|
355
|
+
* expect(await res.json()).toEqual({ ok: true });
|
|
214
356
|
* ```
|
|
215
357
|
*/
|
|
216
358
|
export async function dispatch<TEnv = any>(
|
|
217
359
|
publicRouter: Rango<TEnv, any>,
|
|
218
|
-
|
|
219
|
-
opts: DispatchOptions<TEnv> = {},
|
|
360
|
+
opts: DispatchOptions<TEnv>,
|
|
220
361
|
): Promise<Response> {
|
|
221
362
|
// The public Rango type intentionally hides the matching internals; read them
|
|
222
363
|
// through the dispatchable shape (present at runtime). Consumers pass their
|
|
223
364
|
// real router with no cast.
|
|
224
365
|
const router = publicRouter as unknown as DispatchableRouter<TEnv>;
|
|
225
|
-
const req = toRequest(request);
|
|
366
|
+
const req = toRequest(opts.request);
|
|
226
367
|
const url = new URL(req.url);
|
|
227
368
|
const env = (opts.env ?? {}) as TEnv;
|
|
228
369
|
|
|
@@ -234,39 +375,35 @@ export async function dispatch<TEnv = any>(
|
|
|
234
375
|
|
|
235
376
|
// findMatch carries trailing-slash/redirect targets and null on no match.
|
|
236
377
|
// previewMatch swallows redirects, so detect them here first.
|
|
237
|
-
const match = router.findMatch(url.pathname);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return new Response(null, {
|
|
241
|
-
status: 308,
|
|
242
|
-
headers: { Location: match.redirectTo + url.search },
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!match) {
|
|
247
|
-
return new Response("Not Found", {
|
|
248
|
-
status: 404,
|
|
249
|
-
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
250
|
-
});
|
|
251
|
-
}
|
|
378
|
+
const match = await router.findMatch(url.pathname);
|
|
379
|
+
const redirectTo = match?.redirectTo;
|
|
380
|
+
const isUnmatched = !match;
|
|
252
381
|
|
|
253
382
|
// previewMatch resolves responseType, the response-route handler, and the
|
|
254
383
|
// route middleware from the matched entry tree (with content negotiation).
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
384
|
+
// Skip it for a redirect/unmatched path — there is no response route to
|
|
385
|
+
// resolve, and previewMatch would return null / a redirect marker anyway.
|
|
386
|
+
const preview =
|
|
387
|
+
redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
|
|
388
|
+
|
|
389
|
+
// A bare match with no responseType is an RSC route. The RSC-route throw is a
|
|
390
|
+
// hard boundary of this primitive (no Flight runtime), distinct from the
|
|
391
|
+
// 308/404 outcomes below, so it stays a pre-middleware guard.
|
|
259
392
|
const responseType = preview?.responseType;
|
|
260
393
|
const handler = preview?.handler;
|
|
261
|
-
const params = preview?.params ?? match
|
|
262
|
-
const routeKey = preview?.routeKey ?? match
|
|
263
|
-
|
|
264
|
-
if (
|
|
394
|
+
const params = preview?.params ?? match?.params ?? {};
|
|
395
|
+
const routeKey = preview?.routeKey ?? match?.routeKey;
|
|
396
|
+
|
|
397
|
+
if (
|
|
398
|
+
!redirectTo &&
|
|
399
|
+
!isUnmatched &&
|
|
400
|
+
(!responseType || typeof handler !== "function")
|
|
401
|
+
) {
|
|
265
402
|
throw new Error(
|
|
266
403
|
`dispatch() does not render RSC routes — the route matched at ` +
|
|
267
404
|
`"${url.pathname}" is a React Server Component route, not a response ` +
|
|
268
|
-
`route. Use
|
|
269
|
-
`exercise component rendering.`,
|
|
405
|
+
`route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
|
|
406
|
+
`e2e test to exercise component rendering.`,
|
|
270
407
|
);
|
|
271
408
|
}
|
|
272
409
|
|
|
@@ -293,6 +430,22 @@ export async function dispatch<TEnv = any>(
|
|
|
293
430
|
cacheStore,
|
|
294
431
|
cacheProfiles: router.cacheProfiles,
|
|
295
432
|
});
|
|
433
|
+
// Wire background error reporting so cache degradation (reportCacheError ->
|
|
434
|
+
// _reportBackgroundError) reaches the router's onError, mirroring the production
|
|
435
|
+
// RSC handler (rsc/handler.ts). Without this, dispatch could not observe onError.
|
|
436
|
+
requestContext._reportBackgroundError = (error, category) => {
|
|
437
|
+
if (error != null && typeof error === "object") {
|
|
438
|
+
if (requestContext._reportedErrors.has(error)) return;
|
|
439
|
+
requestContext._reportedErrors.add(error);
|
|
440
|
+
}
|
|
441
|
+
invokeOnError(
|
|
442
|
+
router.onError,
|
|
443
|
+
error,
|
|
444
|
+
"cache",
|
|
445
|
+
{ request: req, url, metadata: { category } },
|
|
446
|
+
"RSC",
|
|
447
|
+
);
|
|
448
|
+
};
|
|
296
449
|
// Match production: the RSC handler stores the router's basename on the
|
|
297
450
|
// request context (handler.ts), and redirect() prefixes root-relative URLs
|
|
298
451
|
// with it. Mirror it so basename-redirect tests behave as they do in a real
|
|
@@ -312,129 +465,349 @@ export async function dispatch<TEnv = any>(
|
|
|
312
465
|
search?: Record<string, unknown>,
|
|
313
466
|
) => string;
|
|
314
467
|
|
|
468
|
+
const isPartial = url.searchParams.has("_rsc_partial");
|
|
469
|
+
const isAction = url.searchParams.has("_rsc_action");
|
|
470
|
+
|
|
471
|
+
// Telemetry: mirror the router's match-transaction lifecycle onto the
|
|
472
|
+
// configured sink so a consumer can unit-test createRouter({ telemetry })
|
|
473
|
+
// wiring in-process (the dogfood gap the RSC-free dispatch left — see
|
|
474
|
+
// tests/cloudflare-basic/test/cache-status.test.ts). Every emit is gated on
|
|
475
|
+
// `sink` truthiness: with no sink configured dispatch does zero new work and
|
|
476
|
+
// stays byte-identical for existing callers. Only request.start/end/error are
|
|
477
|
+
// reachable here — cache.decision and loader.* originate in the real match()/
|
|
478
|
+
// matchPartial() + RSC render pipeline dispatch deliberately does not run
|
|
479
|
+
// (module header), so fabricating them would violate the no-fake rule.
|
|
480
|
+
const sink = router.telemetry;
|
|
481
|
+
const telemetryRequestId = sink ? getRequestId(req) : undefined;
|
|
482
|
+
const telemetryStart = sink ? performance.now() : 0;
|
|
483
|
+
|
|
315
484
|
return runWithRequestContext(requestContext, async () => {
|
|
316
485
|
// Set params before middleware/handler run, so global middleware sees
|
|
317
486
|
// ctx.params (production sets them during matching, before middleware).
|
|
487
|
+
// On a redirect/unmatched path there are no route params.
|
|
318
488
|
if (routeKey !== undefined) {
|
|
319
489
|
setRequestContextParams(params, routeKey);
|
|
320
490
|
} else {
|
|
321
491
|
requestContext.params = params;
|
|
322
492
|
}
|
|
323
493
|
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
494
|
+
// The response-route handler (with its own route middleware) lives inside
|
|
495
|
+
// coreHandler below, mirroring production where handleResponseRoute is
|
|
496
|
+
// nested inside coreHandler. Built lazily so a redirect/404 path never
|
|
497
|
+
// touches it.
|
|
498
|
+
const callResponseRoute = async (): Promise<Response> => {
|
|
499
|
+
// Match production: a partial (client-navigation) request to a response
|
|
500
|
+
// route is short-circuited to X-RSC-Reload (handleResponseRoute), BEFORE
|
|
501
|
+
// route-level middleware runs. Route-level middleware is skipped on a
|
|
502
|
+
// partial, exactly as production skips it.
|
|
503
|
+
const partialFinalHandler = async (): Promise<Response> =>
|
|
504
|
+
createResponseWithMergedHeaders(null, {
|
|
505
|
+
status: 200,
|
|
506
|
+
headers: {
|
|
507
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
508
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const cleanUrl = new URL(req.url);
|
|
513
|
+
for (const key of [...cleanUrl.searchParams.keys()]) {
|
|
514
|
+
if (key.startsWith("_rsc")) cleanUrl.searchParams.delete(key);
|
|
515
|
+
}
|
|
342
516
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
517
|
+
// Lightweight response-handler context mirroring handleResponseRoute.
|
|
518
|
+
const responseHandlerCtx = {
|
|
519
|
+
request: req,
|
|
520
|
+
params,
|
|
521
|
+
env,
|
|
522
|
+
searchParams: cleanUrl.searchParams,
|
|
523
|
+
url: cleanUrl,
|
|
524
|
+
originalUrl: requestContext.originalUrl,
|
|
525
|
+
pathname: url.pathname,
|
|
526
|
+
reverse,
|
|
527
|
+
get: requestContext.get,
|
|
528
|
+
header: (name: string, value: string) =>
|
|
529
|
+
requestContext.header(name, value),
|
|
530
|
+
waitUntil: requestContext.waitUntil.bind(requestContext),
|
|
531
|
+
executionContext: requestContext.executionContext,
|
|
532
|
+
_responseType: responseType,
|
|
533
|
+
};
|
|
534
|
+
// Brand as request-scoped so a "use cache" inside a response-route handler
|
|
535
|
+
// is detected as a request-scope violation here exactly as in production
|
|
536
|
+
// (response-route-handler.ts brands the same shape).
|
|
537
|
+
(responseHandlerCtx as Record<symbol, unknown>)[NOCACHE_SYMBOL] = true;
|
|
538
|
+
|
|
539
|
+
const callHandler = async (): Promise<Response> => {
|
|
540
|
+
let merged: Response;
|
|
541
|
+
try {
|
|
542
|
+
const result = await (handler as Function)(responseHandlerCtx);
|
|
543
|
+
if (result instanceof Response) {
|
|
544
|
+
// Handler returned a Response: mirror handleResponseRoute's
|
|
545
|
+
// rewrapResponse (WebSocket-upgrade bypass + Set-Cookie-preserving
|
|
546
|
+
// header rebuild, statusText dropped) rather than the generic
|
|
547
|
+
// createResponseWithMergedHeaders re-wrap below.
|
|
548
|
+
merged = rewrapHandlerResponse(result);
|
|
549
|
+
} else {
|
|
550
|
+
// Route the serialized (json/text/...) body through the SAME
|
|
551
|
+
// production finalizer the RSC handler uses, so ctx.onResponse()
|
|
552
|
+
// callbacks fire and stub headers/cookies + the ctx.setStatus
|
|
553
|
+
// override merge identically to production. Runs inside
|
|
554
|
+
// runWithRequestContext, so _getRequestContext() resolves here.
|
|
555
|
+
const serialized = serializeResponseRouteResult(
|
|
556
|
+
result,
|
|
557
|
+
responseType as string,
|
|
558
|
+
);
|
|
559
|
+
merged = createResponseWithMergedHeaders(serialized.body, {
|
|
560
|
+
status: serialized.status,
|
|
561
|
+
headers: serialized.headers,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
} catch (error) {
|
|
565
|
+
// Mirror handleResponseRoute's catch: a genuine handler error becomes
|
|
566
|
+
// the router's typed 500 / RouterError-status Response (NOT a rejected
|
|
567
|
+
// promise). Middleware short-circuit via thrown Response is handled by
|
|
568
|
+
// executeMiddleware and never reaches here.
|
|
569
|
+
const derivedStatus =
|
|
570
|
+
error instanceof RouterError ? error.status : 500;
|
|
571
|
+
// Resolve the effective status the way createResponseWithMergedHeaders
|
|
572
|
+
// (below) will (ctx.res.status override) BEFORE building the problem
|
|
573
|
+
// body, so the body's status/title match the actual HTTP status when a
|
|
574
|
+
// handler called ctx.setStatus() before throwing — exactly as
|
|
575
|
+
// handleResponseRoute resolves it.
|
|
576
|
+
const status =
|
|
577
|
+
requestContext.res.status !== 200
|
|
578
|
+
? requestContext.res.status
|
|
579
|
+
: derivedStatus;
|
|
580
|
+
const serialized = serializeResponseRouteError(
|
|
581
|
+
error,
|
|
582
|
+
responseType as string,
|
|
583
|
+
status,
|
|
584
|
+
);
|
|
585
|
+
merged = createResponseWithMergedHeaders(serialized.body, {
|
|
586
|
+
status: serialized.status,
|
|
587
|
+
headers: serialized.headers,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Append Vary: Accept on content-negotiated responses, matching
|
|
592
|
+
// handleResponseRoute's callHandlerWithVary. Skipped on WebSocket
|
|
593
|
+
// upgrade responses (immutable headers, Vary meaningless for a 101).
|
|
594
|
+
if (preview?.negotiated && !isWebSocketUpgradeResponse(merged)) {
|
|
595
|
+
merged.headers.append("Vary", "Accept");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return merged;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// On a partial request the reload IS the terminal handler and route
|
|
602
|
+
// middleware is skipped; otherwise the response-route handler is wrapped
|
|
603
|
+
// by route-level middleware (production order: route middleware runs
|
|
604
|
+
// inside handleResponseRoute, after the global chain).
|
|
605
|
+
if (isPartial) {
|
|
606
|
+
return partialFinalHandler();
|
|
607
|
+
}
|
|
347
608
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
609
|
+
// executeHandler = callHandler wrapped by route-level middleware, exactly
|
|
610
|
+
// the unit the production response cache wraps (response-route-handler.ts).
|
|
611
|
+
const executeHandler = (): Promise<Response> => {
|
|
612
|
+
const routeMiddlewareEntries = (preview?.routeMiddleware ?? []).map(
|
|
613
|
+
(mw) => ({
|
|
614
|
+
entry: {
|
|
615
|
+
pattern: null,
|
|
616
|
+
regex: null,
|
|
617
|
+
paramNames: [],
|
|
618
|
+
handler: mw.handler,
|
|
619
|
+
} as MiddlewareEntry<TEnv>,
|
|
620
|
+
params: mw.params,
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
if (routeMiddlewareEntries.length === 0) {
|
|
624
|
+
return callHandler();
|
|
625
|
+
}
|
|
626
|
+
return executeMiddleware<TEnv>(
|
|
627
|
+
routeMiddlewareEntries,
|
|
628
|
+
req,
|
|
629
|
+
env,
|
|
630
|
+
variables,
|
|
631
|
+
callHandler,
|
|
632
|
+
reverse,
|
|
633
|
+
);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Response-route cache path: resolved through the SAME shared serve leaf
|
|
637
|
+
// (rsc/response-cache-serve.ts) production uses, so a cached
|
|
638
|
+
// path.json/path.text route hits/SWRs/writes tags through dispatch exactly
|
|
639
|
+
// as in production — and the two can never drift. Resolved from the matched
|
|
640
|
+
// entry tree (preview.manifestEntry), which previewMatch surfaces for
|
|
641
|
+
// response routes.
|
|
642
|
+
const manifestEntry = preview?.manifestEntry;
|
|
643
|
+
if (manifestEntry) {
|
|
644
|
+
// Lazy so the testing barrel's eager graph stays plugin-rsc-free (see the
|
|
645
|
+
// import note above): the leaf takes createCacheScope/resolveCacheTags as
|
|
646
|
+
// INJECTED deps so it never imports plugin-rsc; we hand it the lazily
|
|
647
|
+
// imported pair here, only once a response route actually matched.
|
|
648
|
+
const cacheScopeMod = await import("../cache/cache-scope.js");
|
|
649
|
+
const { serveResponseRouteWithCache } =
|
|
650
|
+
await import("../rsc/response-cache-serve.js");
|
|
651
|
+
// requestContext is RequestContext<TEnv>; the leaf is typed against the
|
|
652
|
+
// default-env RequestContext (it reads only env-agnostic config).
|
|
653
|
+
// Assignable in the router's own tsc but not when a consumer pins a
|
|
654
|
+
// concrete Env — cast to the leaf's param type.
|
|
655
|
+
const cached = await serveResponseRouteWithCache({
|
|
656
|
+
reqCtx: requestContext as Parameters<
|
|
657
|
+
typeof serveResponseRouteWithCache
|
|
658
|
+
>[0]["reqCtx"],
|
|
659
|
+
manifestEntry,
|
|
660
|
+
responseType: responseType as string,
|
|
661
|
+
url,
|
|
662
|
+
executeHandler,
|
|
663
|
+
deps: {
|
|
664
|
+
createCacheScope: cacheScopeMod.createCacheScope,
|
|
665
|
+
resolveCacheTags: cacheScopeMod.resolveCacheTags,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
if (cached !== undefined) return cached;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return executeHandler().then(finalizeResponse);
|
|
364
672
|
};
|
|
365
|
-
|
|
366
|
-
// is
|
|
367
|
-
// (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
673
|
+
|
|
674
|
+
// coreHandler is the single terminal the global middleware chain wraps,
|
|
675
|
+
// mirroring production's coreHandler (handler.ts): a trailing-slash/redirect
|
|
676
|
+
// 308, an unmatched-path 404, or the response route. Both the 308 and the
|
|
677
|
+
// 404 are produced via createResponseWithMergedHeaders so middleware-set
|
|
678
|
+
// cookies/headers merge onto them, identical to production's
|
|
679
|
+
// rsc-rendering.ts redirect path — and because they sit inside the chain, a
|
|
680
|
+
// global middleware that short-circuits (e.g. an auth 401) runs first and
|
|
681
|
+
// wins, never reaching the 308/404.
|
|
682
|
+
const coreHandler = async (): Promise<Response> => {
|
|
683
|
+
if (redirectTo) {
|
|
684
|
+
return createResponseWithMergedHeaders(null, {
|
|
685
|
+
status: 308,
|
|
686
|
+
headers: { Location: redirectTo + url.search },
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (isUnmatched) {
|
|
690
|
+
return createResponseWithMergedHeaders("Not Found", {
|
|
691
|
+
status: 404,
|
|
692
|
+
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
693
|
+
});
|
|
381
694
|
}
|
|
695
|
+
return callResponseRoute();
|
|
696
|
+
};
|
|
382
697
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
698
|
+
// request.start opens the match transaction, mirroring match-handlers.ts.
|
|
699
|
+
// transaction is always "match" (dispatch has no matchPartial split);
|
|
700
|
+
// isPartial carries the ?_rsc_partial signal the same way production does.
|
|
701
|
+
if (sink) {
|
|
702
|
+
safeEmit(resolveSink(sink), {
|
|
703
|
+
type: "request.start",
|
|
704
|
+
timestamp: telemetryStart,
|
|
705
|
+
requestId: telemetryRequestId,
|
|
706
|
+
method: req.method,
|
|
707
|
+
pathname: url.pathname,
|
|
708
|
+
transaction: "match",
|
|
709
|
+
isPartial,
|
|
392
710
|
});
|
|
711
|
+
}
|
|
393
712
|
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
713
|
+
try {
|
|
714
|
+
// Global (pattern-matched) middleware wraps coreHandler, exactly as
|
|
715
|
+
// production wraps coreHandler with executeMiddleware (handler.ts).
|
|
716
|
+
const globalMatches = matchMiddleware(url.pathname, router.middleware);
|
|
717
|
+
const mwResponse =
|
|
718
|
+
globalMatches.length === 0
|
|
719
|
+
? await coreHandler()
|
|
720
|
+
: await executeMiddleware<TEnv>(
|
|
721
|
+
globalMatches,
|
|
722
|
+
req,
|
|
723
|
+
env,
|
|
724
|
+
variables,
|
|
725
|
+
coreHandler,
|
|
726
|
+
reverse,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
730
|
+
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
731
|
+
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
732
|
+
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
733
|
+
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
734
|
+
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
735
|
+
let finalResponse: Response;
|
|
736
|
+
if (isPartial || isAction) {
|
|
737
|
+
const intercepted = interceptRedirectForPartial(
|
|
738
|
+
mwResponse,
|
|
739
|
+
(redirectUrl) => createSimpleRedirectResponse(redirectUrl),
|
|
740
|
+
);
|
|
741
|
+
finalResponse = finalizeResponse(intercepted ?? mwResponse);
|
|
742
|
+
} else {
|
|
743
|
+
finalResponse = finalizeResponse(mwResponse);
|
|
399
744
|
}
|
|
400
745
|
|
|
401
|
-
|
|
402
|
-
|
|
746
|
+
// request.end closes the transaction. dispatch produces no RSC segments and
|
|
747
|
+
// holds no match-cache state, so segmentCount/cacheHit are 0/false — the
|
|
748
|
+
// same shape production emits for its own redirect (segment-less) result.
|
|
749
|
+
if (sink) {
|
|
750
|
+
safeEmit(resolveSink(sink), {
|
|
751
|
+
type: "request.end",
|
|
752
|
+
timestamp: performance.now(),
|
|
753
|
+
requestId: telemetryRequestId,
|
|
754
|
+
method: req.method,
|
|
755
|
+
pathname: url.pathname,
|
|
756
|
+
transaction: "match",
|
|
757
|
+
durationMs: performance.now() - telemetryStart,
|
|
758
|
+
segmentCount: 0,
|
|
759
|
+
cacheHit: false,
|
|
760
|
+
// dispatch's final response IS built before request.end (unlike
|
|
761
|
+
// match()/matchPartial(), whose Response is built after), so stamp its
|
|
762
|
+
// status — the same field a thrown-Response short-circuit carries.
|
|
763
|
+
status: finalResponse.status,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
403
766
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
767
|
+
// Mirror production's single open-redirect chokepoint (handler.ts): every
|
|
768
|
+
// browser-followed (3xx + Location) redirect is same-origin guarded before
|
|
769
|
+
// it leaves -- a cross-origin Location is rewritten to the basename root
|
|
770
|
+
// unless redirect(url, { external: true }) opted out. Soft partial/action
|
|
771
|
+
// redirects are 204 + X-RSC-Redirect and pass through untouched (the client
|
|
772
|
+
// validates them), so this is a no-op for them.
|
|
773
|
+
return guardOutgoingRedirect(finalResponse, url.origin, router.basename);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (sink) {
|
|
776
|
+
if (error instanceof Response) {
|
|
777
|
+
// executeMiddleware absorbs a middleware-thrown Response and returns it
|
|
778
|
+
// (middleware.ts:566), so a thrown Response never actually reaches this
|
|
779
|
+
// level in dispatch. Defensive: if one ever does it is a completed
|
|
780
|
+
// request from the consumer's seat (a short-circuit redirect), so mirror
|
|
781
|
+
// plan 002 / match-handlers.ts and emit request.end, not request.error.
|
|
782
|
+
safeEmit(resolveSink(sink), {
|
|
783
|
+
type: "request.end",
|
|
784
|
+
timestamp: performance.now(),
|
|
785
|
+
requestId: telemetryRequestId,
|
|
786
|
+
method: req.method,
|
|
787
|
+
pathname: url.pathname,
|
|
788
|
+
transaction: "match",
|
|
789
|
+
durationMs: performance.now() - telemetryStart,
|
|
790
|
+
segmentCount: 0,
|
|
791
|
+
cacheHit: false,
|
|
792
|
+
// Carry the short-circuit Response's status (parity with
|
|
793
|
+
// match-handlers.ts's thrown-Response request.end).
|
|
794
|
+
status: error.status,
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
safeEmit(resolveSink(sink), {
|
|
798
|
+
type: "request.error",
|
|
799
|
+
timestamp: performance.now(),
|
|
800
|
+
requestId: telemetryRequestId,
|
|
801
|
+
method: req.method,
|
|
802
|
+
pathname: url.pathname,
|
|
803
|
+
transaction: "match",
|
|
804
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
805
|
+
phase: "routing",
|
|
806
|
+
durationMs: performance.now() - telemetryStart,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
throw error;
|
|
429
811
|
}
|
|
430
|
-
|
|
431
|
-
return executeMiddleware<TEnv>(
|
|
432
|
-
allMiddleware,
|
|
433
|
-
req,
|
|
434
|
-
env,
|
|
435
|
-
variables,
|
|
436
|
-
finalHandler,
|
|
437
|
-
reverse,
|
|
438
|
-
);
|
|
439
812
|
});
|
|
440
813
|
}
|