@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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 +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- 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 +214 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
package/src/router/middleware.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { contextGet, contextSet } from "../context-var.js";
|
|
13
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
14
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
15
|
import type {
|
|
14
16
|
CollectedMiddleware,
|
|
15
17
|
MiddlewareCollectableEntry,
|
|
@@ -20,6 +22,9 @@ import type {
|
|
|
20
22
|
} from "./middleware-types.js";
|
|
21
23
|
import { _getRequestContext } from "../server/request-context.js";
|
|
22
24
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
25
|
+
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
26
|
+
import { stripInternalParams } from "./handler-context.js";
|
|
27
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
23
28
|
|
|
24
29
|
// Re-export types and cookie utilities for backward compatibility
|
|
25
30
|
export type {
|
|
@@ -33,25 +38,29 @@ export type {
|
|
|
33
38
|
} from "./middleware-types.js";
|
|
34
39
|
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
42
|
+
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
43
|
+
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
44
|
+
|
|
45
|
+
function getMiddlewareMetricBase<TEnv>(
|
|
46
|
+
entry: MiddlewareEntry<TEnv>,
|
|
47
|
+
ordinal: number,
|
|
48
|
+
): string {
|
|
49
|
+
const handlerName = entry.handler.name?.trim();
|
|
50
|
+
const scope = entry.pattern ?? "*";
|
|
51
|
+
|
|
52
|
+
if (handlerName) {
|
|
53
|
+
return `${handlerName}@${scope}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `${scope}#${ordinal + 1}`;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
60
|
+
entry: MiddlewareEntry<TEnv>,
|
|
61
|
+
ordinal: number,
|
|
62
|
+
): string {
|
|
63
|
+
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -106,7 +115,12 @@ function escapeRegex(str: string): string {
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
/**
|
|
109
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
118
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
119
|
+
*
|
|
120
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
121
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
122
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
123
|
+
* Express/React Router/Fastify/Koa.
|
|
110
124
|
*/
|
|
111
125
|
export function extractParams(
|
|
112
126
|
pathname: string,
|
|
@@ -118,7 +132,7 @@ export function extractParams(
|
|
|
118
132
|
|
|
119
133
|
const params: Record<string, string> = {};
|
|
120
134
|
for (let i = 0; i < paramNames.length; i++) {
|
|
121
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
135
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
122
136
|
}
|
|
123
137
|
return params;
|
|
124
138
|
}
|
|
@@ -142,7 +156,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
142
156
|
search?: Record<string, unknown>,
|
|
143
157
|
) => string,
|
|
144
158
|
): MiddlewareContext<TEnv> {
|
|
145
|
-
const url = new URL(request.url);
|
|
159
|
+
const url = stripInternalParams(new URL(request.url));
|
|
146
160
|
|
|
147
161
|
// Track the initial response to detect pre/post-next() phase.
|
|
148
162
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -158,13 +172,37 @@ export function createMiddlewareContext<TEnv>(
|
|
|
158
172
|
// Cookie operations are handled by the standalone cookies() function which
|
|
159
173
|
// delegates to the shared RequestContext internally.
|
|
160
174
|
// The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
|
|
175
|
+
// Internal helper: resolve the current response (stub before next(), real after).
|
|
176
|
+
// Not exposed on the public MiddlewareContext type — use ctx.headers instead.
|
|
177
|
+
const getResponse = (): Response => {
|
|
178
|
+
if (isPreNext()) {
|
|
179
|
+
const reqCtx = _getRequestContext();
|
|
180
|
+
if (reqCtx) return reqCtx.res;
|
|
181
|
+
}
|
|
182
|
+
if (!responseHolder.response) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"Response is not available - responseHolder was not initialized",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return responseHolder.response;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
191
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
192
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
193
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
194
|
+
// during `await next()`.
|
|
195
|
+
const reqCtx = _getRequestContext();
|
|
161
196
|
return {
|
|
162
197
|
request,
|
|
163
198
|
url,
|
|
199
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
164
200
|
pathname: url.pathname,
|
|
165
201
|
searchParams: url.searchParams,
|
|
166
202
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
167
203
|
params,
|
|
204
|
+
executionContext: reqCtx?.executionContext,
|
|
205
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
168
206
|
// Getter: re-derives from request context on each access so that global
|
|
169
207
|
// middleware sees the matched route name after await next().
|
|
170
208
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -175,33 +213,16 @@ export function createMiddlewareContext<TEnv>(
|
|
|
175
213
|
) as MiddlewareContext<TEnv>["routeName"];
|
|
176
214
|
},
|
|
177
215
|
|
|
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
|
-
);
|
|
216
|
+
get headers(): Headers {
|
|
217
|
+
return getResponse().headers;
|
|
196
218
|
},
|
|
197
219
|
|
|
198
220
|
get: ((keyOrVar: any) =>
|
|
199
221
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
200
222
|
|
|
201
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
202
|
-
contextSet(variables, keyOrVar, value);
|
|
223
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
224
|
+
contextSet(variables, keyOrVar, value, options);
|
|
203
225
|
}) as MiddlewareContext<TEnv>["set"],
|
|
204
|
-
|
|
205
226
|
header(name: string, value: string): void {
|
|
206
227
|
// Before next(): delegate to shared RequestContext stub
|
|
207
228
|
if (isPreNext()) {
|
|
@@ -220,6 +241,24 @@ export function createMiddlewareContext<TEnv>(
|
|
|
220
241
|
responseHolder.response.headers.set(name, value);
|
|
221
242
|
},
|
|
222
243
|
|
|
244
|
+
get theme(): MiddlewareContext<TEnv>["theme"] {
|
|
245
|
+
return _getRequestContext()?.theme;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
|
|
249
|
+
return _getRequestContext()?.setTheme;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
setLocationState(entries) {
|
|
253
|
+
const reqCtx = _getRequestContext();
|
|
254
|
+
if (!reqCtx) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"setLocationState() is not available outside a request context",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
reqCtx.setLocationState(entries);
|
|
260
|
+
},
|
|
261
|
+
|
|
223
262
|
reverse:
|
|
224
263
|
reverse ??
|
|
225
264
|
((name: string) => {
|
|
@@ -227,6 +266,14 @@ export function createMiddlewareContext<TEnv>(
|
|
|
227
266
|
`ctx.reverse() is not available - route map was not provided to middleware context`,
|
|
228
267
|
);
|
|
229
268
|
}),
|
|
269
|
+
|
|
270
|
+
debugPerformance(): void {
|
|
271
|
+
const reqCtx = _getRequestContext();
|
|
272
|
+
if (reqCtx) {
|
|
273
|
+
reqCtx._debugPerformance = true;
|
|
274
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
230
277
|
};
|
|
231
278
|
}
|
|
232
279
|
|
|
@@ -265,9 +312,9 @@ export function matchMiddleware<TEnv>(
|
|
|
265
312
|
*
|
|
266
313
|
* Features:
|
|
267
314
|
* - `await next()` returns actual Response
|
|
268
|
-
* - `ctx.
|
|
269
|
-
* - `ctx.header()` shorthand for setting
|
|
270
|
-
* - Forgiving: if middleware doesn't return, uses
|
|
315
|
+
* - `ctx.headers` available before and after `await next()`
|
|
316
|
+
* - `ctx.header()` shorthand for setting a single header
|
|
317
|
+
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
271
318
|
* - Short-circuit: return Response to stop chain
|
|
272
319
|
* - Error catching: try/catch around `next()` works
|
|
273
320
|
*/
|
|
@@ -309,19 +356,31 @@ export async function executeMiddleware<TEnv>(
|
|
|
309
356
|
}
|
|
310
357
|
});
|
|
311
358
|
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
312
|
-
// Set-Cookie
|
|
313
|
-
//
|
|
359
|
+
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
360
|
+
// may have already merged the same reqCtx cookies into the response.
|
|
314
361
|
const reqCtx = _getRequestContext();
|
|
315
362
|
if (reqCtx) {
|
|
363
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
364
|
+
if (stubCookies.length > 0) {
|
|
365
|
+
const existing = new Set(mergedHeaders.getSetCookie());
|
|
366
|
+
for (const cookie of stubCookies) {
|
|
367
|
+
if (!existing.has(cookie)) {
|
|
368
|
+
mergedHeaders.append("set-cookie", cookie);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
316
372
|
reqCtx.res.headers.forEach((value, name) => {
|
|
317
|
-
if (name
|
|
318
|
-
mergedHeaders.append(name, value);
|
|
319
|
-
} else if (!mergedHeaders.has(name)) {
|
|
373
|
+
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
320
374
|
mergedHeaders.set(name, value);
|
|
321
375
|
}
|
|
322
376
|
});
|
|
323
377
|
}
|
|
324
378
|
|
|
379
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
380
|
+
responseHolder.response = response;
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
383
|
+
|
|
325
384
|
// Clone response with merged headers (mutable for post-next() modifications)
|
|
326
385
|
responseHolder.response = new Response(response.body, {
|
|
327
386
|
status: response.status,
|
|
@@ -332,6 +391,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
332
391
|
return responseHolder.response;
|
|
333
392
|
}
|
|
334
393
|
|
|
394
|
+
const middlewareOrdinal = index;
|
|
335
395
|
const { entry, params } = middlewares[index++];
|
|
336
396
|
const ctx = createMiddlewareContext(
|
|
337
397
|
request,
|
|
@@ -341,48 +401,81 @@ export async function executeMiddleware<TEnv>(
|
|
|
341
401
|
responseHolder,
|
|
342
402
|
reverse,
|
|
343
403
|
);
|
|
404
|
+
const metricStart = performance.now();
|
|
405
|
+
const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
|
|
406
|
+
let middlewareFinished = false;
|
|
407
|
+
const finishMiddleware = () => {
|
|
408
|
+
if (!middlewareFinished) {
|
|
409
|
+
middlewareFinished = true;
|
|
410
|
+
appendMetric(
|
|
411
|
+
_getRequestContext()?._metricsStore,
|
|
412
|
+
`${metricLabel}:pre`,
|
|
413
|
+
metricStart,
|
|
414
|
+
performance.now() - metricStart,
|
|
415
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
344
419
|
|
|
345
420
|
// Track if next() was called and capture its Promise.
|
|
346
421
|
// Guard against double-calling: a second call would re-enter the
|
|
347
422
|
// downstream chain and overwrite responseHolder.response.
|
|
348
423
|
let nextPromise: Promise<Response> | null = null;
|
|
424
|
+
let nextResolvedAt: number | undefined;
|
|
349
425
|
const wrappedNext = (): Promise<Response> => {
|
|
350
426
|
if (nextPromise) {
|
|
351
427
|
throw new Error(
|
|
352
428
|
`[@rangojs/router] Middleware called next() more than once.`,
|
|
353
429
|
);
|
|
354
430
|
}
|
|
355
|
-
|
|
431
|
+
finishMiddleware();
|
|
432
|
+
const downstream = next();
|
|
433
|
+
nextPromise = downstream.then(
|
|
434
|
+
(res) => {
|
|
435
|
+
nextResolvedAt = performance.now();
|
|
436
|
+
return res;
|
|
437
|
+
},
|
|
438
|
+
(err) => {
|
|
439
|
+
nextResolvedAt = performance.now();
|
|
440
|
+
throw err;
|
|
441
|
+
},
|
|
442
|
+
);
|
|
356
443
|
return nextPromise;
|
|
357
444
|
};
|
|
358
445
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
446
|
+
let result: Response | void;
|
|
447
|
+
try {
|
|
448
|
+
result = await entry.handler(ctx, wrappedNext);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
finishMiddleware();
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
finishMiddleware();
|
|
454
|
+
|
|
455
|
+
// Record post-next() processing time when middleware did work after
|
|
456
|
+
// the downstream chain resolved (e.g. adding headers, logging).
|
|
457
|
+
if (nextResolvedAt !== undefined) {
|
|
458
|
+
const postDur = performance.now() - nextResolvedAt;
|
|
459
|
+
if (postDur > POST_METRIC_MIN_DURATION_MS) {
|
|
460
|
+
appendMetric(
|
|
461
|
+
_getRequestContext()?._metricsStore,
|
|
462
|
+
`${metricLabel}:post`,
|
|
463
|
+
nextResolvedAt,
|
|
464
|
+
postDur,
|
|
465
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
367
468
|
}
|
|
368
|
-
|
|
369
|
-
const result = await entry.handler(ctx, wrappedNext);
|
|
370
469
|
|
|
371
470
|
// Explicit return takes precedence (middleware short-circuit).
|
|
372
471
|
// Merge stub headers (from ctx.header before this point) and
|
|
373
472
|
// RequestContext stub headers (from ctx.setCookie) into the
|
|
374
473
|
// returned Response so they are not lost.
|
|
375
474
|
if (result instanceof Response) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
ctxSetCalled &&
|
|
380
|
-
result.status >= 300 &&
|
|
381
|
-
result.status < 400
|
|
382
|
-
) {
|
|
383
|
-
warnCtxSetBeforeRedirect(entry.handler);
|
|
475
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
476
|
+
responseHolder.response = result;
|
|
477
|
+
return result;
|
|
384
478
|
}
|
|
385
|
-
|
|
386
479
|
const mergedHeaders = new Headers(result.headers);
|
|
387
480
|
stubResponse.headers.forEach((value, name) => {
|
|
388
481
|
if (name.toLowerCase() === "set-cookie") {
|
|
@@ -391,13 +484,22 @@ export async function executeMiddleware<TEnv>(
|
|
|
391
484
|
mergedHeaders.set(name, value);
|
|
392
485
|
}
|
|
393
486
|
});
|
|
394
|
-
// Also merge shared RequestContext stub (cookies written via setCookie)
|
|
487
|
+
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
488
|
+
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
489
|
+
// may have already merged the same reqCtx cookies into the response.
|
|
395
490
|
const reqCtx = _getRequestContext();
|
|
396
491
|
if (reqCtx) {
|
|
492
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
493
|
+
if (stubCookies.length > 0) {
|
|
494
|
+
const existing = new Set(mergedHeaders.getSetCookie());
|
|
495
|
+
for (const cookie of stubCookies) {
|
|
496
|
+
if (!existing.has(cookie)) {
|
|
497
|
+
mergedHeaders.append("set-cookie", cookie);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
397
501
|
reqCtx.res.headers.forEach((value, name) => {
|
|
398
|
-
if (name
|
|
399
|
-
mergedHeaders.append(name, value);
|
|
400
|
-
} else if (!mergedHeaders.has(name)) {
|
|
502
|
+
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
401
503
|
mergedHeaders.set(name, value);
|
|
402
504
|
}
|
|
403
505
|
});
|
|
@@ -424,19 +526,6 @@ export async function executeMiddleware<TEnv>(
|
|
|
424
526
|
// If middleware called next(), await it and return the response
|
|
425
527
|
if (nextPromise) {
|
|
426
528
|
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
529
|
return responseHolder.response!;
|
|
441
530
|
}
|
|
442
531
|
|
|
@@ -459,6 +548,32 @@ export async function executeMiddleware<TEnv>(
|
|
|
459
548
|
throw new Error("No response generated by middleware chain");
|
|
460
549
|
}
|
|
461
550
|
|
|
551
|
+
// Final re-merge: capture any RequestContext stub headers added after the
|
|
552
|
+
// last merge point (e.g. cookies().set() called after await next()).
|
|
553
|
+
// The reqCtx stub may have already been partially merged during finalHandler
|
|
554
|
+
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
555
|
+
//
|
|
556
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
557
|
+
// set-cookie on an upgrade is not meaningful.
|
|
558
|
+
const reqCtx = _getRequestContext();
|
|
559
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
560
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
561
|
+
if (stubCookies.length > 0) {
|
|
562
|
+
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
563
|
+
for (const cookie of stubCookies) {
|
|
564
|
+
if (!existingCookies.has(cookie)) {
|
|
565
|
+
finalResponse.headers.append("set-cookie", cookie);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Fill in non-cookie headers that aren't already on the response
|
|
570
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
571
|
+
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
572
|
+
finalResponse.headers.set(name, value);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
462
577
|
return finalResponse;
|
|
463
578
|
}
|
|
464
579
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Snapshot
|
|
3
|
+
*
|
|
4
|
+
* Pure data type representing the navigation-specific state for partial requests.
|
|
5
|
+
* Consolidates the header parsing, previous-route matching, intercept-context
|
|
6
|
+
* detection, and segment ID filtering that previously lived inline in
|
|
7
|
+
* createMatchContextForPartial (match-api.ts).
|
|
8
|
+
*
|
|
9
|
+
* resolveNavigation() is the factory: given a request + URL + current route key,
|
|
10
|
+
* it returns a NavigationSnapshot (or null if no previous URL).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot of navigation state for a partial (navigation/action) request.
|
|
17
|
+
*
|
|
18
|
+
* Contains the "where are we coming from?" data: previous route, intercept
|
|
19
|
+
* source, client segment state, and derived flags.
|
|
20
|
+
*/
|
|
21
|
+
export interface NavigationSnapshot {
|
|
22
|
+
/** Previous page URL (from X-RSC-Router-Client-Path or Referer) */
|
|
23
|
+
prevUrl: URL;
|
|
24
|
+
/** Params from the previous route match */
|
|
25
|
+
prevParams: Record<string, string>;
|
|
26
|
+
/** Previous route match result (null if prev URL doesn't match any route) */
|
|
27
|
+
prevMatch: RouteMatchResult | null;
|
|
28
|
+
|
|
29
|
+
/** URL used as intercept context source */
|
|
30
|
+
interceptContextUrl: URL;
|
|
31
|
+
/** Route match for the intercept context URL */
|
|
32
|
+
interceptContextMatch: RouteMatchResult | null;
|
|
33
|
+
|
|
34
|
+
/** Raw segment IDs the client currently has */
|
|
35
|
+
clientSegmentIds: string[];
|
|
36
|
+
/** Set version for O(1) lookup */
|
|
37
|
+
clientSegmentSet: Set<string>;
|
|
38
|
+
/** Segment IDs filtered to remove parallel (.@) and loader (D\d+.) entries */
|
|
39
|
+
filteredSegmentIds: string[];
|
|
40
|
+
|
|
41
|
+
/** Whether client considers its cache stale */
|
|
42
|
+
stale: boolean;
|
|
43
|
+
|
|
44
|
+
/** Whether the intercept context route is the same as the current route */
|
|
45
|
+
isSameRouteNavigation: boolean;
|
|
46
|
+
|
|
47
|
+
/** Effective "from" URL (intercept source URL when present, else prevUrl) */
|
|
48
|
+
effectiveFromUrl: URL;
|
|
49
|
+
/** Effective "from" match (intercept source match when present, else prevMatch) */
|
|
50
|
+
effectiveFromMatch: RouteMatchResult | null;
|
|
51
|
+
|
|
52
|
+
/** Whether an intercept source header was present */
|
|
53
|
+
hasInterceptSource: boolean;
|
|
54
|
+
|
|
55
|
+
/** Whether an HMR request header was present */
|
|
56
|
+
isHmr: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ResolveNavigationDeps {
|
|
60
|
+
findMatch: (pathname: string) => RouteMatchResult | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve navigation state from a partial request.
|
|
65
|
+
*
|
|
66
|
+
* Returns null if no previous URL is available (required for partial navigation).
|
|
67
|
+
*
|
|
68
|
+
* @param request - The incoming HTTP request
|
|
69
|
+
* @param url - Parsed URL of the request
|
|
70
|
+
* @param currentRouteKey - Route key of the current (target) route match
|
|
71
|
+
* @param deps - Dependencies (findMatch)
|
|
72
|
+
*/
|
|
73
|
+
export function resolveNavigation(
|
|
74
|
+
request: Request,
|
|
75
|
+
url: URL,
|
|
76
|
+
currentRouteKey: string,
|
|
77
|
+
deps: ResolveNavigationDeps,
|
|
78
|
+
): NavigationSnapshot | null {
|
|
79
|
+
// Parse client state from RSC request params/headers
|
|
80
|
+
const clientSegmentIds =
|
|
81
|
+
url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
|
|
82
|
+
const stale = url.searchParams.get("_rsc_stale") === "true";
|
|
83
|
+
const previousUrl =
|
|
84
|
+
request.headers.get("X-RSC-Router-Client-Path") ||
|
|
85
|
+
request.headers.get("Referer");
|
|
86
|
+
const interceptSourceUrl = request.headers.get(
|
|
87
|
+
"X-RSC-Router-Intercept-Source",
|
|
88
|
+
);
|
|
89
|
+
const isHmr = !!request.headers.get("X-RSC-HMR");
|
|
90
|
+
|
|
91
|
+
if (!previousUrl) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse previous URL
|
|
96
|
+
let prevUrl: URL;
|
|
97
|
+
try {
|
|
98
|
+
prevUrl = new URL(previousUrl, url.origin);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse intercept context URL
|
|
104
|
+
let interceptContextUrl: URL;
|
|
105
|
+
try {
|
|
106
|
+
interceptContextUrl = interceptSourceUrl
|
|
107
|
+
? new URL(interceptSourceUrl, url.origin)
|
|
108
|
+
: prevUrl;
|
|
109
|
+
} catch {
|
|
110
|
+
interceptContextUrl = prevUrl;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Match previous and intercept context routes
|
|
114
|
+
const prevMatch = deps.findMatch(prevUrl.pathname);
|
|
115
|
+
const prevParams = prevMatch?.params || {};
|
|
116
|
+
const interceptContextMatch = interceptSourceUrl
|
|
117
|
+
? deps.findMatch(interceptContextUrl.pathname)
|
|
118
|
+
: prevMatch;
|
|
119
|
+
|
|
120
|
+
// Derived state
|
|
121
|
+
const isSameRouteNavigation = !!(
|
|
122
|
+
interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const hasInterceptSource = !!interceptSourceUrl;
|
|
126
|
+
const effectiveFromUrl = hasInterceptSource ? interceptContextUrl : prevUrl;
|
|
127
|
+
const effectiveFromMatch = hasInterceptSource
|
|
128
|
+
? interceptContextMatch
|
|
129
|
+
: prevMatch;
|
|
130
|
+
|
|
131
|
+
// Filter segment IDs: remove parallel (.@) and loader (D\d+.) entries
|
|
132
|
+
const filteredSegmentIds = clientSegmentIds.filter((id) => {
|
|
133
|
+
if (id.includes(".@")) return false;
|
|
134
|
+
if (/D\d+\./.test(id)) return false;
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const clientSegmentSet = new Set(clientSegmentIds);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
prevUrl,
|
|
142
|
+
prevParams,
|
|
143
|
+
prevMatch,
|
|
144
|
+
interceptContextUrl,
|
|
145
|
+
interceptContextMatch,
|
|
146
|
+
clientSegmentIds,
|
|
147
|
+
clientSegmentSet,
|
|
148
|
+
filteredSegmentIds,
|
|
149
|
+
stale,
|
|
150
|
+
isSameRouteNavigation,
|
|
151
|
+
effectiveFromUrl,
|
|
152
|
+
effectiveFromMatch,
|
|
153
|
+
hasInterceptSource,
|
|
154
|
+
isHmr,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Test helper: create a NavigationSnapshot with sensible defaults and overrides.
|
|
160
|
+
*/
|
|
161
|
+
export function createNavigationSnapshot(
|
|
162
|
+
overrides?: Partial<NavigationSnapshot>,
|
|
163
|
+
): NavigationSnapshot {
|
|
164
|
+
const defaultUrl = new URL("http://localhost/");
|
|
165
|
+
return {
|
|
166
|
+
prevUrl: defaultUrl,
|
|
167
|
+
prevParams: {},
|
|
168
|
+
prevMatch: null,
|
|
169
|
+
interceptContextUrl: defaultUrl,
|
|
170
|
+
interceptContextMatch: null,
|
|
171
|
+
clientSegmentIds: [],
|
|
172
|
+
clientSegmentSet: new Set(),
|
|
173
|
+
filteredSegmentIds: [],
|
|
174
|
+
stale: false,
|
|
175
|
+
isSameRouteNavigation: false,
|
|
176
|
+
effectiveFromUrl: defaultUrl,
|
|
177
|
+
effectiveFromMatch: null,
|
|
178
|
+
hasInterceptSource: false,
|
|
179
|
+
isHmr: false,
|
|
180
|
+
...overrides,
|
|
181
|
+
};
|
|
182
|
+
}
|