@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -0
- package/README.md +291 -61
- package/dist/bin/rango.js +544 -143
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3744 -1329
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +67 -13
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +312 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +322 -19
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +282 -60
- package/skills/host-router/SKILL.md +278 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +297 -31
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +155 -111
- package/skills/rango/SKILL.md +251 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +104 -9
- package/skills/router-setup/SKILL.md +124 -11
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +125 -222
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +357 -52
- package/skills/use-cache/SKILL.md +46 -14
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/bin/rango.ts +18 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +200 -30
- package/src/browser/navigation-client.ts +217 -58
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +312 -30
- package/src/browser/prefetch/fetch.ts +344 -47
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +125 -18
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +80 -5
- package/src/browser/rsc-router.tsx +226 -75
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +619 -442
- package/src/browser/types.ts +115 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +7 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +182 -37
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +392 -106
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +45 -21
- package/src/client.tsx +120 -336
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +95 -0
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -5
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +79 -29
- package/src/index.ts +149 -65
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +63 -26
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +110 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -49
- package/src/router/metrics.ts +217 -26
- package/src/router/middleware-types.ts +24 -110
- package/src/router/middleware.ts +384 -197
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +148 -91
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +199 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +144 -74
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +129 -36
- package/src/router/router-options.ts +185 -23
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +281 -76
- package/src/router/segment-resolution/helpers.ts +116 -31
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +493 -391
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +180 -58
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +182 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +702 -460
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +98 -19
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +126 -106
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +269 -114
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +34 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +49 -41
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +317 -63
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +425 -177
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +25 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +3 -19
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +236 -88
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +10 -45
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -19
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +64 -91
- package/src/vite/discovery/discover-routers.ts +147 -88
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +247 -145
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +61 -13
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +10 -3
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +241 -287
- package/src/vite/router-discovery.ts +956 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +141 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/CLAUDE.md +0 -5
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -6
- package/src/network-error-thrower.tsx +0 -23
- package/src/route-definition/route-function.ts +0 -119
- package/src/router/middleware-cookies.ts +0 -55
package/src/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,46 +14,79 @@ 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
|
+
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";
|
|
23
27
|
|
|
24
|
-
// Re-export types
|
|
28
|
+
// Re-export types consumed through this module's path.
|
|
25
29
|
export type {
|
|
26
30
|
CookieOptions,
|
|
27
|
-
CollectedMiddleware,
|
|
28
|
-
MiddlewareCollectableEntry,
|
|
29
31
|
MiddlewareContext,
|
|
30
32
|
MiddlewareEntry,
|
|
31
33
|
MiddlewareFn,
|
|
32
|
-
ResponseHolder,
|
|
33
34
|
} from "./middleware-types.js";
|
|
34
|
-
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
35
|
-
|
|
36
|
-
// W5: Deduplicate by function reference so each distinct middleware warns once,
|
|
37
|
-
// regardless of whether it is named or anonymous.
|
|
38
|
-
let warnedRedirectMiddleware = new WeakSet<Function>();
|
|
39
|
-
|
|
40
|
-
function warnCtxSetBeforeRedirect(handler: Function): void {
|
|
41
|
-
if (warnedRedirectMiddleware.has(handler)) return;
|
|
42
|
-
warnedRedirectMiddleware.add(handler);
|
|
43
|
-
const label = handler.name || "(anonymous)";
|
|
44
|
-
console.warn(
|
|
45
|
-
`[rango] Route middleware "${label}" called ctx.set() then returned a ` +
|
|
46
|
-
`redirect. Context variables are per-request and won't be available ` +
|
|
47
|
-
`on the redirect target. Use cookies to persist state across ` +
|
|
48
|
-
`redirects, or move ctx.set() to the target route's middleware.`,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 ?? "*";
|
|
45
|
+
|
|
46
|
+
if (handlerName) {
|
|
47
|
+
return `middleware:${handlerName}@${scope}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `middleware:${scope}#${ordinal + 1}`;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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).
|
|
60
88
|
*/
|
|
61
|
-
export function
|
|
89
|
+
export function compileMiddlewarePattern(pattern: string): {
|
|
62
90
|
regex: RegExp;
|
|
63
91
|
paramNames: string[];
|
|
64
92
|
} {
|
|
@@ -66,47 +94,54 @@ export function parsePattern(pattern: string): {
|
|
|
66
94
|
return { regex: /^.*$/, paramNames: [] };
|
|
67
95
|
}
|
|
68
96
|
|
|
97
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
98
|
+
const segments = parseRoutePattern(normalizedPattern);
|
|
69
99
|
const paramNames: string[] = [];
|
|
70
100
|
let regexStr = "^";
|
|
101
|
+
let hasTrailingWildcard = false;
|
|
71
102
|
|
|
72
|
-
|
|
103
|
+
for (let i = 0; i < segments.length; i++) {
|
|
104
|
+
const segment = segments[i];
|
|
73
105
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
106
|
+
if (segment.type === "wildcard") {
|
|
107
|
+
// Optional subtree match (parity with the original middleware parser,
|
|
108
|
+
// which compiled every `*` as `(?:/.*)?`). A trailing `*` matches the
|
|
109
|
+
// subtree; a non-trailing `*` matches zero-or-more intermediate segments,
|
|
110
|
+
// so `/a/<star>/b` still matches `/a/b`.
|
|
79
111
|
regexStr += "(?:/.*)?";
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
if (i === segments.length - 1) {
|
|
113
|
+
hasTrailingWildcard = true;
|
|
114
|
+
}
|
|
115
|
+
} else if (segment.type === "param") {
|
|
116
|
+
paramNames.push(segment.value);
|
|
117
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
118
|
+
const valuePattern = segment.constraint
|
|
119
|
+
? `(${segment.constraint.map(escapeRegExp).join("|")})`
|
|
120
|
+
: "([^/]+)";
|
|
121
|
+
if (segment.optional) {
|
|
122
|
+
regexStr += `(?:/${valuePattern}${suffixPattern})?`;
|
|
123
|
+
} else {
|
|
124
|
+
regexStr += `/${valuePattern}${suffixPattern}`;
|
|
125
|
+
}
|
|
85
126
|
} else {
|
|
86
|
-
//
|
|
87
|
-
regexStr += "/" +
|
|
127
|
+
// Static literal
|
|
128
|
+
regexStr += "/" + escapeRegExp(segment.value);
|
|
88
129
|
}
|
|
89
130
|
}
|
|
90
131
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
regexStr += "/?$";
|
|
94
|
-
} else {
|
|
95
|
-
regexStr += "$";
|
|
96
|
-
}
|
|
132
|
+
// Without a trailing `*`, match exactly with an optional trailing slash.
|
|
133
|
+
regexStr += hasTrailingWildcard ? "$" : "/?$";
|
|
97
134
|
|
|
98
135
|
return { regex: new RegExp(regexStr), paramNames };
|
|
99
136
|
}
|
|
100
137
|
|
|
101
138
|
/**
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
139
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
140
|
+
*
|
|
141
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
142
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
143
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
144
|
+
* Express/React Router/Fastify/Koa.
|
|
110
145
|
*/
|
|
111
146
|
export function extractParams(
|
|
112
147
|
pathname: string,
|
|
@@ -118,7 +153,7 @@ export function extractParams(
|
|
|
118
153
|
|
|
119
154
|
const params: Record<string, string> = {};
|
|
120
155
|
for (let i = 0; i < paramNames.length; i++) {
|
|
121
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
156
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
122
157
|
}
|
|
123
158
|
return params;
|
|
124
159
|
}
|
|
@@ -142,7 +177,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
142
177
|
search?: Record<string, unknown>,
|
|
143
178
|
) => string,
|
|
144
179
|
): MiddlewareContext<TEnv> {
|
|
145
|
-
const url = new URL(request.url);
|
|
180
|
+
const url = stripInternalParams(new URL(request.url));
|
|
146
181
|
|
|
147
182
|
// Track the initial response to detect pre/post-next() phase.
|
|
148
183
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -158,13 +193,37 @@ export function createMiddlewareContext<TEnv>(
|
|
|
158
193
|
// Cookie operations are handled by the standalone cookies() function which
|
|
159
194
|
// delegates to the shared RequestContext internally.
|
|
160
195
|
// The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
|
|
196
|
+
// Internal helper: resolve the current response (stub before next(), real after).
|
|
197
|
+
// Not exposed on the public MiddlewareContext type — use ctx.headers instead.
|
|
198
|
+
const getResponse = (): Response => {
|
|
199
|
+
if (isPreNext()) {
|
|
200
|
+
const reqCtx = _getRequestContext();
|
|
201
|
+
if (reqCtx) return reqCtx.res;
|
|
202
|
+
}
|
|
203
|
+
if (!responseHolder.response) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"Response is not available - responseHolder was not initialized",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return responseHolder.response;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
212
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
213
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
214
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
215
|
+
// during `await next()`.
|
|
216
|
+
const reqCtx = _getRequestContext();
|
|
161
217
|
return {
|
|
162
218
|
request,
|
|
163
219
|
url,
|
|
220
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
164
221
|
pathname: url.pathname,
|
|
165
222
|
searchParams: url.searchParams,
|
|
166
223
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
167
224
|
params,
|
|
225
|
+
executionContext: reqCtx?.executionContext,
|
|
226
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
168
227
|
// Getter: re-derives from request context on each access so that global
|
|
169
228
|
// middleware sees the matched route name after await next().
|
|
170
229
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -175,33 +234,16 @@ export function createMiddlewareContext<TEnv>(
|
|
|
175
234
|
) as MiddlewareContext<TEnv>["routeName"];
|
|
176
235
|
},
|
|
177
236
|
|
|
178
|
-
get
|
|
179
|
-
|
|
180
|
-
// set via ctx.header() are visible on ctx.res.
|
|
181
|
-
if (isPreNext()) {
|
|
182
|
-
const reqCtx = _getRequestContext();
|
|
183
|
-
if (reqCtx) return reqCtx.res;
|
|
184
|
-
}
|
|
185
|
-
if (!responseHolder.response) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
"ctx.res is not available - responseHolder was not initialized",
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
return responseHolder.response;
|
|
191
|
-
},
|
|
192
|
-
set res(_: Response) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
"ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
|
|
195
|
-
);
|
|
237
|
+
get headers(): Headers {
|
|
238
|
+
return getResponse().headers;
|
|
196
239
|
},
|
|
197
240
|
|
|
198
241
|
get: ((keyOrVar: any) =>
|
|
199
242
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
200
243
|
|
|
201
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
202
|
-
contextSet(variables, keyOrVar, value);
|
|
244
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
245
|
+
contextSet(variables, keyOrVar, value, options);
|
|
203
246
|
}) as MiddlewareContext<TEnv>["set"],
|
|
204
|
-
|
|
205
247
|
header(name: string, value: string): void {
|
|
206
248
|
// Before next(): delegate to shared RequestContext stub
|
|
207
249
|
if (isPreNext()) {
|
|
@@ -220,13 +262,43 @@ export function createMiddlewareContext<TEnv>(
|
|
|
220
262
|
responseHolder.response.headers.set(name, value);
|
|
221
263
|
},
|
|
222
264
|
|
|
265
|
+
get theme(): MiddlewareContext<TEnv>["theme"] {
|
|
266
|
+
return _getRequestContext()?.theme;
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
|
|
270
|
+
return _getRequestContext()?.setTheme;
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
setLocationState(entries) {
|
|
274
|
+
const reqCtx = _getRequestContext();
|
|
275
|
+
if (!reqCtx) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
"setLocationState() is not available outside a request context",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
reqCtx.setLocationState(entries);
|
|
281
|
+
},
|
|
282
|
+
|
|
223
283
|
reverse:
|
|
224
284
|
reverse ??
|
|
225
|
-
((
|
|
285
|
+
((
|
|
286
|
+
name: string,
|
|
287
|
+
_params?: Record<string, string>,
|
|
288
|
+
_search?: Record<string, unknown>,
|
|
289
|
+
) => {
|
|
226
290
|
throw new Error(
|
|
227
|
-
`ctx.reverse() is not available
|
|
291
|
+
`ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
|
|
228
292
|
);
|
|
229
293
|
}),
|
|
294
|
+
|
|
295
|
+
debugPerformance(): void {
|
|
296
|
+
const reqCtx = _getRequestContext();
|
|
297
|
+
if (reqCtx) {
|
|
298
|
+
reqCtx._debugPerformance = true;
|
|
299
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
230
302
|
};
|
|
231
303
|
}
|
|
232
304
|
|
|
@@ -250,9 +322,16 @@ export function matchMiddleware<TEnv>(
|
|
|
250
322
|
continue;
|
|
251
323
|
}
|
|
252
324
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
325
|
+
// Run the scope regex ONCE per entry. The old code ran test() then, on a
|
|
326
|
+
// hit, extractParams' match() — a second full pass over the same string for
|
|
327
|
+
// every matching entry on the per-request hot path. The regexes carry no
|
|
328
|
+
// `g` flag, so there is no lastIndex statefulness across this single match.
|
|
329
|
+
const m = pathname.match(entry.regex);
|
|
330
|
+
if (m) {
|
|
331
|
+
const params: Record<string, string> = {};
|
|
332
|
+
for (let i = 0; i < entry.paramNames.length; i++) {
|
|
333
|
+
params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
|
|
334
|
+
}
|
|
256
335
|
matches.push({ entry, params });
|
|
257
336
|
}
|
|
258
337
|
}
|
|
@@ -260,15 +339,97 @@ export function matchMiddleware<TEnv>(
|
|
|
260
339
|
return matches;
|
|
261
340
|
}
|
|
262
341
|
|
|
342
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
343
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
344
|
+
// explicit short-circuit Response's own headers win).
|
|
345
|
+
function mergeStubHeaders(
|
|
346
|
+
target: Headers,
|
|
347
|
+
stub: Headers,
|
|
348
|
+
stubOverridesNonCookie: boolean,
|
|
349
|
+
): void {
|
|
350
|
+
stub.forEach((value, name) => {
|
|
351
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
352
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
353
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
354
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
355
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
356
|
+
target.append(name, value);
|
|
357
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
358
|
+
target.set(name, value);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
364
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
365
|
+
function mergeReqCtxStub(
|
|
366
|
+
target: Headers,
|
|
367
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
368
|
+
): void {
|
|
369
|
+
if (!reqCtx) return;
|
|
370
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
371
|
+
if (stubCookies.length > 0) {
|
|
372
|
+
const existing = new Set(target.getSetCookie());
|
|
373
|
+
for (const cookie of stubCookies) {
|
|
374
|
+
if (!existing.has(cookie)) {
|
|
375
|
+
target.append("set-cookie", cookie);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
380
|
+
// Never propagate the reserved external-redirect marker (see mergeStubHeaders).
|
|
381
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
382
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
383
|
+
target.set(name, value);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Clone `base` with stub headers merged into a fresh Headers (the clone keeps
|
|
389
|
+
// the body mutable for post-next() modifications). Set-Cookie is always
|
|
390
|
+
// appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
|
|
391
|
+
// mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
|
|
392
|
+
// short-circuit path passes false (its reqCtx headers are not merged here),
|
|
393
|
+
// which is the one deliberate divergence between the call sites.
|
|
394
|
+
function mergeResponse(
|
|
395
|
+
base: Response,
|
|
396
|
+
stub: Headers,
|
|
397
|
+
opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
|
|
398
|
+
): Response {
|
|
399
|
+
const mergedHeaders = new Headers(base.headers);
|
|
400
|
+
// The reserved external-redirect marker is never a trust signal and must never
|
|
401
|
+
// reach the browser. The guard strips it on 3xx redirects; strip it here too so
|
|
402
|
+
// a forged value cannot ride a non-3xx middleware response (which the 3xx-only
|
|
403
|
+
// guard would not touch) to the client. The opt-in is the out-of-band brand.
|
|
404
|
+
mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
|
|
405
|
+
mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
|
|
406
|
+
if (opts.mergeReqCtx) {
|
|
407
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
408
|
+
}
|
|
409
|
+
const merged = new Response(base.body, {
|
|
410
|
+
status: base.status,
|
|
411
|
+
statusText: base.statusText,
|
|
412
|
+
headers: mergedHeaders,
|
|
413
|
+
});
|
|
414
|
+
// Transfer the out-of-band external-redirect brand across this rebuild: a
|
|
415
|
+
// middleware short-circuit `return redirect(url, { external: true })` reaches
|
|
416
|
+
// the open-redirect guard only after this merge, and the brand lives on the
|
|
417
|
+
// Response object, not in its headers.
|
|
418
|
+
if (isExternalRedirect(base)) {
|
|
419
|
+
markExternalRedirect(merged);
|
|
420
|
+
}
|
|
421
|
+
return merged;
|
|
422
|
+
}
|
|
423
|
+
|
|
263
424
|
/**
|
|
264
425
|
* Execute middleware chain
|
|
265
426
|
*
|
|
266
427
|
* Features:
|
|
267
428
|
* - `await next()` returns actual Response
|
|
268
|
-
* - `ctx.
|
|
269
|
-
* - `ctx.header()` shorthand for setting
|
|
270
|
-
* - Forgiving: if middleware doesn't return, uses
|
|
271
|
-
* - Short-circuit: return Response to stop chain
|
|
429
|
+
* - `ctx.headers` available before and after `await next()`
|
|
430
|
+
* - `ctx.header()` shorthand for setting a single header
|
|
431
|
+
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
432
|
+
* - Short-circuit: return OR throw a Response to stop chain
|
|
272
433
|
* - Error catching: try/catch around `next()` works
|
|
273
434
|
*/
|
|
274
435
|
export async function executeMiddleware<TEnv>(
|
|
@@ -298,40 +459,22 @@ export async function executeMiddleware<TEnv>(
|
|
|
298
459
|
// End of chain - call actual RSC handler
|
|
299
460
|
const response = await finalHandler();
|
|
300
461
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
stubResponse.headers.forEach((value, name) => {
|
|
305
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
306
|
-
mergedHeaders.append(name, value);
|
|
307
|
-
} else {
|
|
308
|
-
mergedHeaders.set(name, value);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
312
|
-
// Set-Cookie duplication is prevented by createResponseWithMergedHeaders
|
|
313
|
-
// draining Set-Cookie from ctx.res after merging (helpers.ts).
|
|
314
|
-
const reqCtx = _getRequestContext();
|
|
315
|
-
if (reqCtx) {
|
|
316
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
317
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
318
|
-
mergedHeaders.append(name, value);
|
|
319
|
-
} else if (!mergedHeaders.has(name)) {
|
|
320
|
-
mergedHeaders.set(name, value);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
462
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
463
|
+
responseHolder.response = response;
|
|
464
|
+
return response;
|
|
323
465
|
}
|
|
324
466
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
467
|
+
// Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
|
|
468
|
+
// and reqCtx stub headers are merged in.
|
|
469
|
+
responseHolder.response = mergeResponse(response, stubResponse.headers, {
|
|
470
|
+
stubOverridesNonCookie: true,
|
|
471
|
+
mergeReqCtx: true,
|
|
330
472
|
});
|
|
331
473
|
|
|
332
474
|
return responseHolder.response;
|
|
333
475
|
}
|
|
334
476
|
|
|
477
|
+
const middlewareOrdinal = index;
|
|
335
478
|
const { entry, params } = middlewares[index++];
|
|
336
479
|
const ctx = createMiddlewareContext(
|
|
337
480
|
request,
|
|
@@ -341,71 +484,102 @@ export async function executeMiddleware<TEnv>(
|
|
|
341
484
|
responseHolder,
|
|
342
485
|
reverse,
|
|
343
486
|
);
|
|
487
|
+
const metricStart = performance.now();
|
|
488
|
+
const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
|
|
489
|
+
let middlewareFinished = false;
|
|
490
|
+
const finishMiddleware = () => {
|
|
491
|
+
if (!middlewareFinished) {
|
|
492
|
+
middlewareFinished = true;
|
|
493
|
+
appendMetric(
|
|
494
|
+
_getRequestContext()?._metricsStore,
|
|
495
|
+
`${metricLabel}:pre`,
|
|
496
|
+
metricStart,
|
|
497
|
+
performance.now() - metricStart,
|
|
498
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
344
502
|
|
|
345
503
|
// Track if next() was called and capture its Promise.
|
|
346
504
|
// Guard against double-calling: a second call would re-enter the
|
|
347
505
|
// downstream chain and overwrite responseHolder.response.
|
|
348
506
|
let nextPromise: Promise<Response> | null = null;
|
|
507
|
+
let nextResolvedAt: number | undefined;
|
|
349
508
|
const wrappedNext = (): Promise<Response> => {
|
|
350
509
|
if (nextPromise) {
|
|
351
510
|
throw new Error(
|
|
352
511
|
`[@rangojs/router] Middleware called next() more than once.`,
|
|
353
512
|
);
|
|
354
513
|
}
|
|
355
|
-
|
|
514
|
+
finishMiddleware();
|
|
515
|
+
const downstream = next();
|
|
516
|
+
nextPromise = downstream.then(
|
|
517
|
+
(res) => {
|
|
518
|
+
nextResolvedAt = performance.now();
|
|
519
|
+
return res;
|
|
520
|
+
},
|
|
521
|
+
(err) => {
|
|
522
|
+
nextResolvedAt = performance.now();
|
|
523
|
+
throw err;
|
|
524
|
+
},
|
|
525
|
+
);
|
|
356
526
|
return nextPromise;
|
|
357
527
|
};
|
|
358
528
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
529
|
+
// Wrap the middleware (including its downstream next() chain) in its span
|
|
530
|
+
// via the unified phase API. metric:false — the middleware's perf metric is
|
|
531
|
+
// its exclusive pre/post own-time, recorded directly above and below, finer
|
|
532
|
+
// than a single wrap. Spans nest by async context, so this onions
|
|
533
|
+
// middleware-over-middleware and the core handler underneath. Pass-through
|
|
534
|
+
// when neither surface is active.
|
|
535
|
+
let result: Response | void;
|
|
536
|
+
try {
|
|
537
|
+
result = await observePhase(PHASES.middleware(metricLabel), () =>
|
|
538
|
+
entry.handler(ctx, wrappedNext),
|
|
539
|
+
);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
// Thrown Response is short-circuit control flow, not an error.
|
|
542
|
+
// Fall through to the `if (result instanceof Response)` branch below
|
|
543
|
+
// so stub headers and request-context cookies merge as they do for
|
|
544
|
+
// an explicit `return new Response(...)`. Real errors propagate.
|
|
545
|
+
if (error instanceof Response) {
|
|
546
|
+
result = error;
|
|
547
|
+
} else {
|
|
548
|
+
finishMiddleware();
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
finishMiddleware();
|
|
553
|
+
|
|
554
|
+
// Record post-next() processing time when middleware did work after
|
|
555
|
+
// the downstream chain resolved (e.g. adding headers, logging).
|
|
556
|
+
if (nextResolvedAt !== undefined) {
|
|
557
|
+
const postDur = performance.now() - nextResolvedAt;
|
|
558
|
+
if (postDur > POST_METRIC_MIN_DURATION_MS) {
|
|
559
|
+
appendMetric(
|
|
560
|
+
_getRequestContext()?._metricsStore,
|
|
561
|
+
`${metricLabel}:post`,
|
|
562
|
+
nextResolvedAt,
|
|
563
|
+
postDur,
|
|
564
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
367
567
|
}
|
|
368
|
-
|
|
369
|
-
const result = await entry.handler(ctx, wrappedNext);
|
|
370
568
|
|
|
371
569
|
// Explicit return takes precedence (middleware short-circuit).
|
|
372
570
|
// Merge stub headers (from ctx.header before this point) and
|
|
373
|
-
// RequestContext stub headers (from
|
|
571
|
+
// RequestContext stub headers (from cookies().set()) into the
|
|
374
572
|
// returned Response so they are not lost.
|
|
375
573
|
if (result instanceof Response) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
ctxSetCalled &&
|
|
380
|
-
result.status >= 300 &&
|
|
381
|
-
result.status < 400
|
|
382
|
-
) {
|
|
383
|
-
warnCtxSetBeforeRedirect(entry.handler);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const mergedHeaders = new Headers(result.headers);
|
|
387
|
-
stubResponse.headers.forEach((value, name) => {
|
|
388
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
389
|
-
mergedHeaders.append(name, value);
|
|
390
|
-
} else if (!mergedHeaders.has(name)) {
|
|
391
|
-
mergedHeaders.set(name, value);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
// Also merge shared RequestContext stub (cookies written via setCookie)
|
|
395
|
-
const reqCtx = _getRequestContext();
|
|
396
|
-
if (reqCtx) {
|
|
397
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
398
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
399
|
-
mergedHeaders.append(name, value);
|
|
400
|
-
} else if (!mergedHeaders.has(name)) {
|
|
401
|
-
mergedHeaders.set(name, value);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
574
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
575
|
+
responseHolder.response = result;
|
|
576
|
+
return result;
|
|
404
577
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
578
|
+
// Explicit short-circuit: the returned Response's own headers win
|
|
579
|
+
// (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
|
|
580
|
+
const merged = mergeResponse(result, stubResponse.headers, {
|
|
581
|
+
stubOverridesNonCookie: false,
|
|
582
|
+
mergeReqCtx: true,
|
|
409
583
|
});
|
|
410
584
|
responseHolder.response = merged;
|
|
411
585
|
return merged;
|
|
@@ -424,19 +598,6 @@ export async function executeMiddleware<TEnv>(
|
|
|
424
598
|
// If middleware called next(), await it and return the response
|
|
425
599
|
if (nextPromise) {
|
|
426
600
|
await nextPromise;
|
|
427
|
-
|
|
428
|
-
// W5: warn if ctx.set() was called but the downstream response is a redirect.
|
|
429
|
-
// The ctx.set() values will be lost because the redirect navigates away.
|
|
430
|
-
if (
|
|
431
|
-
process.env.NODE_ENV !== "production" &&
|
|
432
|
-
ctxSetCalled &&
|
|
433
|
-
responseHolder.response &&
|
|
434
|
-
responseHolder.response.status >= 300 &&
|
|
435
|
-
responseHolder.response.status < 400
|
|
436
|
-
) {
|
|
437
|
-
warnCtxSetBeforeRedirect(entry.handler);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
601
|
return responseHolder.response!;
|
|
441
602
|
}
|
|
442
603
|
|
|
@@ -459,6 +620,18 @@ export async function executeMiddleware<TEnv>(
|
|
|
459
620
|
throw new Error("No response generated by middleware chain");
|
|
460
621
|
}
|
|
461
622
|
|
|
623
|
+
// Final re-merge: capture any RequestContext stub headers added after the
|
|
624
|
+
// last merge point (e.g. cookies().set() called after await next()).
|
|
625
|
+
// The reqCtx stub may have already been partially merged during finalHandler
|
|
626
|
+
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
627
|
+
//
|
|
628
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
629
|
+
// set-cookie on an upgrade is not meaningful.
|
|
630
|
+
const reqCtx = _getRequestContext();
|
|
631
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
632
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
633
|
+
}
|
|
634
|
+
|
|
462
635
|
return finalResponse;
|
|
463
636
|
}
|
|
464
637
|
|
|
@@ -467,7 +640,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
467
640
|
*
|
|
468
641
|
* Intercepts use a shared stubResponse from the request context. This function:
|
|
469
642
|
* - Runs middleware in sequence with a simple next() chain
|
|
470
|
-
* - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
|
|
643
|
+
* - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
|
|
471
644
|
* - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
|
|
472
645
|
*
|
|
473
646
|
* @param middlewares - Array of middleware functions
|
|
@@ -505,6 +678,7 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
505
678
|
return stubResponse;
|
|
506
679
|
}
|
|
507
680
|
|
|
681
|
+
const ordinal = index;
|
|
508
682
|
const middleware = middlewares[index++];
|
|
509
683
|
const ctx = createMiddlewareContext(
|
|
510
684
|
request,
|
|
@@ -526,7 +700,29 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
526
700
|
return next();
|
|
527
701
|
};
|
|
528
702
|
|
|
529
|
-
|
|
703
|
+
// Span-wrap each intercept middleware as rango.middleware (metric:false —
|
|
704
|
+
// intercept runs inside the render phase already metered by render:total, so
|
|
705
|
+
// it contributes a span but no separate perf metric). Bare MiddlewareFns
|
|
706
|
+
// have no pattern, so the label is scoped to "*" like a pattern-less entry.
|
|
707
|
+
const label = getMiddlewareMetricLabel(
|
|
708
|
+
{ handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
|
|
709
|
+
ordinal,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
let result: Response | void;
|
|
713
|
+
try {
|
|
714
|
+
result = await observePhase(PHASES.middleware(label), () =>
|
|
715
|
+
middleware(ctx, guardedNext),
|
|
716
|
+
);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
719
|
+
// explicit-return path below. Real errors propagate.
|
|
720
|
+
if (error instanceof Response) {
|
|
721
|
+
result = error;
|
|
722
|
+
} else {
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
530
726
|
|
|
531
727
|
if (result instanceof Response) {
|
|
532
728
|
earlyResponse = result;
|
|
@@ -550,21 +746,13 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
550
746
|
});
|
|
551
747
|
|
|
552
748
|
if (hasStubHeaders) {
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
stubResponse.headers
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
} else if (!mergedHeaders.has(name)) {
|
|
561
|
-
mergedHeaders.set(name, value);
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
return new Response(response.body, {
|
|
565
|
-
status: response.status,
|
|
566
|
-
statusText: response.statusText,
|
|
567
|
-
headers: mergedHeaders,
|
|
749
|
+
// Only fill in missing headers — the returned Response's explicit headers
|
|
750
|
+
// take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
|
|
751
|
+
// mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
|
|
752
|
+
// stub headers here (pinned by intercept-middleware-headers.test.ts).
|
|
753
|
+
return mergeResponse(response, stubResponse.headers, {
|
|
754
|
+
stubOverridesNonCookie: false,
|
|
755
|
+
mergeReqCtx: false,
|
|
568
756
|
});
|
|
569
757
|
}
|
|
570
758
|
return response;
|
|
@@ -605,7 +793,6 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
605
793
|
regex: null,
|
|
606
794
|
paramNames: [],
|
|
607
795
|
handler,
|
|
608
|
-
mountPrefix: null,
|
|
609
796
|
} as MiddlewareEntry<TEnv>,
|
|
610
797
|
params,
|
|
611
798
|
}));
|