@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -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 +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- 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 +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -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 +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/router/middleware.ts
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import { contextGet, contextSet } from "../context-var.js";
|
|
13
13
|
import type {
|
|
14
|
-
CookieOptions,
|
|
15
14
|
CollectedMiddleware,
|
|
16
15
|
MiddlewareCollectableEntry,
|
|
17
16
|
MiddlewareContext,
|
|
@@ -19,7 +18,9 @@ import type {
|
|
|
19
18
|
MiddlewareFn,
|
|
20
19
|
ResponseHolder,
|
|
21
20
|
} from "./middleware-types.js";
|
|
22
|
-
import {
|
|
21
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
22
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
|
+
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
23
24
|
|
|
24
25
|
// Re-export types and cookie utilities for backward compatibility
|
|
25
26
|
export type {
|
|
@@ -33,6 +34,52 @@ export type {
|
|
|
33
34
|
} from "./middleware-types.js";
|
|
34
35
|
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
35
36
|
|
|
37
|
+
// W5: Deduplicate by function reference so each distinct middleware warns once,
|
|
38
|
+
// regardless of whether it is named or anonymous.
|
|
39
|
+
let warnedRedirectMiddleware = new WeakSet<Function>();
|
|
40
|
+
|
|
41
|
+
function warnCtxSetBeforeRedirect(handler: Function): void {
|
|
42
|
+
if (warnedRedirectMiddleware.has(handler)) return;
|
|
43
|
+
warnedRedirectMiddleware.add(handler);
|
|
44
|
+
const label = handler.name || "(anonymous)";
|
|
45
|
+
console.warn(
|
|
46
|
+
`[rango] Route middleware "${label}" called ctx.set() then returned a ` +
|
|
47
|
+
`redirect. Context variables are per-request and won't be available ` +
|
|
48
|
+
`on the redirect target. Use cookies to persist state across ` +
|
|
49
|
+
`redirects, or move ctx.set() to the target route's middleware.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
54
|
+
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
55
|
+
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
56
|
+
|
|
57
|
+
function getMiddlewareMetricBase<TEnv>(
|
|
58
|
+
entry: MiddlewareEntry<TEnv>,
|
|
59
|
+
ordinal: number,
|
|
60
|
+
): string {
|
|
61
|
+
const handlerName = entry.handler.name?.trim();
|
|
62
|
+
const scope = entry.pattern ?? "*";
|
|
63
|
+
|
|
64
|
+
if (handlerName) {
|
|
65
|
+
return `${handlerName}@${scope}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `${scope}#${ordinal + 1}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
72
|
+
entry: MiddlewareEntry<TEnv>,
|
|
73
|
+
ordinal: number,
|
|
74
|
+
): string {
|
|
75
|
+
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reset W5 deduplication state (for tests only). */
|
|
79
|
+
export function _resetW5Warnings(): void {
|
|
80
|
+
warnedRedirectMiddleware = new WeakSet();
|
|
81
|
+
}
|
|
82
|
+
|
|
36
83
|
/**
|
|
37
84
|
* Parse a route pattern into regex and param names
|
|
38
85
|
* Supports: *, /path, /path/*, /path/:param, /path/:param/*
|
|
@@ -107,7 +154,7 @@ export function extractParams(
|
|
|
107
154
|
*
|
|
108
155
|
* Note: The implementation uses runtime values while the interface provides
|
|
109
156
|
* compile-time type safety. The env/get/set types are resolved at call sites
|
|
110
|
-
* via conditional types based on TEnv
|
|
157
|
+
* via conditional types based on TEnv from createRouter<TBindings>().
|
|
111
158
|
*/
|
|
112
159
|
export function createMiddlewareContext<TEnv>(
|
|
113
160
|
request: Request,
|
|
@@ -122,9 +169,20 @@ export function createMiddlewareContext<TEnv>(
|
|
|
122
169
|
) => string,
|
|
123
170
|
): MiddlewareContext<TEnv> {
|
|
124
171
|
const url = new URL(request.url);
|
|
125
|
-
const cookieHeader = request.headers.get("Cookie");
|
|
126
|
-
let parsedCookies: Record<string, string> | null = null;
|
|
127
172
|
|
|
173
|
+
// Track the initial response to detect pre/post-next() phase.
|
|
174
|
+
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
175
|
+
// After next(): responseHolder.response is the real downstream response.
|
|
176
|
+
const initialResponse = responseHolder.response;
|
|
177
|
+
const isPreNext = () => responseHolder.response === initialResponse;
|
|
178
|
+
|
|
179
|
+
// Delegation strategy for RequestContext (reqCtx):
|
|
180
|
+
// - res getter: before next() returns shared reqCtx stub; after next() returns
|
|
181
|
+
// the real downstream response.
|
|
182
|
+
// - header(): before next() delegates to reqCtx; after next() writes to the
|
|
183
|
+
// real downstream response.
|
|
184
|
+
// Cookie operations are handled by the standalone cookies() function which
|
|
185
|
+
// delegates to the shared RequestContext internally.
|
|
128
186
|
// The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
|
|
129
187
|
return {
|
|
130
188
|
request,
|
|
@@ -133,9 +191,23 @@ export function createMiddlewareContext<TEnv>(
|
|
|
133
191
|
searchParams: url.searchParams,
|
|
134
192
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
135
193
|
params,
|
|
194
|
+
// Getter: re-derives from request context on each access so that global
|
|
195
|
+
// middleware sees the matched route name after await next().
|
|
196
|
+
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
197
|
+
const reqCtx = _getRequestContext();
|
|
198
|
+
const raw = reqCtx?._routeName;
|
|
199
|
+
return (
|
|
200
|
+
raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
|
|
201
|
+
) as MiddlewareContext<TEnv>["routeName"];
|
|
202
|
+
},
|
|
136
203
|
|
|
137
|
-
// res getter - returns the stub or real response (always available)
|
|
138
204
|
get res(): Response {
|
|
205
|
+
// Before next(): return shared RequestContext stub so headers
|
|
206
|
+
// set via ctx.header() are visible on ctx.res.
|
|
207
|
+
if (isPreNext()) {
|
|
208
|
+
const reqCtx = _getRequestContext();
|
|
209
|
+
if (reqCtx) return reqCtx.res;
|
|
210
|
+
}
|
|
139
211
|
if (!responseHolder.response) {
|
|
140
212
|
throw new Error(
|
|
141
213
|
"ctx.res is not available - responseHolder was not initialized",
|
|
@@ -143,50 +215,9 @@ export function createMiddlewareContext<TEnv>(
|
|
|
143
215
|
}
|
|
144
216
|
return responseHolder.response;
|
|
145
217
|
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
responseHolder.response = response;
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
cookie(name: string): string | undefined {
|
|
153
|
-
if (!parsedCookies) {
|
|
154
|
-
parsedCookies = parseCookies(cookieHeader);
|
|
155
|
-
}
|
|
156
|
-
return parsedCookies[name];
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
cookies(): Record<string, string> {
|
|
160
|
-
if (!parsedCookies) {
|
|
161
|
-
parsedCookies = parseCookies(cookieHeader);
|
|
162
|
-
}
|
|
163
|
-
return { ...parsedCookies };
|
|
164
|
-
},
|
|
165
|
-
|
|
166
|
-
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
167
|
-
if (!responseHolder.response) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
"ctx.setCookie() is not available - responseHolder was not initialized",
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
responseHolder.response.headers.append(
|
|
173
|
-
"Set-Cookie",
|
|
174
|
-
serializeCookie(name, value, options),
|
|
175
|
-
);
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
deleteCookie(
|
|
179
|
-
name: string,
|
|
180
|
-
options?: Pick<CookieOptions, "domain" | "path">,
|
|
181
|
-
): void {
|
|
182
|
-
if (!responseHolder.response) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
"ctx.deleteCookie() is not available - responseHolder was not initialized",
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
responseHolder.response.headers.append(
|
|
188
|
-
"Set-Cookie",
|
|
189
|
-
serializeCookie(name, "", { ...options, maxAge: 0 }),
|
|
218
|
+
set res(_: Response) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
|
|
190
221
|
);
|
|
191
222
|
},
|
|
192
223
|
|
|
@@ -198,6 +229,15 @@ export function createMiddlewareContext<TEnv>(
|
|
|
198
229
|
}) as MiddlewareContext<TEnv>["set"],
|
|
199
230
|
|
|
200
231
|
header(name: string, value: string): void {
|
|
232
|
+
// Before next(): delegate to shared RequestContext stub
|
|
233
|
+
if (isPreNext()) {
|
|
234
|
+
const reqCtx = _getRequestContext();
|
|
235
|
+
if (reqCtx) {
|
|
236
|
+
reqCtx.header(name, value);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// After next() or standalone: write to current response
|
|
201
241
|
if (!responseHolder.response) {
|
|
202
242
|
throw new Error(
|
|
203
243
|
"ctx.header() is not available - responseHolder was not initialized",
|
|
@@ -213,6 +253,14 @@ export function createMiddlewareContext<TEnv>(
|
|
|
213
253
|
`ctx.reverse() is not available - route map was not provided to middleware context`,
|
|
214
254
|
);
|
|
215
255
|
}),
|
|
256
|
+
|
|
257
|
+
debugPerformance(): void {
|
|
258
|
+
const reqCtx = _getRequestContext();
|
|
259
|
+
if (reqCtx) {
|
|
260
|
+
reqCtx._debugPerformance = true;
|
|
261
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
216
264
|
};
|
|
217
265
|
}
|
|
218
266
|
|
|
@@ -284,8 +332,8 @@ export async function executeMiddleware<TEnv>(
|
|
|
284
332
|
// End of chain - call actual RSC handler
|
|
285
333
|
const response = await finalHandler();
|
|
286
334
|
|
|
287
|
-
// Merge headers set on stub into the real response
|
|
288
|
-
// Use append for Set-Cookie to preserve multiple cookies
|
|
335
|
+
// Merge headers set on stub into the real response.
|
|
336
|
+
// Use append for Set-Cookie to preserve multiple cookies.
|
|
289
337
|
const mergedHeaders = new Headers(response.headers);
|
|
290
338
|
stubResponse.headers.forEach((value, name) => {
|
|
291
339
|
if (name.toLowerCase() === "set-cookie") {
|
|
@@ -294,6 +342,26 @@ export async function executeMiddleware<TEnv>(
|
|
|
294
342
|
mergedHeaders.set(name, value);
|
|
295
343
|
}
|
|
296
344
|
});
|
|
345
|
+
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
346
|
+
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
347
|
+
// may have already merged the same reqCtx cookies into the response.
|
|
348
|
+
const reqCtx = _getRequestContext();
|
|
349
|
+
if (reqCtx) {
|
|
350
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
351
|
+
if (stubCookies.length > 0) {
|
|
352
|
+
const existing = new Set(mergedHeaders.getSetCookie());
|
|
353
|
+
for (const cookie of stubCookies) {
|
|
354
|
+
if (!existing.has(cookie)) {
|
|
355
|
+
mergedHeaders.append("set-cookie", cookie);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
360
|
+
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
361
|
+
mergedHeaders.set(name, value);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
297
365
|
|
|
298
366
|
// Clone response with merged headers (mutable for post-next() modifications)
|
|
299
367
|
responseHolder.response = new Response(response.body, {
|
|
@@ -305,6 +373,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
305
373
|
return responseHolder.response;
|
|
306
374
|
}
|
|
307
375
|
|
|
376
|
+
const middlewareOrdinal = index;
|
|
308
377
|
const { entry, params } = middlewares[index++];
|
|
309
378
|
const ctx = createMiddlewareContext(
|
|
310
379
|
request,
|
|
@@ -314,21 +383,132 @@ export async function executeMiddleware<TEnv>(
|
|
|
314
383
|
responseHolder,
|
|
315
384
|
reverse,
|
|
316
385
|
);
|
|
386
|
+
const metricStart = performance.now();
|
|
387
|
+
const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
|
|
388
|
+
let middlewareFinished = false;
|
|
389
|
+
const finishMiddleware = () => {
|
|
390
|
+
if (!middlewareFinished) {
|
|
391
|
+
middlewareFinished = true;
|
|
392
|
+
appendMetric(
|
|
393
|
+
_getRequestContext()?._metricsStore,
|
|
394
|
+
`${metricLabel}:pre`,
|
|
395
|
+
metricStart,
|
|
396
|
+
performance.now() - metricStart,
|
|
397
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
317
401
|
|
|
318
|
-
// Track if next() was called and capture its Promise
|
|
319
|
-
//
|
|
402
|
+
// Track if next() was called and capture its Promise.
|
|
403
|
+
// Guard against double-calling: a second call would re-enter the
|
|
404
|
+
// downstream chain and overwrite responseHolder.response.
|
|
320
405
|
let nextPromise: Promise<Response> | null = null;
|
|
406
|
+
let nextResolvedAt: number | undefined;
|
|
321
407
|
const wrappedNext = (): Promise<Response> => {
|
|
322
|
-
|
|
408
|
+
if (nextPromise) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`[@rangojs/router] Middleware called next() more than once.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
finishMiddleware();
|
|
414
|
+
const downstream = next();
|
|
415
|
+
nextPromise = downstream.then(
|
|
416
|
+
(res) => {
|
|
417
|
+
nextResolvedAt = performance.now();
|
|
418
|
+
return res;
|
|
419
|
+
},
|
|
420
|
+
(err) => {
|
|
421
|
+
nextResolvedAt = performance.now();
|
|
422
|
+
throw err;
|
|
423
|
+
},
|
|
424
|
+
);
|
|
323
425
|
return nextPromise;
|
|
324
426
|
};
|
|
325
427
|
|
|
326
|
-
|
|
428
|
+
// W5: track whether ctx.set() is called during this middleware
|
|
429
|
+
let ctxSetCalled = false;
|
|
430
|
+
if (process.env.NODE_ENV !== "production") {
|
|
431
|
+
const originalSet = ctx.set;
|
|
432
|
+
ctx.set = ((...args: any[]) => {
|
|
433
|
+
ctxSetCalled = true;
|
|
434
|
+
return (originalSet as Function).apply(ctx, args);
|
|
435
|
+
}) as typeof ctx.set;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let result: Response | void;
|
|
439
|
+
try {
|
|
440
|
+
result = await entry.handler(ctx, wrappedNext);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
finishMiddleware();
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
finishMiddleware();
|
|
446
|
+
|
|
447
|
+
// Record post-next() processing time when middleware did work after
|
|
448
|
+
// the downstream chain resolved (e.g. adding headers, logging).
|
|
449
|
+
if (nextResolvedAt !== undefined) {
|
|
450
|
+
const postDur = performance.now() - nextResolvedAt;
|
|
451
|
+
if (postDur > POST_METRIC_MIN_DURATION_MS) {
|
|
452
|
+
appendMetric(
|
|
453
|
+
_getRequestContext()?._metricsStore,
|
|
454
|
+
`${metricLabel}:post`,
|
|
455
|
+
nextResolvedAt,
|
|
456
|
+
postDur,
|
|
457
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
327
461
|
|
|
328
|
-
// Explicit return takes precedence
|
|
462
|
+
// Explicit return takes precedence (middleware short-circuit).
|
|
463
|
+
// Merge stub headers (from ctx.header before this point) and
|
|
464
|
+
// RequestContext stub headers (from ctx.setCookie) into the
|
|
465
|
+
// returned Response so they are not lost.
|
|
329
466
|
if (result instanceof Response) {
|
|
330
|
-
|
|
331
|
-
|
|
467
|
+
// W5: warn if ctx.set() was called but middleware returned a redirect
|
|
468
|
+
if (
|
|
469
|
+
process.env.NODE_ENV !== "production" &&
|
|
470
|
+
ctxSetCalled &&
|
|
471
|
+
result.status >= 300 &&
|
|
472
|
+
result.status < 400
|
|
473
|
+
) {
|
|
474
|
+
warnCtxSetBeforeRedirect(entry.handler);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const mergedHeaders = new Headers(result.headers);
|
|
478
|
+
stubResponse.headers.forEach((value, name) => {
|
|
479
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
480
|
+
mergedHeaders.append(name, value);
|
|
481
|
+
} else if (!mergedHeaders.has(name)) {
|
|
482
|
+
mergedHeaders.set(name, value);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
486
|
+
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
487
|
+
// may have already merged the same reqCtx cookies into the response.
|
|
488
|
+
const reqCtx = _getRequestContext();
|
|
489
|
+
if (reqCtx) {
|
|
490
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
491
|
+
if (stubCookies.length > 0) {
|
|
492
|
+
const existing = new Set(mergedHeaders.getSetCookie());
|
|
493
|
+
for (const cookie of stubCookies) {
|
|
494
|
+
if (!existing.has(cookie)) {
|
|
495
|
+
mergedHeaders.append("set-cookie", cookie);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
500
|
+
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
501
|
+
mergedHeaders.set(name, value);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const merged = new Response(result.body, {
|
|
506
|
+
status: result.status,
|
|
507
|
+
statusText: result.statusText,
|
|
508
|
+
headers: mergedHeaders,
|
|
509
|
+
});
|
|
510
|
+
responseHolder.response = merged;
|
|
511
|
+
return merged;
|
|
332
512
|
}
|
|
333
513
|
|
|
334
514
|
// Warn about unexpected return values (non-Response, non-undefined)
|
|
@@ -344,6 +524,19 @@ export async function executeMiddleware<TEnv>(
|
|
|
344
524
|
// If middleware called next(), await it and return the response
|
|
345
525
|
if (nextPromise) {
|
|
346
526
|
await nextPromise;
|
|
527
|
+
|
|
528
|
+
// W5: warn if ctx.set() was called but the downstream response is a redirect.
|
|
529
|
+
// The ctx.set() values will be lost because the redirect navigates away.
|
|
530
|
+
if (
|
|
531
|
+
process.env.NODE_ENV !== "production" &&
|
|
532
|
+
ctxSetCalled &&
|
|
533
|
+
responseHolder.response &&
|
|
534
|
+
responseHolder.response.status >= 300 &&
|
|
535
|
+
responseHolder.response.status < 400
|
|
536
|
+
) {
|
|
537
|
+
warnCtxSetBeforeRedirect(entry.handler);
|
|
538
|
+
}
|
|
539
|
+
|
|
347
540
|
return responseHolder.response!;
|
|
348
541
|
}
|
|
349
542
|
|
|
@@ -366,70 +559,30 @@ export async function executeMiddleware<TEnv>(
|
|
|
366
559
|
throw new Error("No response generated by middleware chain");
|
|
367
560
|
}
|
|
368
561
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
request: Request,
|
|
384
|
-
env: TEnv,
|
|
385
|
-
params: Record<string, string>,
|
|
386
|
-
variables: Record<string, any>,
|
|
387
|
-
stubResponse: Response,
|
|
388
|
-
reverse?: (
|
|
389
|
-
name: string,
|
|
390
|
-
params?: Record<string, string>,
|
|
391
|
-
search?: Record<string, unknown>,
|
|
392
|
-
) => string,
|
|
393
|
-
): Promise<void> {
|
|
394
|
-
if (middlewares.length === 0) {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
let index = 0;
|
|
399
|
-
const responseHolder: ResponseHolder = { response: stubResponse };
|
|
400
|
-
|
|
401
|
-
const next = async (): Promise<Response> => {
|
|
402
|
-
if (index >= middlewares.length) {
|
|
403
|
-
return stubResponse;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const middleware = middlewares[index++];
|
|
407
|
-
const ctx = createMiddlewareContext(
|
|
408
|
-
request,
|
|
409
|
-
env,
|
|
410
|
-
params,
|
|
411
|
-
variables,
|
|
412
|
-
responseHolder,
|
|
413
|
-
reverse,
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
const result = await middleware(ctx, next);
|
|
417
|
-
|
|
418
|
-
// If middleware returned a Response, throw an error
|
|
419
|
-
// Server actions can't short-circuit with a Response
|
|
420
|
-
if (result instanceof Response) {
|
|
421
|
-
throw new Error(
|
|
422
|
-
`Loader middleware returned a Response (status: ${result.status}). ` +
|
|
423
|
-
`Server actions cannot return Response. ` +
|
|
424
|
-
`Use GET-based loader fetching for redirects, or throw an error instead.`,
|
|
425
|
-
);
|
|
562
|
+
// Final re-merge: capture any RequestContext stub headers added after the
|
|
563
|
+
// last merge point (e.g. cookies().set() called after await next()).
|
|
564
|
+
// The reqCtx stub may have already been partially merged during finalHandler
|
|
565
|
+
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
566
|
+
const reqCtx = _getRequestContext();
|
|
567
|
+
if (reqCtx) {
|
|
568
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
569
|
+
if (stubCookies.length > 0) {
|
|
570
|
+
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
571
|
+
for (const cookie of stubCookies) {
|
|
572
|
+
if (!existingCookies.has(cookie)) {
|
|
573
|
+
finalResponse.headers.append("set-cookie", cookie);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
426
576
|
}
|
|
577
|
+
// Fill in non-cookie headers that aren't already on the response
|
|
578
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
579
|
+
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
580
|
+
finalResponse.headers.set(name, value);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
427
584
|
|
|
428
|
-
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
await next();
|
|
432
|
-
// Headers/cookies set on stubResponse will be merged by the caller
|
|
585
|
+
return finalResponse;
|
|
433
586
|
}
|
|
434
587
|
|
|
435
588
|
/**
|
|
@@ -485,19 +638,24 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
485
638
|
reverse,
|
|
486
639
|
);
|
|
487
640
|
|
|
488
|
-
|
|
641
|
+
let nextCalled = false;
|
|
642
|
+
const guardedNext = (): Promise<Response> => {
|
|
643
|
+
if (nextCalled) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`[@rangojs/router] Intercept middleware called next() more than once.`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
nextCalled = true;
|
|
649
|
+
return next();
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const result = await middleware(ctx, guardedNext);
|
|
489
653
|
|
|
490
654
|
if (result instanceof Response) {
|
|
491
655
|
earlyResponse = result;
|
|
492
656
|
return result;
|
|
493
657
|
}
|
|
494
658
|
|
|
495
|
-
// Check if middleware replaced ctx.res with a different response
|
|
496
|
-
if (responseHolder.response && responseHolder.response !== stubResponse) {
|
|
497
|
-
earlyResponse = responseHolder.response;
|
|
498
|
-
return earlyResponse;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
659
|
return stubResponse;
|
|
502
660
|
};
|
|
503
661
|
|
|
@@ -515,12 +673,14 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
515
673
|
});
|
|
516
674
|
|
|
517
675
|
if (hasStubHeaders) {
|
|
518
|
-
// Clone and merge headers from stub into early response
|
|
676
|
+
// Clone and merge headers from stub into early response.
|
|
677
|
+
// Only fill in missing headers — the returned Response's explicit
|
|
678
|
+
// headers take precedence, matching executeMiddleware behavior.
|
|
519
679
|
const mergedHeaders = new Headers(response.headers);
|
|
520
680
|
stubResponse.headers.forEach((value, name) => {
|
|
521
681
|
if (name.toLowerCase() === "set-cookie") {
|
|
522
682
|
mergedHeaders.append(name, value);
|
|
523
|
-
} else {
|
|
683
|
+
} else if (!mergedHeaders.has(name)) {
|
|
524
684
|
mergedHeaders.set(name, value);
|
|
525
685
|
}
|
|
526
686
|
});
|