@rangojs/router 0.0.0-experimental.b9cb8739 → 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 +303 -741
- package/dist/bin/rango.js +730 -184
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +4344 -1335
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +86 -15
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +251 -24
- package/skills/caching/SKILL.md +375 -17
- 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 +110 -4
- 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 +87 -56
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +73 -691
- 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 +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +94 -18
- package/skills/layout/SKILL.md +62 -19
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +302 -54
- package/skills/middleware/SKILL.md +59 -16
- package/skills/migrate-nextjs/SKILL.md +745 -0
- package/skills/migrate-react-router/SKILL.md +153 -0
- 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 +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +225 -10
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +178 -124
- package/skills/rango/SKILL.md +318 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +172 -9
- package/skills/router-setup/SKILL.md +131 -11
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +130 -0
- 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 -616
- 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 +74 -15
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +257 -158
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +132 -33
- package/src/browser/navigation-client.ts +218 -68
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +203 -80
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +241 -127
- package/src/browser/prefetch/cache.ts +271 -44
- package/src/browser/prefetch/fetch.ts +367 -40
- package/src/browser/prefetch/queue.ts +144 -23
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +121 -16
- package/src/browser/react/NavigationProvider.tsx +240 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +272 -80
- package/src/browser/scroll-restoration.ts +56 -22
- package/src/browser/segment-reconciler.ts +44 -7
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +244 -71
- package/src/browser/types.ts +136 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +207 -158
- package/src/build/generate-route-types.ts +6 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +198 -41
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +464 -63
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +336 -110
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +13 -21
- 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 -97
- package/src/cache/cache-scope.ts +235 -103
- 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 +2446 -170
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +144 -49
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +24 -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/taint.ts +55 -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 +41 -21
- package/src/client.tsx +116 -290
- 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 +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +104 -34
- 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 +9 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +100 -13
- package/src/index.ts +143 -19
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +215 -86
- 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 +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +485 -303
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +153 -77
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +48 -21
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +130 -29
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +89 -63
- package/src/router/loader-resolution.ts +286 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +105 -56
- package/src/router/match-api.ts +178 -218
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +66 -22
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +105 -50
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +128 -44
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +301 -177
- package/src/router/navigation-snapshot.ts +133 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +203 -58
- package/src/router/preview-match.ts +35 -103
- package/src/router/request-classification.ts +291 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +256 -0
- package/src/router/router-context.ts +11 -29
- package/src/router/router-interfaces.ts +146 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +301 -78
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +156 -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 +477 -385
- 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 +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +105 -20
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +210 -71
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +682 -508
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +138 -15
- 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 +99 -189
- package/src/rsc/rsc-rendering.ts +509 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +62 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +167 -0
- package/src/segment-system.tsx +449 -132
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +367 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +558 -173
- package/src/ssr/index.tsx +491 -174
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +234 -82
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +137 -14
- package/src/urls/include-helper.ts +40 -75
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +102 -23
- package/src/urls/path-helper.ts +62 -111
- package/src/urls/pattern-types.ts +84 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +15 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +234 -62
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +353 -303
- package/src/vite/router-discovery.ts +1090 -166
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +98 -38
- package/src/vite/utils/shared-utils.ts +144 -44
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/router/middleware.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/// <reference types="vite/types/importMeta.d.ts" />
|
|
2
|
-
/**
|
|
3
|
-
* Middleware Execution
|
|
4
|
-
*
|
|
5
|
-
* True middleware that wraps the entire RSC handler.
|
|
6
|
-
* - `await next()` returns actual Response
|
|
7
|
-
* - Can modify response headers
|
|
8
|
-
* - Can catch errors from RSC rendering
|
|
9
|
-
* - Forgiving API: if middleware doesn't return, original response is used
|
|
10
|
-
*/
|
|
11
2
|
|
|
12
3
|
import { contextGet, contextSet } from "../context-var.js";
|
|
4
|
+
import { escapeRegExp } from "../regex-escape.js";
|
|
5
|
+
import { parsePattern as parseRoutePattern } from "./pattern-matching.js";
|
|
6
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
7
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
8
|
import type {
|
|
14
9
|
CollectedMiddleware,
|
|
15
10
|
MiddlewareCollectableEntry,
|
|
@@ -19,26 +14,29 @@ import type {
|
|
|
19
14
|
ResponseHolder,
|
|
20
15
|
} from "./middleware-types.js";
|
|
21
16
|
import { _getRequestContext } from "../server/request-context.js";
|
|
17
|
+
import {
|
|
18
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
19
|
+
isExternalRedirect,
|
|
20
|
+
markExternalRedirect,
|
|
21
|
+
} from "../redirect-origin.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
23
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
|
+
import { observePhase, PHASES } from "./instrument.js";
|
|
25
|
+
import { stripInternalParams } from "./handler-context.js";
|
|
26
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
24
27
|
|
|
25
|
-
// Re-export types
|
|
28
|
+
// Re-export types consumed through this module's path.
|
|
26
29
|
export type {
|
|
27
30
|
CookieOptions,
|
|
28
|
-
CollectedMiddleware,
|
|
29
|
-
MiddlewareCollectableEntry,
|
|
30
31
|
MiddlewareContext,
|
|
31
32
|
MiddlewareEntry,
|
|
32
33
|
MiddlewareFn,
|
|
33
|
-
ResponseHolder,
|
|
34
34
|
} from "./middleware-types.js";
|
|
35
|
-
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
36
35
|
|
|
37
36
|
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
38
|
-
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
39
37
|
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
40
38
|
|
|
41
|
-
function
|
|
39
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
42
40
|
entry: MiddlewareEntry<TEnv>,
|
|
43
41
|
ordinal: number,
|
|
44
42
|
): string {
|
|
@@ -46,24 +44,49 @@ function getMiddlewareMetricBase<TEnv>(
|
|
|
46
44
|
const scope = entry.pattern ?? "*";
|
|
47
45
|
|
|
48
46
|
if (handlerName) {
|
|
49
|
-
return
|
|
47
|
+
return `middleware:${handlerName}@${scope}`;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getMiddlewareMetricLabel<TEnv>(
|
|
56
|
-
entry: MiddlewareEntry<TEnv>,
|
|
57
|
-
ordinal: number,
|
|
58
|
-
): string {
|
|
59
|
-
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
50
|
+
return `middleware:${scope}#${ordinal + 1}`;
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
54
|
+
* Compile a middleware scope pattern to a regex + param names.
|
|
55
|
+
*
|
|
56
|
+
* Middleware scopes reuse the route pattern parser (`parsePattern` from
|
|
57
|
+
* pattern-matching.ts), so they support the same param forms as routes —
|
|
58
|
+
* optional (`:x?`), constrained (`:x(en|gb)`), and suffix (`:x.html`) — in
|
|
59
|
+
* addition to the trailing-`*` wildcard middleware relies on. Before this
|
|
60
|
+
* unification the middleware-side parser handled only static, bare `:param`,
|
|
61
|
+
* and trailing `*`, so e.g. `router.use("/:locale(en|gb)/*", mw)` silently
|
|
62
|
+
* named the param "locale(en|gb)" and never enforced the constraint.
|
|
63
|
+
*
|
|
64
|
+
* Middleware matching semantics deliberately differ from route matching, so we
|
|
65
|
+
* emit the regex here rather than route through `compilePattern`:
|
|
66
|
+
* - `*` alone matches every path (`/^.*$/`).
|
|
67
|
+
* - A trailing `*` segment is an OPTIONAL subtree match (`(?:/.*)?`): `/admin/*`
|
|
68
|
+
* matches `/admin`, `/admin/`, and `/admin/users`. It contributes no param
|
|
69
|
+
* name (unlike route wildcards, which capture `*`).
|
|
70
|
+
* - A NON-trailing `*` is also OPTIONAL (`(?:/.*)?`), matching zero-or-more
|
|
71
|
+
* intermediate segments: `/a/<star>/b` matches both `/a/b` and `/a/x/b`. This
|
|
72
|
+
* mirrors the pre-unification parser, which compiled every `*` part as
|
|
73
|
+
* optional regardless of position.
|
|
74
|
+
* - A pattern without a trailing `*` tolerates a trailing slash (`/?$`).
|
|
75
|
+
* - Constraints are baked into the regex as an alternation so `matchMiddleware`
|
|
76
|
+
* (a bare `regex.test`) enforces them without extra validation. Constraint
|
|
77
|
+
* values are matched against the raw (still URL-encoded) path segment, which
|
|
78
|
+
* matches the pre-unification middleware behavior (it never decoded for
|
|
79
|
+
* matching); the constraint string is regex-escaped so values like `en.gb`
|
|
80
|
+
* are treated literally.
|
|
81
|
+
*
|
|
82
|
+
* The route segment parser only recognizes `/`-prefixed segments, but the
|
|
83
|
+
* pre-unification middleware parser split on `/` and dropped empty parts, so a
|
|
84
|
+
* leading slash was irrelevant: `use("admin/*")` and `use("/admin/*")` scoped
|
|
85
|
+
* identically. Normalize a non-`*` pattern to have a leading slash before
|
|
86
|
+
* parsing so that behavior is preserved (without it, `parseRoutePattern("admin/*")`
|
|
87
|
+
* drops the static `admin` and the scope explodes to every path).
|
|
65
88
|
*/
|
|
66
|
-
export function
|
|
89
|
+
export function compileMiddlewarePattern(pattern: string): {
|
|
67
90
|
regex: RegExp;
|
|
68
91
|
paramNames: string[];
|
|
69
92
|
} {
|
|
@@ -71,47 +94,63 @@ export function parsePattern(pattern: string): {
|
|
|
71
94
|
return { regex: /^.*$/, paramNames: [] };
|
|
72
95
|
}
|
|
73
96
|
|
|
97
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
98
|
+
const segments = parseRoutePattern(normalizedPattern);
|
|
74
99
|
const paramNames: string[] = [];
|
|
75
100
|
let regexStr = "^";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
let hasTrailingWildcard = false;
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < segments.length; i++) {
|
|
104
|
+
const segment = segments[i];
|
|
105
|
+
|
|
106
|
+
if (segment.type === "wildcard") {
|
|
107
|
+
if (segment.value === "*") {
|
|
108
|
+
// Bare `*`: optional subtree match, no capture (parity with the original
|
|
109
|
+
// middleware parser). A trailing `*` matches the subtree; a non-trailing
|
|
110
|
+
// `*` matches zero-or-more intermediate segments, so `/a/<star>/b` still
|
|
111
|
+
// matches `/a/b`.
|
|
112
|
+
regexStr += "(?:/.*)?";
|
|
113
|
+
} else {
|
|
114
|
+
// Named catch-all `:name+` / `:name*`: capture the remainder under the
|
|
115
|
+
// name so a scoping middleware sees `ctx.params.<name>`, and respect the
|
|
116
|
+
// one-or-more arity (`+` must not match the bare prefix), mirroring the
|
|
117
|
+
// route matcher instead of collapsing to the bare-`*` subtree.
|
|
118
|
+
paramNames.push(segment.value);
|
|
119
|
+
regexStr += segment.oneOrMore ? "/(.+)" : "(?:/(.*))?";
|
|
120
|
+
}
|
|
121
|
+
if (i === segments.length - 1) {
|
|
122
|
+
hasTrailingWildcard = true;
|
|
123
|
+
}
|
|
124
|
+
} else if (segment.type === "param") {
|
|
125
|
+
paramNames.push(segment.value);
|
|
126
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
127
|
+
const valuePattern = segment.constraint
|
|
128
|
+
? `(${segment.constraint.map(escapeRegExp).join("|")})`
|
|
129
|
+
: "([^/]+)";
|
|
130
|
+
if (segment.optional) {
|
|
131
|
+
regexStr += `(?:/${valuePattern}${suffixPattern})?`;
|
|
132
|
+
} else {
|
|
133
|
+
regexStr += `/${valuePattern}${suffixPattern}`;
|
|
134
|
+
}
|
|
90
135
|
} else {
|
|
91
|
-
//
|
|
92
|
-
regexStr += "/" +
|
|
136
|
+
// Static literal
|
|
137
|
+
regexStr += "/" + escapeRegExp(segment.value);
|
|
93
138
|
}
|
|
94
139
|
}
|
|
95
140
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
regexStr += "/?$";
|
|
99
|
-
} else {
|
|
100
|
-
regexStr += "$";
|
|
101
|
-
}
|
|
141
|
+
// Without a trailing `*`, match exactly with an optional trailing slash.
|
|
142
|
+
regexStr += hasTrailingWildcard ? "$" : "/?$";
|
|
102
143
|
|
|
103
144
|
return { regex: new RegExp(regexStr), paramNames };
|
|
104
145
|
}
|
|
105
146
|
|
|
106
147
|
/**
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
148
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
149
|
+
*
|
|
150
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
151
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
152
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
153
|
+
* Express/React Router/Fastify/Koa.
|
|
115
154
|
*/
|
|
116
155
|
export function extractParams(
|
|
117
156
|
pathname: string,
|
|
@@ -123,7 +162,7 @@ export function extractParams(
|
|
|
123
162
|
|
|
124
163
|
const params: Record<string, string> = {};
|
|
125
164
|
for (let i = 0; i < paramNames.length; i++) {
|
|
126
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
165
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
127
166
|
}
|
|
128
167
|
return params;
|
|
129
168
|
}
|
|
@@ -147,7 +186,15 @@ export function createMiddlewareContext<TEnv>(
|
|
|
147
186
|
search?: Record<string, unknown>,
|
|
148
187
|
) => string,
|
|
149
188
|
): MiddlewareContext<TEnv> {
|
|
150
|
-
|
|
189
|
+
// Reuse the request context's clean URL when available. Each middleware still
|
|
190
|
+
// gets its OWN URL clone (a middleware may mutate ctx.url / ctx.searchParams),
|
|
191
|
+
// but cloning the already-clean URL is cheaper than re-parsing + re-stripping
|
|
192
|
+
// request.url per middleware. Fall back to parsing when no ALS context.
|
|
193
|
+
const reqCtx = _getRequestContext();
|
|
194
|
+
const ctxUrl = reqCtx?.url;
|
|
195
|
+
const url = ctxUrl
|
|
196
|
+
? new URL(ctxUrl)
|
|
197
|
+
: stripInternalParams(new URL(request.url));
|
|
151
198
|
|
|
152
199
|
// Track the initial response to detect pre/post-next() phase.
|
|
153
200
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -178,14 +225,21 @@ export function createMiddlewareContext<TEnv>(
|
|
|
178
225
|
return responseHolder.response;
|
|
179
226
|
};
|
|
180
227
|
|
|
228
|
+
// reqCtx captured once above: the request-scoped platform fields
|
|
229
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
230
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
231
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
232
|
+
// during `await next()`.
|
|
181
233
|
return {
|
|
182
234
|
request,
|
|
183
235
|
url,
|
|
184
|
-
originalUrl: new URL(request.url),
|
|
236
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
185
237
|
pathname: url.pathname,
|
|
186
238
|
searchParams: url.searchParams,
|
|
187
239
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
188
240
|
params,
|
|
241
|
+
executionContext: reqCtx?.executionContext,
|
|
242
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
189
243
|
// Getter: re-derives from request context on each access so that global
|
|
190
244
|
// middleware sees the matched route name after await next().
|
|
191
245
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -203,12 +257,9 @@ export function createMiddlewareContext<TEnv>(
|
|
|
203
257
|
get: ((keyOrVar: any) =>
|
|
204
258
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
205
259
|
|
|
206
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
207
|
-
contextSet(variables, keyOrVar, value);
|
|
260
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
261
|
+
contextSet(variables, keyOrVar, value, options);
|
|
208
262
|
}) as MiddlewareContext<TEnv>["set"],
|
|
209
|
-
|
|
210
|
-
var: variables as MiddlewareContext<TEnv>["var"],
|
|
211
|
-
|
|
212
263
|
header(name: string, value: string): void {
|
|
213
264
|
// Before next(): delegate to shared RequestContext stub
|
|
214
265
|
if (isPreNext()) {
|
|
@@ -247,9 +298,13 @@ export function createMiddlewareContext<TEnv>(
|
|
|
247
298
|
|
|
248
299
|
reverse:
|
|
249
300
|
reverse ??
|
|
250
|
-
((
|
|
301
|
+
((
|
|
302
|
+
name: string,
|
|
303
|
+
_params?: Record<string, string>,
|
|
304
|
+
_search?: Record<string, unknown>,
|
|
305
|
+
) => {
|
|
251
306
|
throw new Error(
|
|
252
|
-
`ctx.reverse() is not available
|
|
307
|
+
`ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
|
|
253
308
|
);
|
|
254
309
|
}),
|
|
255
310
|
|
|
@@ -257,7 +312,10 @@ export function createMiddlewareContext<TEnv>(
|
|
|
257
312
|
const reqCtx = _getRequestContext();
|
|
258
313
|
if (reqCtx) {
|
|
259
314
|
reqCtx._debugPerformance = true;
|
|
260
|
-
reqCtx.
|
|
315
|
+
// Anchor to the true request entry (reqCtx._handlerStart) so phases that
|
|
316
|
+
// began before this opt-in report non-negative offsets; undefined falls
|
|
317
|
+
// back to performance.now() in createMetricsStore.
|
|
318
|
+
reqCtx._metricsStore ??= createMetricsStore(true, reqCtx._handlerStart);
|
|
261
319
|
}
|
|
262
320
|
},
|
|
263
321
|
};
|
|
@@ -283,9 +341,16 @@ export function matchMiddleware<TEnv>(
|
|
|
283
341
|
continue;
|
|
284
342
|
}
|
|
285
343
|
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
344
|
+
// Run the scope regex ONCE per entry. The old code ran test() then, on a
|
|
345
|
+
// hit, extractParams' match() — a second full pass over the same string for
|
|
346
|
+
// every matching entry on the per-request hot path. The regexes carry no
|
|
347
|
+
// `g` flag, so there is no lastIndex statefulness across this single match.
|
|
348
|
+
const m = pathname.match(entry.regex);
|
|
349
|
+
if (m) {
|
|
350
|
+
const params: Record<string, string> = {};
|
|
351
|
+
for (let i = 0; i < entry.paramNames.length; i++) {
|
|
352
|
+
params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
|
|
353
|
+
}
|
|
289
354
|
matches.push({ entry, params });
|
|
290
355
|
}
|
|
291
356
|
}
|
|
@@ -293,6 +358,88 @@ export function matchMiddleware<TEnv>(
|
|
|
293
358
|
return matches;
|
|
294
359
|
}
|
|
295
360
|
|
|
361
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
362
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
363
|
+
// explicit short-circuit Response's own headers win).
|
|
364
|
+
function mergeStubHeaders(
|
|
365
|
+
target: Headers,
|
|
366
|
+
stub: Headers,
|
|
367
|
+
stubOverridesNonCookie: boolean,
|
|
368
|
+
): void {
|
|
369
|
+
stub.forEach((value, name) => {
|
|
370
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
371
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
372
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
373
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
374
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
375
|
+
target.append(name, value);
|
|
376
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
377
|
+
target.set(name, value);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
383
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
384
|
+
function mergeReqCtxStub(
|
|
385
|
+
target: Headers,
|
|
386
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
387
|
+
): void {
|
|
388
|
+
if (!reqCtx) return;
|
|
389
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
390
|
+
if (stubCookies.length > 0) {
|
|
391
|
+
const existing = new Set(target.getSetCookie());
|
|
392
|
+
for (const cookie of stubCookies) {
|
|
393
|
+
if (!existing.has(cookie)) {
|
|
394
|
+
target.append("set-cookie", cookie);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
399
|
+
// Never propagate the reserved external-redirect marker (see mergeStubHeaders).
|
|
400
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
401
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
402
|
+
target.set(name, value);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Clone `base` with stub headers merged into a fresh Headers (the clone keeps
|
|
408
|
+
// the body mutable for post-next() modifications). Set-Cookie is always
|
|
409
|
+
// appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
|
|
410
|
+
// mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
|
|
411
|
+
// short-circuit path passes false (its reqCtx headers are not merged here),
|
|
412
|
+
// which is the one deliberate divergence between the call sites.
|
|
413
|
+
function mergeResponse(
|
|
414
|
+
base: Response,
|
|
415
|
+
stub: Headers,
|
|
416
|
+
opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
|
|
417
|
+
): Response {
|
|
418
|
+
const mergedHeaders = new Headers(base.headers);
|
|
419
|
+
// The reserved external-redirect marker is never a trust signal and must never
|
|
420
|
+
// reach the browser. The guard strips it on 3xx redirects; strip it here too so
|
|
421
|
+
// a forged value cannot ride a non-3xx middleware response (which the 3xx-only
|
|
422
|
+
// guard would not touch) to the client. The opt-in is the out-of-band brand.
|
|
423
|
+
mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
|
|
424
|
+
mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
|
|
425
|
+
if (opts.mergeReqCtx) {
|
|
426
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
427
|
+
}
|
|
428
|
+
const merged = new Response(base.body, {
|
|
429
|
+
status: base.status,
|
|
430
|
+
statusText: base.statusText,
|
|
431
|
+
headers: mergedHeaders,
|
|
432
|
+
});
|
|
433
|
+
// Transfer the out-of-band external-redirect brand across this rebuild: a
|
|
434
|
+
// middleware short-circuit `return redirect(url, { external: true })` reaches
|
|
435
|
+
// the open-redirect guard only after this merge, and the brand lives on the
|
|
436
|
+
// Response object, not in its headers.
|
|
437
|
+
if (isExternalRedirect(base)) {
|
|
438
|
+
markExternalRedirect(merged);
|
|
439
|
+
}
|
|
440
|
+
return merged;
|
|
441
|
+
}
|
|
442
|
+
|
|
296
443
|
/**
|
|
297
444
|
* Execute middleware chain
|
|
298
445
|
*
|
|
@@ -301,7 +448,7 @@ export function matchMiddleware<TEnv>(
|
|
|
301
448
|
* - `ctx.headers` available before and after `await next()`
|
|
302
449
|
* - `ctx.header()` shorthand for setting a single header
|
|
303
450
|
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
304
|
-
* - Short-circuit: return Response to stop chain
|
|
451
|
+
* - Short-circuit: return OR throw a Response to stop chain
|
|
305
452
|
* - Error catching: try/catch around `next()` works
|
|
306
453
|
*/
|
|
307
454
|
export async function executeMiddleware<TEnv>(
|
|
@@ -331,42 +478,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
331
478
|
// End of chain - call actual RSC handler
|
|
332
479
|
const response = await finalHandler();
|
|
333
480
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
stubResponse.headers.forEach((value, name) => {
|
|
338
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
339
|
-
mergedHeaders.append(name, value);
|
|
340
|
-
} else {
|
|
341
|
-
mergedHeaders.set(name, value);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
345
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
346
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
347
|
-
const reqCtx = _getRequestContext();
|
|
348
|
-
if (reqCtx) {
|
|
349
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
350
|
-
if (stubCookies.length > 0) {
|
|
351
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
352
|
-
for (const cookie of stubCookies) {
|
|
353
|
-
if (!existing.has(cookie)) {
|
|
354
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
359
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
360
|
-
mergedHeaders.set(name, value);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
481
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
482
|
+
responseHolder.response = response;
|
|
483
|
+
return response;
|
|
363
484
|
}
|
|
364
485
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
486
|
+
// Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
|
|
487
|
+
// and reqCtx stub headers are merged in.
|
|
488
|
+
responseHolder.response = mergeResponse(response, stubResponse.headers, {
|
|
489
|
+
stubOverridesNonCookie: true,
|
|
490
|
+
mergeReqCtx: true,
|
|
370
491
|
});
|
|
371
492
|
|
|
372
493
|
return responseHolder.response;
|
|
@@ -424,14 +545,36 @@ export async function executeMiddleware<TEnv>(
|
|
|
424
545
|
return nextPromise;
|
|
425
546
|
};
|
|
426
547
|
|
|
548
|
+
// Wrap the middleware (including its downstream next() chain) in its span
|
|
549
|
+
// via the unified phase API. metric:false — the middleware's perf metric is
|
|
550
|
+
// its exclusive pre/post own-time, recorded directly above and below, finer
|
|
551
|
+
// than a single wrap. Spans nest by async context, so this onions
|
|
552
|
+
// middleware-over-middleware and the core handler underneath. Pass-through
|
|
553
|
+
// when neither surface is active.
|
|
427
554
|
let result: Response | void;
|
|
428
555
|
try {
|
|
429
|
-
result = await
|
|
430
|
-
|
|
556
|
+
result = await observePhase(PHASES.middleware(metricLabel), async () => {
|
|
557
|
+
try {
|
|
558
|
+
return await entry.handler(ctx, wrappedNext);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Thrown Response is short-circuit control flow, not an error —
|
|
561
|
+
// absorb it INSIDE the span so the tracing runner settles the
|
|
562
|
+
// rango.middleware span as success, not STATUS_ERROR (every auth
|
|
563
|
+
// redirect would otherwise inflate trace error rates). Returning it
|
|
564
|
+
// routes through the `if (result instanceof Response)` branch below,
|
|
565
|
+
// so stub headers and request-context cookies merge identically to an
|
|
566
|
+
// explicit `return new Response(...)`. Segment handlers already follow
|
|
567
|
+
// this convention (segment-resolution/helpers.ts keeps result handling
|
|
568
|
+
// outside the span). Real errors propagate past the span.
|
|
569
|
+
if (error instanceof Response) return error;
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
// Settle the middleware own-time metric once on both the success and
|
|
575
|
+
// error paths (idempotent guard in finishMiddleware).
|
|
431
576
|
finishMiddleware();
|
|
432
|
-
throw error;
|
|
433
577
|
}
|
|
434
|
-
finishMiddleware();
|
|
435
578
|
|
|
436
579
|
// Record post-next() processing time when middleware did work after
|
|
437
580
|
// the downstream chain resolved (e.g. adding headers, logging).
|
|
@@ -450,41 +593,18 @@ export async function executeMiddleware<TEnv>(
|
|
|
450
593
|
|
|
451
594
|
// Explicit return takes precedence (middleware short-circuit).
|
|
452
595
|
// Merge stub headers (from ctx.header before this point) and
|
|
453
|
-
// RequestContext stub headers (from
|
|
596
|
+
// RequestContext stub headers (from cookies().set()) into the
|
|
454
597
|
// returned Response so they are not lost.
|
|
455
598
|
if (result instanceof Response) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
mergedHeaders.append(name, value);
|
|
460
|
-
} else if (!mergedHeaders.has(name)) {
|
|
461
|
-
mergedHeaders.set(name, value);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
465
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
466
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
467
|
-
const reqCtx = _getRequestContext();
|
|
468
|
-
if (reqCtx) {
|
|
469
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
470
|
-
if (stubCookies.length > 0) {
|
|
471
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
472
|
-
for (const cookie of stubCookies) {
|
|
473
|
-
if (!existing.has(cookie)) {
|
|
474
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
479
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
480
|
-
mergedHeaders.set(name, value);
|
|
481
|
-
}
|
|
482
|
-
});
|
|
599
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
600
|
+
responseHolder.response = result;
|
|
601
|
+
return result;
|
|
483
602
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
603
|
+
// Explicit short-circuit: the returned Response's own headers win
|
|
604
|
+
// (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
|
|
605
|
+
const merged = mergeResponse(result, stubResponse.headers, {
|
|
606
|
+
stubOverridesNonCookie: false,
|
|
607
|
+
mergeReqCtx: true,
|
|
488
608
|
});
|
|
489
609
|
responseHolder.response = merged;
|
|
490
610
|
return merged;
|
|
@@ -529,23 +649,12 @@ export async function executeMiddleware<TEnv>(
|
|
|
529
649
|
// last merge point (e.g. cookies().set() called after await next()).
|
|
530
650
|
// The reqCtx stub may have already been partially merged during finalHandler
|
|
531
651
|
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
652
|
+
//
|
|
653
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
654
|
+
// set-cookie on an upgrade is not meaningful.
|
|
532
655
|
const reqCtx = _getRequestContext();
|
|
533
|
-
if (reqCtx) {
|
|
534
|
-
|
|
535
|
-
if (stubCookies.length > 0) {
|
|
536
|
-
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
537
|
-
for (const cookie of stubCookies) {
|
|
538
|
-
if (!existingCookies.has(cookie)) {
|
|
539
|
-
finalResponse.headers.append("set-cookie", cookie);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
// Fill in non-cookie headers that aren't already on the response
|
|
544
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
545
|
-
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
546
|
-
finalResponse.headers.set(name, value);
|
|
547
|
-
}
|
|
548
|
-
});
|
|
656
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
657
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
549
658
|
}
|
|
550
659
|
|
|
551
660
|
return finalResponse;
|
|
@@ -556,7 +665,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
556
665
|
*
|
|
557
666
|
* Intercepts use a shared stubResponse from the request context. This function:
|
|
558
667
|
* - Runs middleware in sequence with a simple next() chain
|
|
559
|
-
* - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
|
|
668
|
+
* - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
|
|
560
669
|
* - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
|
|
561
670
|
*
|
|
562
671
|
* @param middlewares - Array of middleware functions
|
|
@@ -594,6 +703,7 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
594
703
|
return stubResponse;
|
|
595
704
|
}
|
|
596
705
|
|
|
706
|
+
const ordinal = index;
|
|
597
707
|
const middleware = middlewares[index++];
|
|
598
708
|
const ctx = createMiddlewareContext(
|
|
599
709
|
request,
|
|
@@ -615,7 +725,30 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
615
725
|
return next();
|
|
616
726
|
};
|
|
617
727
|
|
|
618
|
-
|
|
728
|
+
// Span-wrap each intercept middleware as rango.middleware (metric:false —
|
|
729
|
+
// intercept runs inside the render phase already metered by render:total, so
|
|
730
|
+
// it contributes a span but no separate perf metric). Bare MiddlewareFns
|
|
731
|
+
// have no pattern, so the label is scoped to "*" like a pattern-less entry.
|
|
732
|
+
const label = getMiddlewareMetricLabel(
|
|
733
|
+
{ handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
|
|
734
|
+
ordinal,
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const result: Response | void = await observePhase(
|
|
738
|
+
PHASES.middleware(label),
|
|
739
|
+
async () => {
|
|
740
|
+
try {
|
|
741
|
+
return await middleware(ctx, guardedNext);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
744
|
+
// explicit-return path below. Absorb it INSIDE the span so the tracing
|
|
745
|
+
// runner settles rango.middleware as success, not STATUS_ERROR (same
|
|
746
|
+
// reasoning as executeMiddleware's main chain). Real errors propagate.
|
|
747
|
+
if (error instanceof Response) return error;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
);
|
|
619
752
|
|
|
620
753
|
if (result instanceof Response) {
|
|
621
754
|
earlyResponse = result;
|
|
@@ -639,21 +772,13 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
639
772
|
});
|
|
640
773
|
|
|
641
774
|
if (hasStubHeaders) {
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
stubResponse.headers
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
} else if (!mergedHeaders.has(name)) {
|
|
650
|
-
mergedHeaders.set(name, value);
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
return new Response(response.body, {
|
|
654
|
-
status: response.status,
|
|
655
|
-
statusText: response.statusText,
|
|
656
|
-
headers: mergedHeaders,
|
|
775
|
+
// Only fill in missing headers — the returned Response's explicit headers
|
|
776
|
+
// take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
|
|
777
|
+
// mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
|
|
778
|
+
// stub headers here (pinned by intercept-middleware-headers.test.ts).
|
|
779
|
+
return mergeResponse(response, stubResponse.headers, {
|
|
780
|
+
stubOverridesNonCookie: false,
|
|
781
|
+
mergeReqCtx: false,
|
|
657
782
|
});
|
|
658
783
|
}
|
|
659
784
|
return response;
|
|
@@ -694,7 +819,6 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
694
819
|
regex: null,
|
|
695
820
|
paramNames: [],
|
|
696
821
|
handler,
|
|
697
|
-
mountPrefix: null,
|
|
698
822
|
} as MiddlewareEntry<TEnv>,
|
|
699
823
|
params,
|
|
700
824
|
}));
|