@rangojs/router 0.0.0-experimental.0f44aca1
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 +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- 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 +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,748 @@
|
|
|
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
|
+
|
|
12
|
+
import { contextGet, contextSet } from "../context-var.js";
|
|
13
|
+
import type {
|
|
14
|
+
CollectedMiddleware,
|
|
15
|
+
MiddlewareCollectableEntry,
|
|
16
|
+
MiddlewareContext,
|
|
17
|
+
MiddlewareEntry,
|
|
18
|
+
MiddlewareFn,
|
|
19
|
+
ResponseHolder,
|
|
20
|
+
} from "./middleware-types.js";
|
|
21
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
22
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
|
+
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
|
+
|
|
25
|
+
// Re-export types and cookie utilities for backward compatibility
|
|
26
|
+
export type {
|
|
27
|
+
CookieOptions,
|
|
28
|
+
CollectedMiddleware,
|
|
29
|
+
MiddlewareCollectableEntry,
|
|
30
|
+
MiddlewareContext,
|
|
31
|
+
MiddlewareEntry,
|
|
32
|
+
MiddlewareFn,
|
|
33
|
+
ResponseHolder,
|
|
34
|
+
} from "./middleware-types.js";
|
|
35
|
+
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
36
|
+
|
|
37
|
+
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
38
|
+
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
39
|
+
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
40
|
+
|
|
41
|
+
function getMiddlewareMetricBase<TEnv>(
|
|
42
|
+
entry: MiddlewareEntry<TEnv>,
|
|
43
|
+
ordinal: number,
|
|
44
|
+
): string {
|
|
45
|
+
const handlerName = entry.handler.name?.trim();
|
|
46
|
+
const scope = entry.pattern ?? "*";
|
|
47
|
+
|
|
48
|
+
if (handlerName) {
|
|
49
|
+
return `${handlerName}@${scope}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${scope}#${ordinal + 1}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
56
|
+
entry: MiddlewareEntry<TEnv>,
|
|
57
|
+
ordinal: number,
|
|
58
|
+
): string {
|
|
59
|
+
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a route pattern into regex and param names
|
|
64
|
+
* Supports: *, /path, /path/*, /path/:param, /path/:param/*
|
|
65
|
+
*/
|
|
66
|
+
export function parsePattern(pattern: string): {
|
|
67
|
+
regex: RegExp;
|
|
68
|
+
paramNames: string[];
|
|
69
|
+
} {
|
|
70
|
+
if (pattern === "*") {
|
|
71
|
+
return { regex: /^.*$/, paramNames: [] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const paramNames: string[] = [];
|
|
75
|
+
let regexStr = "^";
|
|
76
|
+
|
|
77
|
+
const parts = pattern.split("/").filter(Boolean);
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < parts.length; i++) {
|
|
80
|
+
const part = parts[i];
|
|
81
|
+
|
|
82
|
+
if (part === "*") {
|
|
83
|
+
// Wildcard - match rest of path
|
|
84
|
+
regexStr += "(?:/.*)?";
|
|
85
|
+
} else if (part.startsWith(":")) {
|
|
86
|
+
// Param
|
|
87
|
+
const paramName = part.slice(1);
|
|
88
|
+
paramNames.push(paramName);
|
|
89
|
+
regexStr += "/([^/]+)";
|
|
90
|
+
} else {
|
|
91
|
+
// Literal
|
|
92
|
+
regexStr += "/" + escapeRegex(part);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If pattern doesn't end with *, match exact or with trailing segments
|
|
97
|
+
if (!pattern.endsWith("*")) {
|
|
98
|
+
regexStr += "/?$";
|
|
99
|
+
} else {
|
|
100
|
+
regexStr += "$";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { regex: new RegExp(regexStr), paramNames };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Escape special regex characters
|
|
108
|
+
*/
|
|
109
|
+
function escapeRegex(str: string): string {
|
|
110
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract params from a pathname using a pattern's regex and param names
|
|
115
|
+
*/
|
|
116
|
+
export function extractParams(
|
|
117
|
+
pathname: string,
|
|
118
|
+
regex: RegExp,
|
|
119
|
+
paramNames: string[],
|
|
120
|
+
): Record<string, string> {
|
|
121
|
+
const match = pathname.match(regex);
|
|
122
|
+
if (!match) return {};
|
|
123
|
+
|
|
124
|
+
const params: Record<string, string> = {};
|
|
125
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
126
|
+
params[paramNames[i]] = match[i + 1] || "";
|
|
127
|
+
}
|
|
128
|
+
return params;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create middleware context
|
|
133
|
+
*
|
|
134
|
+
* Note: The implementation uses runtime values while the interface provides
|
|
135
|
+
* compile-time type safety. The env/get/set types are resolved at call sites
|
|
136
|
+
* via conditional types based on TEnv from createRouter<TBindings>().
|
|
137
|
+
*/
|
|
138
|
+
export function createMiddlewareContext<TEnv>(
|
|
139
|
+
request: Request,
|
|
140
|
+
env: TEnv,
|
|
141
|
+
params: Record<string, string>,
|
|
142
|
+
variables: Record<string, unknown>,
|
|
143
|
+
responseHolder: ResponseHolder,
|
|
144
|
+
reverse?: (
|
|
145
|
+
name: string,
|
|
146
|
+
params?: Record<string, string>,
|
|
147
|
+
search?: Record<string, unknown>,
|
|
148
|
+
) => string,
|
|
149
|
+
): MiddlewareContext<TEnv> {
|
|
150
|
+
const url = new URL(request.url);
|
|
151
|
+
|
|
152
|
+
// Track the initial response to detect pre/post-next() phase.
|
|
153
|
+
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
154
|
+
// After next(): responseHolder.response is the real downstream response.
|
|
155
|
+
const initialResponse = responseHolder.response;
|
|
156
|
+
const isPreNext = () => responseHolder.response === initialResponse;
|
|
157
|
+
|
|
158
|
+
// Delegation strategy for RequestContext (reqCtx):
|
|
159
|
+
// - res getter: before next() returns shared reqCtx stub; after next() returns
|
|
160
|
+
// the real downstream response.
|
|
161
|
+
// - header(): before next() delegates to reqCtx; after next() writes to the
|
|
162
|
+
// real downstream response.
|
|
163
|
+
// Cookie operations are handled by the standalone cookies() function which
|
|
164
|
+
// delegates to the shared RequestContext internally.
|
|
165
|
+
// The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
|
|
166
|
+
// Internal helper: resolve the current response (stub before next(), real after).
|
|
167
|
+
// Not exposed on the public MiddlewareContext type — use ctx.headers instead.
|
|
168
|
+
const getResponse = (): Response => {
|
|
169
|
+
if (isPreNext()) {
|
|
170
|
+
const reqCtx = _getRequestContext();
|
|
171
|
+
if (reqCtx) return reqCtx.res;
|
|
172
|
+
}
|
|
173
|
+
if (!responseHolder.response) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
"Response is not available - responseHolder was not initialized",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return responseHolder.response;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
request,
|
|
183
|
+
url,
|
|
184
|
+
originalUrl: new URL(request.url),
|
|
185
|
+
pathname: url.pathname,
|
|
186
|
+
searchParams: url.searchParams,
|
|
187
|
+
env: env as MiddlewareContext<TEnv>["env"],
|
|
188
|
+
params,
|
|
189
|
+
// Getter: re-derives from request context on each access so that global
|
|
190
|
+
// middleware sees the matched route name after await next().
|
|
191
|
+
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
192
|
+
const reqCtx = _getRequestContext();
|
|
193
|
+
const raw = reqCtx?._routeName;
|
|
194
|
+
return (
|
|
195
|
+
raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
|
|
196
|
+
) as MiddlewareContext<TEnv>["routeName"];
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
get headers(): Headers {
|
|
200
|
+
return getResponse().headers;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
get: ((keyOrVar: any) =>
|
|
204
|
+
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
205
|
+
|
|
206
|
+
set: ((keyOrVar: any, value: unknown) => {
|
|
207
|
+
contextSet(variables, keyOrVar, value);
|
|
208
|
+
}) as MiddlewareContext<TEnv>["set"],
|
|
209
|
+
|
|
210
|
+
var: variables as MiddlewareContext<TEnv>["var"],
|
|
211
|
+
|
|
212
|
+
header(name: string, value: string): void {
|
|
213
|
+
// Before next(): delegate to shared RequestContext stub
|
|
214
|
+
if (isPreNext()) {
|
|
215
|
+
const reqCtx = _getRequestContext();
|
|
216
|
+
if (reqCtx) {
|
|
217
|
+
reqCtx.header(name, value);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// After next() or standalone: write to current response
|
|
222
|
+
if (!responseHolder.response) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
"ctx.header() is not available - responseHolder was not initialized",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
responseHolder.response.headers.set(name, value);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
get theme(): MiddlewareContext<TEnv>["theme"] {
|
|
231
|
+
return _getRequestContext()?.theme;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
|
|
235
|
+
return _getRequestContext()?.setTheme;
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
setLocationState(entries) {
|
|
239
|
+
const reqCtx = _getRequestContext();
|
|
240
|
+
if (!reqCtx) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"setLocationState() is not available outside a request context",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
reqCtx.setLocationState(entries);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
reverse:
|
|
249
|
+
reverse ??
|
|
250
|
+
((name: string) => {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`ctx.reverse() is not available - route map was not provided to middleware context`,
|
|
253
|
+
);
|
|
254
|
+
}),
|
|
255
|
+
|
|
256
|
+
debugPerformance(): void {
|
|
257
|
+
const reqCtx = _getRequestContext();
|
|
258
|
+
if (reqCtx) {
|
|
259
|
+
reqCtx._debugPerformance = true;
|
|
260
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Match middleware entries against a pathname
|
|
268
|
+
* Returns entries that match, with extracted params
|
|
269
|
+
*/
|
|
270
|
+
export function matchMiddleware<TEnv>(
|
|
271
|
+
pathname: string,
|
|
272
|
+
entries: MiddlewareEntry<TEnv>[],
|
|
273
|
+
): Array<{ entry: MiddlewareEntry<TEnv>; params: Record<string, string> }> {
|
|
274
|
+
const matches: Array<{
|
|
275
|
+
entry: MiddlewareEntry<TEnv>;
|
|
276
|
+
params: Record<string, string>;
|
|
277
|
+
}> = [];
|
|
278
|
+
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
// No pattern = matches all (global middleware without pattern)
|
|
281
|
+
if (!entry.regex) {
|
|
282
|
+
matches.push({ entry, params: {} });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check if pathname matches
|
|
287
|
+
if (entry.regex.test(pathname)) {
|
|
288
|
+
const params = extractParams(pathname, entry.regex, entry.paramNames);
|
|
289
|
+
matches.push({ entry, params });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return matches;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Execute middleware chain
|
|
298
|
+
*
|
|
299
|
+
* Features:
|
|
300
|
+
* - `await next()` returns actual Response
|
|
301
|
+
* - `ctx.headers` available before and after `await next()`
|
|
302
|
+
* - `ctx.header()` shorthand for setting a single header
|
|
303
|
+
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
304
|
+
* - Short-circuit: return Response to stop chain
|
|
305
|
+
* - Error catching: try/catch around `next()` works
|
|
306
|
+
*/
|
|
307
|
+
export async function executeMiddleware<TEnv>(
|
|
308
|
+
middlewares: Array<{
|
|
309
|
+
entry: MiddlewareEntry<TEnv>;
|
|
310
|
+
params: Record<string, string>;
|
|
311
|
+
}>,
|
|
312
|
+
request: Request,
|
|
313
|
+
env: TEnv,
|
|
314
|
+
variables: Record<string, any>,
|
|
315
|
+
finalHandler: () => Promise<Response>,
|
|
316
|
+
reverse?: (
|
|
317
|
+
name: string,
|
|
318
|
+
params?: Record<string, string>,
|
|
319
|
+
search?: Record<string, unknown>,
|
|
320
|
+
) => string,
|
|
321
|
+
): Promise<Response> {
|
|
322
|
+
let index = 0;
|
|
323
|
+
|
|
324
|
+
// Create a stub response that's available immediately
|
|
325
|
+
// This allows middleware to set headers/cookies before calling next()
|
|
326
|
+
const stubResponse = new Response(null, { status: 200 });
|
|
327
|
+
const responseHolder: ResponseHolder = { response: stubResponse };
|
|
328
|
+
|
|
329
|
+
const next = async (): Promise<Response> => {
|
|
330
|
+
if (index >= middlewares.length) {
|
|
331
|
+
// End of chain - call actual RSC handler
|
|
332
|
+
const response = await finalHandler();
|
|
333
|
+
|
|
334
|
+
// Merge headers set on stub into the real response.
|
|
335
|
+
// Use append for Set-Cookie to preserve multiple cookies.
|
|
336
|
+
const mergedHeaders = new Headers(response.headers);
|
|
337
|
+
stubResponse.headers.forEach((value, name) => {
|
|
338
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
339
|
+
mergedHeaders.append(name, value);
|
|
340
|
+
} else {
|
|
341
|
+
mergedHeaders.set(name, value);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
345
|
+
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
346
|
+
// may have already merged the same reqCtx cookies into the response.
|
|
347
|
+
const reqCtx = _getRequestContext();
|
|
348
|
+
if (reqCtx) {
|
|
349
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
350
|
+
if (stubCookies.length > 0) {
|
|
351
|
+
const existing = new Set(mergedHeaders.getSetCookie());
|
|
352
|
+
for (const cookie of stubCookies) {
|
|
353
|
+
if (!existing.has(cookie)) {
|
|
354
|
+
mergedHeaders.append("set-cookie", cookie);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
359
|
+
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
360
|
+
mergedHeaders.set(name, value);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Clone response with merged headers (mutable for post-next() modifications)
|
|
366
|
+
responseHolder.response = new Response(response.body, {
|
|
367
|
+
status: response.status,
|
|
368
|
+
statusText: response.statusText,
|
|
369
|
+
headers: mergedHeaders,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return responseHolder.response;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const middlewareOrdinal = index;
|
|
376
|
+
const { entry, params } = middlewares[index++];
|
|
377
|
+
const ctx = createMiddlewareContext(
|
|
378
|
+
request,
|
|
379
|
+
env,
|
|
380
|
+
params,
|
|
381
|
+
variables,
|
|
382
|
+
responseHolder,
|
|
383
|
+
reverse,
|
|
384
|
+
);
|
|
385
|
+
const metricStart = performance.now();
|
|
386
|
+
const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
|
|
387
|
+
let middlewareFinished = false;
|
|
388
|
+
const finishMiddleware = () => {
|
|
389
|
+
if (!middlewareFinished) {
|
|
390
|
+
middlewareFinished = true;
|
|
391
|
+
appendMetric(
|
|
392
|
+
_getRequestContext()?._metricsStore,
|
|
393
|
+
`${metricLabel}:pre`,
|
|
394
|
+
metricStart,
|
|
395
|
+
performance.now() - metricStart,
|
|
396
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Track if next() was called and capture its Promise.
|
|
402
|
+
// Guard against double-calling: a second call would re-enter the
|
|
403
|
+
// downstream chain and overwrite responseHolder.response.
|
|
404
|
+
let nextPromise: Promise<Response> | null = null;
|
|
405
|
+
let nextResolvedAt: number | undefined;
|
|
406
|
+
const wrappedNext = (): Promise<Response> => {
|
|
407
|
+
if (nextPromise) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`[@rangojs/router] Middleware called next() more than once.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
finishMiddleware();
|
|
413
|
+
const downstream = next();
|
|
414
|
+
nextPromise = downstream.then(
|
|
415
|
+
(res) => {
|
|
416
|
+
nextResolvedAt = performance.now();
|
|
417
|
+
return res;
|
|
418
|
+
},
|
|
419
|
+
(err) => {
|
|
420
|
+
nextResolvedAt = performance.now();
|
|
421
|
+
throw err;
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
return nextPromise;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
let result: Response | void;
|
|
428
|
+
try {
|
|
429
|
+
result = await entry.handler(ctx, wrappedNext);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
finishMiddleware();
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
finishMiddleware();
|
|
435
|
+
|
|
436
|
+
// Record post-next() processing time when middleware did work after
|
|
437
|
+
// the downstream chain resolved (e.g. adding headers, logging).
|
|
438
|
+
if (nextResolvedAt !== undefined) {
|
|
439
|
+
const postDur = performance.now() - nextResolvedAt;
|
|
440
|
+
if (postDur > POST_METRIC_MIN_DURATION_MS) {
|
|
441
|
+
appendMetric(
|
|
442
|
+
_getRequestContext()?._metricsStore,
|
|
443
|
+
`${metricLabel}:post`,
|
|
444
|
+
nextResolvedAt,
|
|
445
|
+
postDur,
|
|
446
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Explicit return takes precedence (middleware short-circuit).
|
|
452
|
+
// Merge stub headers (from ctx.header before this point) and
|
|
453
|
+
// RequestContext stub headers (from ctx.setCookie) into the
|
|
454
|
+
// returned Response so they are not lost.
|
|
455
|
+
if (result instanceof Response) {
|
|
456
|
+
const mergedHeaders = new Headers(result.headers);
|
|
457
|
+
stubResponse.headers.forEach((value, name) => {
|
|
458
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
459
|
+
mergedHeaders.append(name, value);
|
|
460
|
+
} else if (!mergedHeaders.has(name)) {
|
|
461
|
+
mergedHeaders.set(name, value);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
465
|
+
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
466
|
+
// may have already merged the same reqCtx cookies into the response.
|
|
467
|
+
const reqCtx = _getRequestContext();
|
|
468
|
+
if (reqCtx) {
|
|
469
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
470
|
+
if (stubCookies.length > 0) {
|
|
471
|
+
const existing = new Set(mergedHeaders.getSetCookie());
|
|
472
|
+
for (const cookie of stubCookies) {
|
|
473
|
+
if (!existing.has(cookie)) {
|
|
474
|
+
mergedHeaders.append("set-cookie", cookie);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
479
|
+
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
480
|
+
mergedHeaders.set(name, value);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
const merged = new Response(result.body, {
|
|
485
|
+
status: result.status,
|
|
486
|
+
statusText: result.statusText,
|
|
487
|
+
headers: mergedHeaders,
|
|
488
|
+
});
|
|
489
|
+
responseHolder.response = merged;
|
|
490
|
+
return merged;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Warn about unexpected return values (non-Response, non-undefined)
|
|
494
|
+
// This catches common mistakes like returning strings or objects
|
|
495
|
+
if (result !== undefined) {
|
|
496
|
+
const fnName = entry.handler.name || "(anonymous)";
|
|
497
|
+
console.warn(
|
|
498
|
+
`[Middleware] "${fnName}" returned ${typeof result} instead of Response or undefined. ` +
|
|
499
|
+
`This return value will be ignored. Did you mean to return a Response?`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// If middleware called next(), await it and return the response
|
|
504
|
+
if (nextPromise) {
|
|
505
|
+
await nextPromise;
|
|
506
|
+
return responseHolder.response!;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Middleware didn't call next() and didn't return a Response - that's an error
|
|
510
|
+
// (Note: responseHolder.response is the stub, but we require next() or explicit return)
|
|
511
|
+
const fnName = entry.handler.name || "(anonymous)";
|
|
512
|
+
throw new Error(
|
|
513
|
+
`Middleware must call next() or return a Response. ` +
|
|
514
|
+
`Function: ${fnName}, Pattern: ${entry.pattern ?? "(all)"}
|
|
515
|
+
Source: ${import.meta.env.DEV ? entry.handler.toString().slice(0, 200) : "(source hidden in production)"}`,
|
|
516
|
+
{ cause: { url: request.url, fn: entry.handler } },
|
|
517
|
+
);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
await next();
|
|
521
|
+
|
|
522
|
+
// Use the final response from responseHolder (may have been modified by middleware)
|
|
523
|
+
const finalResponse = responseHolder.response;
|
|
524
|
+
if (!finalResponse) {
|
|
525
|
+
throw new Error("No response generated by middleware chain");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Final re-merge: capture any RequestContext stub headers added after the
|
|
529
|
+
// last merge point (e.g. cookies().set() called after await next()).
|
|
530
|
+
// The reqCtx stub may have already been partially merged during finalHandler
|
|
531
|
+
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
532
|
+
const reqCtx = _getRequestContext();
|
|
533
|
+
if (reqCtx) {
|
|
534
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
535
|
+
if (stubCookies.length > 0) {
|
|
536
|
+
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
537
|
+
for (const cookie of stubCookies) {
|
|
538
|
+
if (!existingCookies.has(cookie)) {
|
|
539
|
+
finalResponse.headers.append("set-cookie", cookie);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Fill in non-cookie headers that aren't already on the response
|
|
544
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
545
|
+
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
546
|
+
finalResponse.headers.set(name, value);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return finalResponse;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Execute middleware for intercepts (simplified execution)
|
|
556
|
+
*
|
|
557
|
+
* Intercepts use a shared stubResponse from the request context. This function:
|
|
558
|
+
* - Runs middleware in sequence with a simple next() chain
|
|
559
|
+
* - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
|
|
560
|
+
* - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
|
|
561
|
+
*
|
|
562
|
+
* @param middlewares - Array of middleware functions
|
|
563
|
+
* @param request - Original request
|
|
564
|
+
* @param env - Environment bindings
|
|
565
|
+
* @param params - Route params
|
|
566
|
+
* @param variables - Shared variables object
|
|
567
|
+
* @param stubResponse - Response from request context for collecting headers/cookies
|
|
568
|
+
*/
|
|
569
|
+
export async function executeInterceptMiddleware<TEnv>(
|
|
570
|
+
middlewares: MiddlewareFn<TEnv>[],
|
|
571
|
+
request: Request,
|
|
572
|
+
env: TEnv,
|
|
573
|
+
params: Record<string, string>,
|
|
574
|
+
variables: Record<string, any>,
|
|
575
|
+
stubResponse: Response,
|
|
576
|
+
reverse?: (
|
|
577
|
+
name: string,
|
|
578
|
+
params?: Record<string, string>,
|
|
579
|
+
search?: Record<string, unknown>,
|
|
580
|
+
) => string,
|
|
581
|
+
): Promise<Response | null> {
|
|
582
|
+
if (middlewares.length === 0) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let index = 0;
|
|
587
|
+
let earlyResponse: Response | null = null;
|
|
588
|
+
|
|
589
|
+
// Use provided stubResponse - headers/cookies set here will be merged by the caller
|
|
590
|
+
const responseHolder: ResponseHolder = { response: stubResponse };
|
|
591
|
+
|
|
592
|
+
const next = async (): Promise<Response> => {
|
|
593
|
+
if (index >= middlewares.length || earlyResponse) {
|
|
594
|
+
return stubResponse;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const middleware = middlewares[index++];
|
|
598
|
+
const ctx = createMiddlewareContext(
|
|
599
|
+
request,
|
|
600
|
+
env,
|
|
601
|
+
params,
|
|
602
|
+
variables,
|
|
603
|
+
responseHolder,
|
|
604
|
+
reverse,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
let nextCalled = false;
|
|
608
|
+
const guardedNext = (): Promise<Response> => {
|
|
609
|
+
if (nextCalled) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`[@rangojs/router] Intercept middleware called next() more than once.`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
nextCalled = true;
|
|
615
|
+
return next();
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const result = await middleware(ctx, guardedNext);
|
|
619
|
+
|
|
620
|
+
if (result instanceof Response) {
|
|
621
|
+
earlyResponse = result;
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return stubResponse;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
await next();
|
|
629
|
+
|
|
630
|
+
// Return early response if middleware short-circuited (returned Response BEFORE next())
|
|
631
|
+
if (earlyResponse) {
|
|
632
|
+
// Capture in const for TypeScript narrowing (earlyResponse is `let` which loses narrowing in callbacks)
|
|
633
|
+
const response: Response = earlyResponse;
|
|
634
|
+
|
|
635
|
+
// Merge any headers/cookies set on stub into the early response
|
|
636
|
+
let hasStubHeaders = false;
|
|
637
|
+
stubResponse.headers.forEach(() => {
|
|
638
|
+
hasStubHeaders = true;
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
if (hasStubHeaders) {
|
|
642
|
+
// Clone and merge headers from stub into early response.
|
|
643
|
+
// Only fill in missing headers — the returned Response's explicit
|
|
644
|
+
// headers take precedence, matching executeMiddleware behavior.
|
|
645
|
+
const mergedHeaders = new Headers(response.headers);
|
|
646
|
+
stubResponse.headers.forEach((value, name) => {
|
|
647
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
648
|
+
mergedHeaders.append(name, value);
|
|
649
|
+
} else if (!mergedHeaders.has(name)) {
|
|
650
|
+
mergedHeaders.set(name, value);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return new Response(response.body, {
|
|
654
|
+
status: response.status,
|
|
655
|
+
statusText: response.statusText,
|
|
656
|
+
headers: mergedHeaders,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return response;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// All middleware completed without short-circuit
|
|
663
|
+
// Headers/cookies set on stubResponse will be merged into the final response by the caller
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Execute middleware chain for loaders (simpler signature)
|
|
669
|
+
*
|
|
670
|
+
* Takes an array of MiddlewareFn directly (no entry wrapper needed).
|
|
671
|
+
* Used for fetchable loader middleware execution.
|
|
672
|
+
*/
|
|
673
|
+
export async function executeLoaderMiddleware<TEnv>(
|
|
674
|
+
middlewares: MiddlewareFn<TEnv>[],
|
|
675
|
+
request: Request,
|
|
676
|
+
env: TEnv,
|
|
677
|
+
params: Record<string, string>,
|
|
678
|
+
variables: Record<string, any>,
|
|
679
|
+
finalHandler: () => Promise<Response>,
|
|
680
|
+
reverse?: (
|
|
681
|
+
name: string,
|
|
682
|
+
params?: Record<string, string>,
|
|
683
|
+
search?: Record<string, unknown>,
|
|
684
|
+
) => string,
|
|
685
|
+
): Promise<Response> {
|
|
686
|
+
if (middlewares.length === 0) {
|
|
687
|
+
return finalHandler();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Convert to the format executeMiddleware expects
|
|
691
|
+
const middlewareEntries = middlewares.map((handler) => ({
|
|
692
|
+
entry: {
|
|
693
|
+
pattern: null,
|
|
694
|
+
regex: null,
|
|
695
|
+
paramNames: [],
|
|
696
|
+
handler,
|
|
697
|
+
mountPrefix: null,
|
|
698
|
+
} as MiddlewareEntry<TEnv>,
|
|
699
|
+
params,
|
|
700
|
+
}));
|
|
701
|
+
|
|
702
|
+
return executeMiddleware(
|
|
703
|
+
middlewareEntries,
|
|
704
|
+
request,
|
|
705
|
+
env,
|
|
706
|
+
variables,
|
|
707
|
+
finalHandler,
|
|
708
|
+
reverse,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Collect route-level middleware from an entry tree
|
|
714
|
+
*
|
|
715
|
+
* Recursively collects middleware from entries and their orphan layouts.
|
|
716
|
+
* Used by match(), matchPartial(), and previewMatch() to gather route middleware.
|
|
717
|
+
*
|
|
718
|
+
* @param entries - Iterable of entries to collect middleware from (typically from traverseBack)
|
|
719
|
+
* @param params - Route params to attach to each middleware entry
|
|
720
|
+
* @returns Array of collected middleware with params
|
|
721
|
+
*/
|
|
722
|
+
export function collectRouteMiddleware(
|
|
723
|
+
entries: Iterable<MiddlewareCollectableEntry>,
|
|
724
|
+
params: Record<string, string>,
|
|
725
|
+
): CollectedMiddleware[] {
|
|
726
|
+
const result: CollectedMiddleware[] = [];
|
|
727
|
+
|
|
728
|
+
const collect = (entry: MiddlewareCollectableEntry): void => {
|
|
729
|
+
// Collect entry's own middleware
|
|
730
|
+
if (entry.middleware && entry.middleware.length > 0) {
|
|
731
|
+
for (const mw of entry.middleware) {
|
|
732
|
+
result.push({ handler: mw, params });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// Collect middleware from orphan layouts (recursive)
|
|
736
|
+
if (entry.layout && entry.layout.length > 0) {
|
|
737
|
+
for (const orphan of entry.layout) {
|
|
738
|
+
collect(orphan);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
for (const entry of entries) {
|
|
744
|
+
collect(entry);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return result;
|
|
748
|
+
}
|