@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -0
- package/README.md +432 -7
- package/dist/bin/rango.js +2073 -213
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +7258 -2714
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +140 -67
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +329 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +487 -0
- package/skills/caching/SKILL.md +357 -25
- 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 +246 -0
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +16 -10
- package/skills/document-cache/SKILL.md +87 -62
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +557 -79
- package/skills/host-router/SKILL.md +320 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +207 -15
- package/skills/layout/SKILL.md +146 -6
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +616 -54
- package/skills/middleware/SKILL.md +217 -37
- package/skills/migrate-nextjs/SKILL.md +611 -0
- package/skills/migrate-react-router/SKILL.md +927 -0
- package/skills/mime-routes/SKILL.md +42 -11
- package/skills/observability/SKILL.md +194 -0
- package/skills/parallel/SKILL.md +284 -3
- package/skills/ppr/SKILL.md +426 -0
- package/skills/prerender/SKILL.md +437 -52
- package/skills/rango/SKILL.md +369 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +263 -121
- package/skills/route/SKILL.md +350 -21
- package/skills/router-setup/SKILL.md +246 -33
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +126 -222
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +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 +9 -8
- package/skills/typesafety/SKILL.md +532 -103
- package/skills/use-cache/SKILL.md +367 -0
- 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 +77 -44
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +114 -0
- 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 +293 -202
- package/src/browser/history-state.ts +101 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +385 -576
- package/src/browser/navigation-client.ts +245 -75
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +184 -118
- package/src/browser/navigation-transaction.ts +247 -0
- package/src/browser/network-error-handler.ts +88 -0
- package/src/browser/partial-update.ts +412 -364
- package/src/browser/prefetch/cache.ts +359 -0
- package/src/browser/prefetch/fetch.ts +452 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +209 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +194 -0
- package/src/browser/react/Link.tsx +275 -68
- package/src/browser/react/NavigationProvider.tsx +265 -109
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +70 -0
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +272 -60
- package/src/browser/react/location-state.ts +90 -20
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +35 -66
- package/src/browser/react/use-handle.ts +39 -126
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +39 -13
- package/src/browser/react/use-navigation.ts +53 -69
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +98 -0
- package/src/browser/react/use-search-params.ts +51 -0
- package/src/browser/react/use-segments.ts +72 -99
- package/src/browser/response-adapter.ts +164 -0
- package/src/browser/rsc-router.tsx +300 -72
- package/src/browser/scroll-restoration.ts +138 -50
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +17 -1
- package/src/browser/server-action-bridge.ts +668 -613
- package/src/browser/types.ts +223 -51
- package/src/browser/validate-redirect-origin.ts +56 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +252 -161
- package/src/build/generate-route-types.ts +41 -1038
- package/src/build/index.ts +12 -7
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +225 -42
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +105 -0
- package/src/build/route-types/codegen.ts +113 -0
- package/src/build/route-types/include-resolution.ts +812 -0
- package/src/build/route-types/param-extraction.ts +51 -0
- package/src/build/route-types/per-module-writer.ts +144 -0
- package/src/build/route-types/router-processing.ts +695 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +223 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +60 -0
- package/src/cache/cache-policy.ts +199 -0
- package/src/cache/cache-runtime.ts +525 -0
- package/src/cache/cache-scope.ts +298 -332
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2500 -158
- 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 +17 -17
- package/src/cache/document-cache.ts +199 -92
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +111 -0
- package/src/cache/index.ts +29 -35
- package/src/cache/memory-segment-store.ts +363 -30
- package/src/cache/profile-registry.ts +88 -0
- package/src/cache/read-through-swr.ts +178 -0
- package/src/cache/segment-codec.ts +248 -0
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +156 -211
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1102 -0
- package/src/client.rsc.tsx +43 -21
- package/src/client.tsx +131 -347
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +23 -4
- package/src/components/DefaultDocument.tsx +13 -3
- package/src/context-var.ts +168 -0
- package/src/debug.ts +19 -9
- 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 +106 -10
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +110 -35
- package/src/handles/MetaTags.tsx +83 -59
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +93 -0
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +44 -53
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +20 -65
- package/src/host/errors.ts +21 -30
- package/src/host/index.ts +13 -9
- package/src/host/pattern-matcher.ts +50 -79
- package/src/host/router.ts +151 -121
- package/src/host/testing.ts +45 -32
- package/src/host/types.ts +52 -11
- package/src/host/utils.ts +2 -2
- package/src/href-client.ts +192 -57
- package/src/index.rsc.ts +177 -35
- package/src/index.ts +255 -71
- package/src/internal-debug.ts +9 -2
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +31 -99
- package/src/loader.ts +30 -12
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +41 -0
- package/src/prerender/param-hash.ts +16 -14
- package/src/prerender/store.ts +121 -21
- package/src/prerender.ts +460 -26
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +198 -128
- package/src/root-error-boundary.tsx +42 -48
- package/src/route-content-wrapper.tsx +22 -77
- package/src/route-definition/dsl-helpers.ts +1116 -0
- package/src/route-definition/helper-factories.ts +88 -0
- package/src/route-definition/helpers-types.ts +505 -0
- package/src/route-definition/index.ts +54 -0
- package/src/route-definition/redirect.ts +134 -0
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-definition.ts +1 -1481
- package/src/route-map-builder.ts +82 -144
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +71 -45
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +263 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +54 -27
- package/src/router/find-match.ts +245 -0
- package/src/router/handler-context.ts +377 -125
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +59 -28
- package/src/router/lazy-includes.ts +254 -0
- package/src/router/loader-resolution.ts +421 -157
- package/src/router/logging.ts +106 -6
- package/src/router/manifest.ts +131 -57
- package/src/router/match-api.ts +167 -246
- package/src/router/match-context.ts +4 -24
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +117 -93
- package/src/router/match-middleware/cache-lookup.ts +297 -150
- package/src/router/match-middleware/cache-store.ts +123 -51
- package/src/router/match-middleware/intercept-resolution.ts +44 -43
- package/src/router/match-middleware/segment-resolution.ts +64 -22
- package/src/router/match-pipelines.ts +11 -87
- package/src/router/match-result.ts +121 -50
- package/src/router/metrics.ts +219 -28
- package/src/router/middleware-types.ts +93 -0
- package/src/router/middleware.ts +505 -441
- 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 +311 -142
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +547 -0
- package/src/router/preview-match.ts +102 -0
- package/src/router/request-classification.ts +278 -0
- package/src/router/revalidation.ts +203 -62
- package/src/router/route-snapshot.ts +246 -0
- package/src/router/router-context.ts +45 -48
- package/src/router/router-interfaces.ts +554 -0
- package/src/router/router-options.ts +779 -0
- package/src/router/router-registry.ts +21 -0
- package/src/router/segment-resolution/fresh.ts +772 -0
- package/src/router/segment-resolution/helpers.ts +348 -0
- package/src/router/segment-resolution/loader-cache.ts +250 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/segment-resolution/revalidation.ts +1331 -0
- package/src/router/segment-resolution/static-store.ts +81 -0
- 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 +25 -1354
- package/src/router/segment-wrappers.ts +292 -0
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +261 -0
- package/src/router/telemetry.ts +377 -0
- package/src/router/timeout.ts +128 -0
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +240 -61
- package/src/router/types.ts +23 -70
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +781 -2378
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +46 -0
- package/src/rsc/handler.ts +905 -1142
- package/src/rsc/helpers.ts +275 -19
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +305 -0
- package/src/rsc/manifest-init.ts +77 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +155 -0
- package/src/rsc/progressive-enhancement.ts +502 -0
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +104 -0
- package/src/rsc/response-route-handler.ts +257 -0
- package/src/rsc/rsc-rendering.ts +337 -0
- package/src/rsc/runtime-warnings.ts +55 -0
- package/src/rsc/server-action.ts +522 -0
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +95 -12
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +99 -82
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +349 -134
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +452 -85
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +310 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +123 -42
- package/src/server/live.ts +130 -0
- package/src/server/loader-registry.ts +51 -100
- package/src/server/request-context.ts +842 -157
- package/src/server.ts +15 -8
- package/src/ssr/index.tsx +412 -136
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +45 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +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 +40 -72
- package/src/theme/ThemeScript.tsx +12 -14
- package/src/theme/constants.ts +57 -15
- package/src/theme/index.ts +3 -20
- package/src/theme/theme-context.ts +5 -35
- package/src/theme/theme-script.ts +43 -39
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +123 -0
- package/src/types/cache-types.ts +207 -0
- package/src/types/error-types.ts +132 -0
- package/src/types/global-namespace.ts +113 -0
- package/src/types/handler-context.ts +839 -0
- package/src/types/index.ts +81 -0
- package/src/types/loader-types.ts +212 -0
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +138 -0
- package/src/types/route-entry.ts +114 -0
- package/src/types/segments.ts +271 -0
- package/src/types.ts +1 -1795
- package/src/urls/include-helper.ts +162 -0
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +43 -0
- package/src/urls/path-helper-types.ts +413 -0
- package/src/urls/path-helper.ts +275 -0
- package/src/urls/pattern-types.ts +124 -0
- package/src/urls/response-types.ts +109 -0
- package/src/urls/type-extraction.ts +316 -0
- package/src/urls/urls-function.ts +80 -0
- package/src/urls.ts +1 -1341
- package/src/use-loader.tsx +406 -141
- 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 +182 -0
- package/src/vite/discovery/discover-routers.ts +389 -0
- 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 +467 -0
- package/src/vite/discovery/route-types-writer.ts +214 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +161 -0
- package/src/vite/discovery/virtual-module-codegen.ts +183 -0
- package/src/vite/index.ts +23 -2255
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +303 -0
- package/src/vite/plugins/cjs-to-esm.ts +90 -0
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +118 -0
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
- package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +805 -0
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +313 -0
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +94 -0
- package/src/vite/plugins/version-plugin.ts +263 -0
- package/src/vite/plugins/virtual-entries.ts +234 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +560 -0
- package/src/vite/router-discovery.ts +1638 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +132 -0
- 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 +15 -0
- package/src/vite/utils/package-resolution.ts +89 -0
- package/src/vite/utils/prerender-utils.ts +249 -0
- package/src/vite/utils/shared-utils.ts +269 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/react/use-client-cache.ts +0 -56
- package/src/browser/request-controller.ts +0 -164
- package/src/browser/shallow.ts +0 -35
- package/src/cache/memory-store.ts +0 -253
- package/src/handles/index.ts +0 -6
- package/src/href-context.ts +0 -33
- package/src/network-error-thrower.tsx +0 -21
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- package/src/vite/package-resolution.ts +0 -125
- package/src/vite/virtual-entries.ts +0 -114
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/router/middleware.ts
CHANGED
|
@@ -1,184 +1,92 @@
|
|
|
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
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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";
|
|
8
|
+
import type {
|
|
9
|
+
CollectedMiddleware,
|
|
10
|
+
MiddlewareCollectableEntry,
|
|
11
|
+
MiddlewareContext,
|
|
12
|
+
MiddlewareEntry,
|
|
13
|
+
MiddlewareFn,
|
|
14
|
+
ResponseHolder,
|
|
15
|
+
} from "./middleware-types.js";
|
|
16
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
17
|
+
import {
|
|
18
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
19
|
+
isExternalRedirect,
|
|
20
|
+
markExternalRedirect,
|
|
21
|
+
} from "../redirect-origin.js";
|
|
22
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
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";
|
|
27
|
+
|
|
28
|
+
// Re-export types consumed through this module's path.
|
|
29
|
+
export type {
|
|
30
|
+
CookieOptions,
|
|
31
|
+
MiddlewareContext,
|
|
32
|
+
MiddlewareEntry,
|
|
33
|
+
MiddlewareFn,
|
|
34
|
+
} from "./middleware-types.js";
|
|
35
|
+
|
|
36
|
+
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
37
|
+
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
38
|
+
|
|
39
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
40
|
+
entry: MiddlewareEntry<TEnv>,
|
|
41
|
+
ordinal: number,
|
|
42
|
+
): string {
|
|
43
|
+
const handlerName = entry.handler.name?.trim();
|
|
44
|
+
const scope = entry.pattern ?? "*";
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export interface CookieOptions {
|
|
43
|
-
domain?: string;
|
|
44
|
-
path?: string;
|
|
45
|
-
maxAge?: number;
|
|
46
|
-
expires?: Date;
|
|
47
|
-
httpOnly?: boolean;
|
|
48
|
-
secure?: boolean;
|
|
49
|
-
sameSite?: "strict" | "lax" | "none";
|
|
50
|
-
}
|
|
46
|
+
if (handlerName) {
|
|
47
|
+
return `middleware:${handlerName}@${scope}`;
|
|
48
|
+
}
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
* Context passed to middleware
|
|
54
|
-
*
|
|
55
|
-
* @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
|
|
56
|
-
* @template TParams - URL params type (typed for route middleware, Record<string, string> for global middleware)
|
|
57
|
-
*/
|
|
58
|
-
export interface MiddlewareContext<
|
|
59
|
-
TEnv = any,
|
|
60
|
-
TParams = Record<string, string>,
|
|
61
|
-
> {
|
|
62
|
-
/** Original request */
|
|
63
|
-
request: Request;
|
|
64
|
-
|
|
65
|
-
/** Parsed URL */
|
|
66
|
-
url: URL;
|
|
67
|
-
|
|
68
|
-
/** URL pathname */
|
|
69
|
-
pathname: string;
|
|
70
|
-
|
|
71
|
-
/** URL search params */
|
|
72
|
-
searchParams: URLSearchParams;
|
|
73
|
-
|
|
74
|
-
/** Platform bindings (Cloudflare, etc.) */
|
|
75
|
-
env: TEnv extends RouterEnv<infer B, unknown> ? B : {};
|
|
76
|
-
|
|
77
|
-
/** URL params extracted from route/middleware pattern */
|
|
78
|
-
params: TParams;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Response object - available immediately via stub, real response after `await next()`
|
|
82
|
-
*
|
|
83
|
-
* Headers set before `next()` are merged into the final response.
|
|
84
|
-
* Can be used to modify headers directly like Hono's `c.res`.
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* middleware(async (ctx, next) => {
|
|
89
|
-
* // Set headers BEFORE next() - will be merged into final response
|
|
90
|
-
* ctx.res.headers.set('X-Request-Id', generateId());
|
|
91
|
-
*
|
|
92
|
-
* await next();
|
|
93
|
-
*
|
|
94
|
-
* // Set headers AFTER next() - applied directly
|
|
95
|
-
* ctx.res.headers.set('X-Custom', 'value');
|
|
96
|
-
* // No return needed!
|
|
97
|
-
* });
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
res: Response;
|
|
101
|
-
|
|
102
|
-
/** Get a cookie value */
|
|
103
|
-
cookie(name: string): string | undefined;
|
|
104
|
-
|
|
105
|
-
/** Get all cookies as object */
|
|
106
|
-
cookies(): Record<string, string>;
|
|
107
|
-
|
|
108
|
-
/** Set a cookie on the response */
|
|
109
|
-
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
110
|
-
|
|
111
|
-
/** Delete a cookie */
|
|
112
|
-
deleteCookie(
|
|
113
|
-
name: string,
|
|
114
|
-
options?: Pick<CookieOptions, "domain" | "path">
|
|
115
|
-
): void;
|
|
116
|
-
|
|
117
|
-
/** Get a context variable (shared with route handlers) */
|
|
118
|
-
get: GetVariableFn<TEnv>;
|
|
119
|
-
|
|
120
|
-
/** Set a context variable (shared with route handlers) */
|
|
121
|
-
set: SetVariableFn<TEnv>;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Set a response header - can be called before or after `next()`
|
|
125
|
-
*
|
|
126
|
-
* When called before `next()`, headers are queued and merged into the final response.
|
|
127
|
-
* When called after `next()`, headers are set directly on the response.
|
|
128
|
-
* Shorthand for `ctx.res.headers.set()`.
|
|
129
|
-
*/
|
|
130
|
-
header(name: string, value: string): void;
|
|
50
|
+
return `middleware:${scope}#${ordinal + 1}`;
|
|
131
51
|
}
|
|
132
52
|
|
|
133
53
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* @template TEnv - Environment type - defaults to any for internal flexibility
|
|
137
|
-
* @template TParams - URL params type (typed for route middleware)
|
|
54
|
+
* Compile a middleware scope pattern to a regex + param names.
|
|
138
55
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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.
|
|
141
63
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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.
|
|
146
81
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
next: () => Promise<Response>
|
|
154
|
-
) => Response | void | Promise<Response | void>;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Stored middleware entry with pattern matching info
|
|
158
|
-
* @internal - uses any for internal flexibility
|
|
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).
|
|
159
88
|
*/
|
|
160
|
-
export
|
|
161
|
-
/** Original pattern string */
|
|
162
|
-
pattern: string | null;
|
|
163
|
-
|
|
164
|
-
/** Compiled regex for matching */
|
|
165
|
-
regex: RegExp | null;
|
|
166
|
-
|
|
167
|
-
/** Param names extracted from pattern */
|
|
168
|
-
paramNames: string[];
|
|
169
|
-
|
|
170
|
-
/** The middleware function */
|
|
171
|
-
handler: MiddlewareFn<TEnv>;
|
|
172
|
-
|
|
173
|
-
/** Mount prefix this middleware is scoped to (null = global) */
|
|
174
|
-
mountPrefix: string | null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Parse a route pattern into regex and param names
|
|
179
|
-
* Supports: *, /path, /path/*, /path/:param, /path/:param/*
|
|
180
|
-
*/
|
|
181
|
-
export function parsePattern(pattern: string): {
|
|
89
|
+
export function compileMiddlewarePattern(pattern: string): {
|
|
182
90
|
regex: RegExp;
|
|
183
91
|
paramNames: string[];
|
|
184
92
|
} {
|
|
@@ -186,208 +94,219 @@ export function parsePattern(pattern: string): {
|
|
|
186
94
|
return { regex: /^.*$/, paramNames: [] };
|
|
187
95
|
}
|
|
188
96
|
|
|
97
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
98
|
+
const segments = parseRoutePattern(normalizedPattern);
|
|
189
99
|
const paramNames: string[] = [];
|
|
190
100
|
let regexStr = "^";
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
205
135
|
} else {
|
|
206
|
-
//
|
|
207
|
-
regexStr += "/" +
|
|
136
|
+
// Static literal
|
|
137
|
+
regexStr += "/" + escapeRegExp(segment.value);
|
|
208
138
|
}
|
|
209
139
|
}
|
|
210
140
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
regexStr += "/?$";
|
|
214
|
-
} else {
|
|
215
|
-
regexStr += "$";
|
|
216
|
-
}
|
|
141
|
+
// Without a trailing `*`, match exactly with an optional trailing slash.
|
|
142
|
+
regexStr += hasTrailingWildcard ? "$" : "/?$";
|
|
217
143
|
|
|
218
144
|
return { regex: new RegExp(regexStr), paramNames };
|
|
219
145
|
}
|
|
220
146
|
|
|
221
147
|
/**
|
|
222
|
-
*
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* 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.
|
|
230
154
|
*/
|
|
231
155
|
export function extractParams(
|
|
232
156
|
pathname: string,
|
|
233
157
|
regex: RegExp,
|
|
234
|
-
paramNames: string[]
|
|
158
|
+
paramNames: string[],
|
|
235
159
|
): Record<string, string> {
|
|
236
160
|
const match = pathname.match(regex);
|
|
237
161
|
if (!match) return {};
|
|
238
162
|
|
|
239
163
|
const params: Record<string, string> = {};
|
|
240
164
|
for (let i = 0; i < paramNames.length; i++) {
|
|
241
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
165
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
242
166
|
}
|
|
243
167
|
return params;
|
|
244
168
|
}
|
|
245
169
|
|
|
246
|
-
/**
|
|
247
|
-
* Parse cookies from Cookie header
|
|
248
|
-
*/
|
|
249
|
-
export function parseCookies(
|
|
250
|
-
cookieHeader: string | null
|
|
251
|
-
): Record<string, string> {
|
|
252
|
-
if (!cookieHeader) return {};
|
|
253
|
-
|
|
254
|
-
const cookies: Record<string, string> = {};
|
|
255
|
-
const pairs = cookieHeader.split(";");
|
|
256
|
-
|
|
257
|
-
for (const pair of pairs) {
|
|
258
|
-
const [name, ...rest] = pair.trim().split("=");
|
|
259
|
-
if (name) {
|
|
260
|
-
cookies[name] = decodeURIComponent(rest.join("="));
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return cookies;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Serialize a cookie for Set-Cookie header
|
|
269
|
-
*/
|
|
270
|
-
export function serializeCookie(
|
|
271
|
-
name: string,
|
|
272
|
-
value: string,
|
|
273
|
-
options: CookieOptions = {}
|
|
274
|
-
): string {
|
|
275
|
-
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
276
|
-
|
|
277
|
-
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
278
|
-
if (options.path) cookie += `; Path=${options.path}`;
|
|
279
|
-
if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
|
|
280
|
-
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
281
|
-
if (options.httpOnly) cookie += "; HttpOnly";
|
|
282
|
-
if (options.secure) cookie += "; Secure";
|
|
283
|
-
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
284
|
-
|
|
285
|
-
return cookie;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Mutable response holder - allows ctx.res to be updated after next() is called
|
|
290
|
-
*/
|
|
291
|
-
export interface ResponseHolder {
|
|
292
|
-
response: Response | null;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
170
|
/**
|
|
296
171
|
* Create middleware context
|
|
297
172
|
*
|
|
298
173
|
* Note: The implementation uses runtime values while the interface provides
|
|
299
174
|
* compile-time type safety. The env/get/set types are resolved at call sites
|
|
300
|
-
* via conditional types based on TEnv
|
|
175
|
+
* via conditional types based on TEnv from createRouter<TBindings>().
|
|
301
176
|
*/
|
|
302
177
|
export function createMiddlewareContext<TEnv>(
|
|
303
178
|
request: Request,
|
|
304
179
|
env: TEnv,
|
|
305
180
|
params: Record<string, string>,
|
|
306
181
|
variables: Record<string, unknown>,
|
|
307
|
-
responseHolder: ResponseHolder
|
|
182
|
+
responseHolder: ResponseHolder,
|
|
183
|
+
reverse?: (
|
|
184
|
+
name: string,
|
|
185
|
+
params?: Record<string, string>,
|
|
186
|
+
search?: Record<string, unknown>,
|
|
187
|
+
) => string,
|
|
308
188
|
): MiddlewareContext<TEnv> {
|
|
309
|
-
const url = new URL(request.url);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
189
|
+
const url = stripInternalParams(new URL(request.url));
|
|
190
|
+
|
|
191
|
+
// Track the initial response to detect pre/post-next() phase.
|
|
192
|
+
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
193
|
+
// After next(): responseHolder.response is the real downstream response.
|
|
194
|
+
const initialResponse = responseHolder.response;
|
|
195
|
+
const isPreNext = () => responseHolder.response === initialResponse;
|
|
196
|
+
|
|
197
|
+
// Delegation strategy for RequestContext (reqCtx):
|
|
198
|
+
// - res getter: before next() returns shared reqCtx stub; after next() returns
|
|
199
|
+
// the real downstream response.
|
|
200
|
+
// - header(): before next() delegates to reqCtx; after next() writes to the
|
|
201
|
+
// real downstream response.
|
|
202
|
+
// Cookie operations are handled by the standalone cookies() function which
|
|
203
|
+
// delegates to the shared RequestContext internally.
|
|
313
204
|
// The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
|
|
205
|
+
// Internal helper: resolve the current response (stub before next(), real after).
|
|
206
|
+
// Not exposed on the public MiddlewareContext type — use ctx.headers instead.
|
|
207
|
+
const getResponse = (): Response => {
|
|
208
|
+
if (isPreNext()) {
|
|
209
|
+
const reqCtx = _getRequestContext();
|
|
210
|
+
if (reqCtx) return reqCtx.res;
|
|
211
|
+
}
|
|
212
|
+
if (!responseHolder.response) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"Response is not available - responseHolder was not initialized",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return responseHolder.response;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
221
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
222
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
223
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
224
|
+
// during `await next()`.
|
|
225
|
+
const reqCtx = _getRequestContext();
|
|
314
226
|
return {
|
|
315
227
|
request,
|
|
316
228
|
url,
|
|
229
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
317
230
|
pathname: url.pathname,
|
|
318
231
|
searchParams: url.searchParams,
|
|
319
232
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
320
233
|
params,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
234
|
+
executionContext: reqCtx?.executionContext,
|
|
235
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
236
|
+
// Getter: re-derives from request context on each access so that global
|
|
237
|
+
// middleware sees the matched route name after await next().
|
|
238
|
+
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
239
|
+
const reqCtx = _getRequestContext();
|
|
240
|
+
const raw = reqCtx?._routeName;
|
|
241
|
+
return (
|
|
242
|
+
raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
|
|
243
|
+
) as MiddlewareContext<TEnv>["routeName"];
|
|
330
244
|
},
|
|
331
245
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
responseHolder.response = response;
|
|
246
|
+
get headers(): Headers {
|
|
247
|
+
return getResponse().headers;
|
|
335
248
|
},
|
|
336
249
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
parsedCookies = parseCookies(cookieHeader);
|
|
340
|
-
}
|
|
341
|
-
return parsedCookies[name];
|
|
342
|
-
},
|
|
250
|
+
get: ((keyOrVar: any) =>
|
|
251
|
+
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
343
252
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
253
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
254
|
+
contextSet(variables, keyOrVar, value, options);
|
|
255
|
+
}) as MiddlewareContext<TEnv>["set"],
|
|
256
|
+
header(name: string, value: string): void {
|
|
257
|
+
// Before next(): delegate to shared RequestContext stub
|
|
258
|
+
if (isPreNext()) {
|
|
259
|
+
const reqCtx = _getRequestContext();
|
|
260
|
+
if (reqCtx) {
|
|
261
|
+
reqCtx.header(name, value);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
347
264
|
}
|
|
348
|
-
|
|
349
|
-
},
|
|
350
|
-
|
|
351
|
-
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
265
|
+
// After next() or standalone: write to current response
|
|
352
266
|
if (!responseHolder.response) {
|
|
353
267
|
throw new Error(
|
|
354
|
-
"ctx.
|
|
268
|
+
"ctx.header() is not available - responseHolder was not initialized",
|
|
355
269
|
);
|
|
356
270
|
}
|
|
357
|
-
responseHolder.response.headers.
|
|
358
|
-
"Set-Cookie",
|
|
359
|
-
serializeCookie(name, value, options)
|
|
360
|
-
);
|
|
271
|
+
responseHolder.response.headers.set(name, value);
|
|
361
272
|
},
|
|
362
273
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
274
|
+
get theme(): MiddlewareContext<TEnv>["theme"] {
|
|
275
|
+
return _getRequestContext()?.theme;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
|
|
279
|
+
return _getRequestContext()?.setTheme;
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
setLocationState(entries) {
|
|
283
|
+
const reqCtx = _getRequestContext();
|
|
284
|
+
if (!reqCtx) {
|
|
368
285
|
throw new Error(
|
|
369
|
-
"
|
|
286
|
+
"setLocationState() is not available outside a request context",
|
|
370
287
|
);
|
|
371
288
|
}
|
|
372
|
-
|
|
373
|
-
"Set-Cookie",
|
|
374
|
-
serializeCookie(name, "", { ...options, maxAge: 0 })
|
|
375
|
-
);
|
|
289
|
+
reqCtx.setLocationState(entries);
|
|
376
290
|
},
|
|
377
291
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (!responseHolder.response) {
|
|
292
|
+
reverse:
|
|
293
|
+
reverse ??
|
|
294
|
+
((
|
|
295
|
+
name: string,
|
|
296
|
+
_params?: Record<string, string>,
|
|
297
|
+
_search?: Record<string, unknown>,
|
|
298
|
+
) => {
|
|
386
299
|
throw new Error(
|
|
387
|
-
|
|
300
|
+
`ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
|
|
388
301
|
);
|
|
302
|
+
}),
|
|
303
|
+
|
|
304
|
+
debugPerformance(): void {
|
|
305
|
+
const reqCtx = _getRequestContext();
|
|
306
|
+
if (reqCtx) {
|
|
307
|
+
reqCtx._debugPerformance = true;
|
|
308
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
389
309
|
}
|
|
390
|
-
responseHolder.response.headers.set(name, value);
|
|
391
310
|
},
|
|
392
311
|
};
|
|
393
312
|
}
|
|
@@ -398,7 +317,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
398
317
|
*/
|
|
399
318
|
export function matchMiddleware<TEnv>(
|
|
400
319
|
pathname: string,
|
|
401
|
-
entries: MiddlewareEntry<TEnv>[]
|
|
320
|
+
entries: MiddlewareEntry<TEnv>[],
|
|
402
321
|
): Array<{ entry: MiddlewareEntry<TEnv>; params: Record<string, string> }> {
|
|
403
322
|
const matches: Array<{
|
|
404
323
|
entry: MiddlewareEntry<TEnv>;
|
|
@@ -412,9 +331,16 @@ export function matchMiddleware<TEnv>(
|
|
|
412
331
|
continue;
|
|
413
332
|
}
|
|
414
333
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
334
|
+
// Run the scope regex ONCE per entry. The old code ran test() then, on a
|
|
335
|
+
// hit, extractParams' match() — a second full pass over the same string for
|
|
336
|
+
// every matching entry on the per-request hot path. The regexes carry no
|
|
337
|
+
// `g` flag, so there is no lastIndex statefulness across this single match.
|
|
338
|
+
const m = pathname.match(entry.regex);
|
|
339
|
+
if (m) {
|
|
340
|
+
const params: Record<string, string> = {};
|
|
341
|
+
for (let i = 0; i < entry.paramNames.length; i++) {
|
|
342
|
+
params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
|
|
343
|
+
}
|
|
418
344
|
matches.push({ entry, params });
|
|
419
345
|
}
|
|
420
346
|
}
|
|
@@ -422,15 +348,97 @@ export function matchMiddleware<TEnv>(
|
|
|
422
348
|
return matches;
|
|
423
349
|
}
|
|
424
350
|
|
|
351
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
352
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
353
|
+
// explicit short-circuit Response's own headers win).
|
|
354
|
+
function mergeStubHeaders(
|
|
355
|
+
target: Headers,
|
|
356
|
+
stub: Headers,
|
|
357
|
+
stubOverridesNonCookie: boolean,
|
|
358
|
+
): void {
|
|
359
|
+
stub.forEach((value, name) => {
|
|
360
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
361
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
362
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
363
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
364
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
365
|
+
target.append(name, value);
|
|
366
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
367
|
+
target.set(name, value);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
373
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
374
|
+
function mergeReqCtxStub(
|
|
375
|
+
target: Headers,
|
|
376
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
377
|
+
): void {
|
|
378
|
+
if (!reqCtx) return;
|
|
379
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
380
|
+
if (stubCookies.length > 0) {
|
|
381
|
+
const existing = new Set(target.getSetCookie());
|
|
382
|
+
for (const cookie of stubCookies) {
|
|
383
|
+
if (!existing.has(cookie)) {
|
|
384
|
+
target.append("set-cookie", cookie);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
389
|
+
// Never propagate the reserved external-redirect marker (see mergeStubHeaders).
|
|
390
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
391
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
392
|
+
target.set(name, value);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Clone `base` with stub headers merged into a fresh Headers (the clone keeps
|
|
398
|
+
// the body mutable for post-next() modifications). Set-Cookie is always
|
|
399
|
+
// appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
|
|
400
|
+
// mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
|
|
401
|
+
// short-circuit path passes false (its reqCtx headers are not merged here),
|
|
402
|
+
// which is the one deliberate divergence between the call sites.
|
|
403
|
+
function mergeResponse(
|
|
404
|
+
base: Response,
|
|
405
|
+
stub: Headers,
|
|
406
|
+
opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
|
|
407
|
+
): Response {
|
|
408
|
+
const mergedHeaders = new Headers(base.headers);
|
|
409
|
+
// The reserved external-redirect marker is never a trust signal and must never
|
|
410
|
+
// reach the browser. The guard strips it on 3xx redirects; strip it here too so
|
|
411
|
+
// a forged value cannot ride a non-3xx middleware response (which the 3xx-only
|
|
412
|
+
// guard would not touch) to the client. The opt-in is the out-of-band brand.
|
|
413
|
+
mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
|
|
414
|
+
mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
|
|
415
|
+
if (opts.mergeReqCtx) {
|
|
416
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
417
|
+
}
|
|
418
|
+
const merged = new Response(base.body, {
|
|
419
|
+
status: base.status,
|
|
420
|
+
statusText: base.statusText,
|
|
421
|
+
headers: mergedHeaders,
|
|
422
|
+
});
|
|
423
|
+
// Transfer the out-of-band external-redirect brand across this rebuild: a
|
|
424
|
+
// middleware short-circuit `return redirect(url, { external: true })` reaches
|
|
425
|
+
// the open-redirect guard only after this merge, and the brand lives on the
|
|
426
|
+
// Response object, not in its headers.
|
|
427
|
+
if (isExternalRedirect(base)) {
|
|
428
|
+
markExternalRedirect(merged);
|
|
429
|
+
}
|
|
430
|
+
return merged;
|
|
431
|
+
}
|
|
432
|
+
|
|
425
433
|
/**
|
|
426
434
|
* Execute middleware chain
|
|
427
435
|
*
|
|
428
436
|
* Features:
|
|
429
437
|
* - `await next()` returns actual Response
|
|
430
|
-
* - `ctx.
|
|
431
|
-
* - `ctx.header()` shorthand for setting
|
|
432
|
-
* - Forgiving: if middleware doesn't return, uses
|
|
433
|
-
* - Short-circuit: return Response to stop chain
|
|
438
|
+
* - `ctx.headers` available before and after `await next()`
|
|
439
|
+
* - `ctx.header()` shorthand for setting a single header
|
|
440
|
+
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
441
|
+
* - Short-circuit: return OR throw a Response to stop chain
|
|
434
442
|
* - Error catching: try/catch around `next()` works
|
|
435
443
|
*/
|
|
436
444
|
export async function executeMiddleware<TEnv>(
|
|
@@ -441,7 +449,12 @@ export async function executeMiddleware<TEnv>(
|
|
|
441
449
|
request: Request,
|
|
442
450
|
env: TEnv,
|
|
443
451
|
variables: Record<string, any>,
|
|
444
|
-
finalHandler: () => Promise<Response
|
|
452
|
+
finalHandler: () => Promise<Response>,
|
|
453
|
+
reverse?: (
|
|
454
|
+
name: string,
|
|
455
|
+
params?: Record<string, string>,
|
|
456
|
+
search?: Record<string, unknown>,
|
|
457
|
+
) => string,
|
|
445
458
|
): Promise<Response> {
|
|
446
459
|
let index = 0;
|
|
447
460
|
|
|
@@ -455,50 +468,130 @@ export async function executeMiddleware<TEnv>(
|
|
|
455
468
|
// End of chain - call actual RSC handler
|
|
456
469
|
const response = await finalHandler();
|
|
457
470
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
463
|
-
mergedHeaders.append(name, value);
|
|
464
|
-
} else {
|
|
465
|
-
mergedHeaders.set(name, value);
|
|
466
|
-
}
|
|
467
|
-
});
|
|
471
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
472
|
+
responseHolder.response = response;
|
|
473
|
+
return response;
|
|
474
|
+
}
|
|
468
475
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
476
|
+
// Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
|
|
477
|
+
// and reqCtx stub headers are merged in.
|
|
478
|
+
responseHolder.response = mergeResponse(response, stubResponse.headers, {
|
|
479
|
+
stubOverridesNonCookie: true,
|
|
480
|
+
mergeReqCtx: true,
|
|
474
481
|
});
|
|
475
482
|
|
|
476
483
|
return responseHolder.response;
|
|
477
484
|
}
|
|
478
485
|
|
|
486
|
+
const middlewareOrdinal = index;
|
|
479
487
|
const { entry, params } = middlewares[index++];
|
|
480
488
|
const ctx = createMiddlewareContext(
|
|
481
489
|
request,
|
|
482
490
|
env,
|
|
483
491
|
params,
|
|
484
492
|
variables,
|
|
485
|
-
responseHolder
|
|
493
|
+
responseHolder,
|
|
494
|
+
reverse,
|
|
486
495
|
);
|
|
496
|
+
const metricStart = performance.now();
|
|
497
|
+
const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
|
|
498
|
+
let middlewareFinished = false;
|
|
499
|
+
const finishMiddleware = () => {
|
|
500
|
+
if (!middlewareFinished) {
|
|
501
|
+
middlewareFinished = true;
|
|
502
|
+
appendMetric(
|
|
503
|
+
_getRequestContext()?._metricsStore,
|
|
504
|
+
`${metricLabel}:pre`,
|
|
505
|
+
metricStart,
|
|
506
|
+
performance.now() - metricStart,
|
|
507
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
487
511
|
|
|
488
|
-
// Track if next() was called and capture its Promise
|
|
489
|
-
//
|
|
512
|
+
// Track if next() was called and capture its Promise.
|
|
513
|
+
// Guard against double-calling: a second call would re-enter the
|
|
514
|
+
// downstream chain and overwrite responseHolder.response.
|
|
490
515
|
let nextPromise: Promise<Response> | null = null;
|
|
516
|
+
let nextResolvedAt: number | undefined;
|
|
491
517
|
const wrappedNext = (): Promise<Response> => {
|
|
492
|
-
|
|
518
|
+
if (nextPromise) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`[@rangojs/router] Middleware called next() more than once.`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
finishMiddleware();
|
|
524
|
+
const downstream = next();
|
|
525
|
+
nextPromise = downstream.then(
|
|
526
|
+
(res) => {
|
|
527
|
+
nextResolvedAt = performance.now();
|
|
528
|
+
return res;
|
|
529
|
+
},
|
|
530
|
+
(err) => {
|
|
531
|
+
nextResolvedAt = performance.now();
|
|
532
|
+
throw err;
|
|
533
|
+
},
|
|
534
|
+
);
|
|
493
535
|
return nextPromise;
|
|
494
536
|
};
|
|
495
537
|
|
|
496
|
-
|
|
538
|
+
// Wrap the middleware (including its downstream next() chain) in its span
|
|
539
|
+
// via the unified phase API. metric:false — the middleware's perf metric is
|
|
540
|
+
// its exclusive pre/post own-time, recorded directly above and below, finer
|
|
541
|
+
// than a single wrap. Spans nest by async context, so this onions
|
|
542
|
+
// middleware-over-middleware and the core handler underneath. Pass-through
|
|
543
|
+
// when neither surface is active.
|
|
544
|
+
let result: Response | void;
|
|
545
|
+
try {
|
|
546
|
+
result = await observePhase(PHASES.middleware(metricLabel), () =>
|
|
547
|
+
entry.handler(ctx, wrappedNext),
|
|
548
|
+
);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
// Thrown Response is short-circuit control flow, not an error.
|
|
551
|
+
// Fall through to the `if (result instanceof Response)` branch below
|
|
552
|
+
// so stub headers and request-context cookies merge as they do for
|
|
553
|
+
// an explicit `return new Response(...)`. Real errors propagate.
|
|
554
|
+
if (error instanceof Response) {
|
|
555
|
+
result = error;
|
|
556
|
+
} else {
|
|
557
|
+
finishMiddleware();
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
finishMiddleware();
|
|
562
|
+
|
|
563
|
+
// Record post-next() processing time when middleware did work after
|
|
564
|
+
// the downstream chain resolved (e.g. adding headers, logging).
|
|
565
|
+
if (nextResolvedAt !== undefined) {
|
|
566
|
+
const postDur = performance.now() - nextResolvedAt;
|
|
567
|
+
if (postDur > POST_METRIC_MIN_DURATION_MS) {
|
|
568
|
+
appendMetric(
|
|
569
|
+
_getRequestContext()?._metricsStore,
|
|
570
|
+
`${metricLabel}:post`,
|
|
571
|
+
nextResolvedAt,
|
|
572
|
+
postDur,
|
|
573
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
497
577
|
|
|
498
|
-
// Explicit return takes precedence
|
|
578
|
+
// Explicit return takes precedence (middleware short-circuit).
|
|
579
|
+
// Merge stub headers (from ctx.header before this point) and
|
|
580
|
+
// RequestContext stub headers (from cookies().set()) into the
|
|
581
|
+
// returned Response so they are not lost.
|
|
499
582
|
if (result instanceof Response) {
|
|
500
|
-
|
|
501
|
-
|
|
583
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
584
|
+
responseHolder.response = result;
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
// Explicit short-circuit: the returned Response's own headers win
|
|
588
|
+
// (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
|
|
589
|
+
const merged = mergeResponse(result, stubResponse.headers, {
|
|
590
|
+
stubOverridesNonCookie: false,
|
|
591
|
+
mergeReqCtx: true,
|
|
592
|
+
});
|
|
593
|
+
responseHolder.response = merged;
|
|
594
|
+
return merged;
|
|
502
595
|
}
|
|
503
596
|
|
|
504
597
|
// Warn about unexpected return values (non-Response, non-undefined)
|
|
@@ -507,7 +600,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
507
600
|
const fnName = entry.handler.name || "(anonymous)";
|
|
508
601
|
console.warn(
|
|
509
602
|
`[Middleware] "${fnName}" returned ${typeof result} instead of Response or undefined. ` +
|
|
510
|
-
`This return value will be ignored. Did you mean to return a Response
|
|
603
|
+
`This return value will be ignored. Did you mean to return a Response?`,
|
|
511
604
|
);
|
|
512
605
|
}
|
|
513
606
|
|
|
@@ -524,7 +617,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
524
617
|
`Middleware must call next() or return a Response. ` +
|
|
525
618
|
`Function: ${fnName}, Pattern: ${entry.pattern ?? "(all)"}
|
|
526
619
|
Source: ${import.meta.env.DEV ? entry.handler.toString().slice(0, 200) : "(source hidden in production)"}`,
|
|
527
|
-
{ cause: { url: request.url, fn: entry.handler } }
|
|
620
|
+
{ cause: { url: request.url, fn: entry.handler } },
|
|
528
621
|
);
|
|
529
622
|
};
|
|
530
623
|
|
|
@@ -536,64 +629,19 @@ export async function executeMiddleware<TEnv>(
|
|
|
536
629
|
throw new Error("No response generated by middleware chain");
|
|
537
630
|
}
|
|
538
631
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
* - Throws if middleware returns Response (can't short-circuit server action)
|
|
550
|
-
*/
|
|
551
|
-
export async function executeServerActionMiddleware<TEnv>(
|
|
552
|
-
middlewares: MiddlewareFn<TEnv>[],
|
|
553
|
-
request: Request,
|
|
554
|
-
env: TEnv,
|
|
555
|
-
params: Record<string, string>,
|
|
556
|
-
variables: Record<string, any>,
|
|
557
|
-
stubResponse: Response
|
|
558
|
-
): Promise<void> {
|
|
559
|
-
if (middlewares.length === 0) {
|
|
560
|
-
return;
|
|
632
|
+
// Final re-merge: capture any RequestContext stub headers added after the
|
|
633
|
+
// last merge point (e.g. cookies().set() called after await next()).
|
|
634
|
+
// The reqCtx stub may have already been partially merged during finalHandler
|
|
635
|
+
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
636
|
+
//
|
|
637
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
638
|
+
// set-cookie on an upgrade is not meaningful.
|
|
639
|
+
const reqCtx = _getRequestContext();
|
|
640
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
641
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
561
642
|
}
|
|
562
643
|
|
|
563
|
-
|
|
564
|
-
const responseHolder: ResponseHolder = { response: stubResponse };
|
|
565
|
-
|
|
566
|
-
const next = async (): Promise<Response> => {
|
|
567
|
-
if (index >= middlewares.length) {
|
|
568
|
-
return stubResponse;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const middleware = middlewares[index++];
|
|
572
|
-
const ctx = createMiddlewareContext(
|
|
573
|
-
request,
|
|
574
|
-
env,
|
|
575
|
-
params,
|
|
576
|
-
variables,
|
|
577
|
-
responseHolder
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
const result = await middleware(ctx, next);
|
|
581
|
-
|
|
582
|
-
// If middleware returned a Response, throw an error
|
|
583
|
-
// Server actions can't short-circuit with a Response
|
|
584
|
-
if (result instanceof Response) {
|
|
585
|
-
throw new Error(
|
|
586
|
-
`Loader middleware returned a Response (status: ${result.status}). ` +
|
|
587
|
-
`Server actions cannot return Response. ` +
|
|
588
|
-
`Use GET-based loader fetching for redirects, or throw an error instead.`
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return stubResponse;
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
await next();
|
|
596
|
-
// Headers/cookies set on stubResponse will be merged by the caller
|
|
644
|
+
return finalResponse;
|
|
597
645
|
}
|
|
598
646
|
|
|
599
647
|
/**
|
|
@@ -601,7 +649,7 @@ export async function executeServerActionMiddleware<TEnv>(
|
|
|
601
649
|
*
|
|
602
650
|
* Intercepts use a shared stubResponse from the request context. This function:
|
|
603
651
|
* - Runs middleware in sequence with a simple next() chain
|
|
604
|
-
* - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
|
|
652
|
+
* - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
|
|
605
653
|
* - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
|
|
606
654
|
*
|
|
607
655
|
* @param middlewares - Array of middleware functions
|
|
@@ -617,7 +665,12 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
617
665
|
env: TEnv,
|
|
618
666
|
params: Record<string, string>,
|
|
619
667
|
variables: Record<string, any>,
|
|
620
|
-
stubResponse: Response
|
|
668
|
+
stubResponse: Response,
|
|
669
|
+
reverse?: (
|
|
670
|
+
name: string,
|
|
671
|
+
params?: Record<string, string>,
|
|
672
|
+
search?: Record<string, unknown>,
|
|
673
|
+
) => string,
|
|
621
674
|
): Promise<Response | null> {
|
|
622
675
|
if (middlewares.length === 0) {
|
|
623
676
|
return null;
|
|
@@ -634,28 +687,57 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
634
687
|
return stubResponse;
|
|
635
688
|
}
|
|
636
689
|
|
|
690
|
+
const ordinal = index;
|
|
637
691
|
const middleware = middlewares[index++];
|
|
638
692
|
const ctx = createMiddlewareContext(
|
|
639
693
|
request,
|
|
640
694
|
env,
|
|
641
695
|
params,
|
|
642
696
|
variables,
|
|
643
|
-
responseHolder
|
|
697
|
+
responseHolder,
|
|
698
|
+
reverse,
|
|
644
699
|
);
|
|
645
700
|
|
|
646
|
-
|
|
701
|
+
let nextCalled = false;
|
|
702
|
+
const guardedNext = (): Promise<Response> => {
|
|
703
|
+
if (nextCalled) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`[@rangojs/router] Intercept middleware called next() more than once.`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
nextCalled = true;
|
|
709
|
+
return next();
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// Span-wrap each intercept middleware as rango.middleware (metric:false —
|
|
713
|
+
// intercept runs inside the render phase already metered by render:total, so
|
|
714
|
+
// it contributes a span but no separate perf metric). Bare MiddlewareFns
|
|
715
|
+
// have no pattern, so the label is scoped to "*" like a pattern-less entry.
|
|
716
|
+
const label = getMiddlewareMetricLabel(
|
|
717
|
+
{ handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
|
|
718
|
+
ordinal,
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
let result: Response | void;
|
|
722
|
+
try {
|
|
723
|
+
result = await observePhase(PHASES.middleware(label), () =>
|
|
724
|
+
middleware(ctx, guardedNext),
|
|
725
|
+
);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
728
|
+
// explicit-return path below. Real errors propagate.
|
|
729
|
+
if (error instanceof Response) {
|
|
730
|
+
result = error;
|
|
731
|
+
} else {
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
647
735
|
|
|
648
736
|
if (result instanceof Response) {
|
|
649
737
|
earlyResponse = result;
|
|
650
738
|
return result;
|
|
651
739
|
}
|
|
652
740
|
|
|
653
|
-
// Check if middleware replaced ctx.res with a different response
|
|
654
|
-
if (responseHolder.response && responseHolder.response !== stubResponse) {
|
|
655
|
-
earlyResponse = responseHolder.response;
|
|
656
|
-
return earlyResponse;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
741
|
return stubResponse;
|
|
660
742
|
};
|
|
661
743
|
|
|
@@ -673,19 +755,13 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
673
755
|
});
|
|
674
756
|
|
|
675
757
|
if (hasStubHeaders) {
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
return new Response(response.body, {
|
|
686
|
-
status: response.status,
|
|
687
|
-
statusText: response.statusText,
|
|
688
|
-
headers: mergedHeaders,
|
|
758
|
+
// Only fill in missing headers — the returned Response's explicit headers
|
|
759
|
+
// take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
|
|
760
|
+
// mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
|
|
761
|
+
// stub headers here (pinned by intercept-middleware-headers.test.ts).
|
|
762
|
+
return mergeResponse(response, stubResponse.headers, {
|
|
763
|
+
stubOverridesNonCookie: false,
|
|
764
|
+
mergeReqCtx: false,
|
|
689
765
|
});
|
|
690
766
|
}
|
|
691
767
|
return response;
|
|
@@ -708,7 +784,12 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
708
784
|
env: TEnv,
|
|
709
785
|
params: Record<string, string>,
|
|
710
786
|
variables: Record<string, any>,
|
|
711
|
-
finalHandler: () => Promise<Response
|
|
787
|
+
finalHandler: () => Promise<Response>,
|
|
788
|
+
reverse?: (
|
|
789
|
+
name: string,
|
|
790
|
+
params?: Record<string, string>,
|
|
791
|
+
search?: Record<string, unknown>,
|
|
792
|
+
) => string,
|
|
712
793
|
): Promise<Response> {
|
|
713
794
|
if (middlewares.length === 0) {
|
|
714
795
|
return finalHandler();
|
|
@@ -721,7 +802,6 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
721
802
|
regex: null,
|
|
722
803
|
paramNames: [],
|
|
723
804
|
handler,
|
|
724
|
-
mountPrefix: null,
|
|
725
805
|
} as MiddlewareEntry<TEnv>,
|
|
726
806
|
params,
|
|
727
807
|
}));
|
|
@@ -731,27 +811,11 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
731
811
|
request,
|
|
732
812
|
env,
|
|
733
813
|
variables,
|
|
734
|
-
finalHandler
|
|
814
|
+
finalHandler,
|
|
815
|
+
reverse,
|
|
735
816
|
);
|
|
736
817
|
}
|
|
737
818
|
|
|
738
|
-
/**
|
|
739
|
-
* Entry type for middleware collection
|
|
740
|
-
* Matches the shape of EntryData used in router.ts
|
|
741
|
-
*/
|
|
742
|
-
export interface MiddlewareCollectableEntry {
|
|
743
|
-
middleware?: MiddlewareFn<any, any>[];
|
|
744
|
-
layout?: MiddlewareCollectableEntry[];
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Collected route middleware with params
|
|
749
|
-
*/
|
|
750
|
-
export interface CollectedMiddleware {
|
|
751
|
-
handler: MiddlewareFn<any, any>;
|
|
752
|
-
params: Record<string, string>;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
819
|
/**
|
|
756
820
|
* Collect route-level middleware from an entry tree
|
|
757
821
|
*
|
|
@@ -764,7 +828,7 @@ export interface CollectedMiddleware {
|
|
|
764
828
|
*/
|
|
765
829
|
export function collectRouteMiddleware(
|
|
766
830
|
entries: Iterable<MiddlewareCollectableEntry>,
|
|
767
|
-
params: Record<string, string
|
|
831
|
+
params: Record<string, string>,
|
|
768
832
|
): CollectedMiddleware[] {
|
|
769
833
|
const result: CollectedMiddleware[] = [];
|
|
770
834
|
|