@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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 +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- 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 +45 -3
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- 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/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- 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/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 +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- 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 +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -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 +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- 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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- 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 +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -86,19 +86,14 @@
|
|
|
86
86
|
* -> output: cached segments + fresh loader data
|
|
87
87
|
*
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* 2. createMatchPartialPipeline (Partial Match)
|
|
98
|
-
* - Used for client-side navigation
|
|
99
|
-
* - Includes revalidation for SWR
|
|
100
|
-
* - Compares with previous params/URL
|
|
101
|
-
* - Supports intercepts (soft navigation modals)
|
|
89
|
+
* PIPELINE VARIANT
|
|
90
|
+
* ================
|
|
91
|
+
*
|
|
92
|
+
* createMatchPartialPipeline handles both full (document) and partial
|
|
93
|
+
* (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
|
|
94
|
+
* - cache-lookup/store work for both
|
|
95
|
+
* - background-revalidation is a no-op for full matches (no stale state)
|
|
96
|
+
* - intercept-resolution is a no-op for full matches (no previous navigation)
|
|
102
97
|
*/
|
|
103
98
|
import type { ResolvedSegment } from "../types.js";
|
|
104
99
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
@@ -182,33 +177,3 @@ export function createMatchPartialPipeline<TEnv>(
|
|
|
182
177
|
// Start with empty source - cache lookup or segment resolution will produce segments
|
|
183
178
|
return pipeline(empty());
|
|
184
179
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Create the full match pipeline (simpler, no revalidation)
|
|
188
|
-
*
|
|
189
|
-
* Used for document requests (initial page load) where we don't need
|
|
190
|
-
* revalidation logic since there's no previous state to compare against.
|
|
191
|
-
*/
|
|
192
|
-
export function createMatchPipeline<TEnv>(
|
|
193
|
-
ctx: MatchContext<TEnv>,
|
|
194
|
-
state: MatchPipelineState,
|
|
195
|
-
): AsyncGenerator<ResolvedSegment> {
|
|
196
|
-
// For full match, we only need:
|
|
197
|
-
// 1. Cache lookup
|
|
198
|
-
// 2. Segment resolution (without revalidation)
|
|
199
|
-
// 3. Intercept resolution
|
|
200
|
-
// 4. Cache store
|
|
201
|
-
|
|
202
|
-
// Note: Full match uses different resolution logic (resolveAllSegments instead of
|
|
203
|
-
// resolveAllSegmentsWithRevalidation). This will be handled by the segment resolution
|
|
204
|
-
// middleware checking ctx.isFullMatch or similar flag.
|
|
205
|
-
|
|
206
|
-
const pipeline = compose<ResolvedSegment>(
|
|
207
|
-
withCacheStore(ctx, state),
|
|
208
|
-
withInterceptResolution(ctx, state),
|
|
209
|
-
withSegmentResolution(ctx, state),
|
|
210
|
-
withCacheLookup(ctx, state),
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
return pipeline(empty());
|
|
214
|
-
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ContextVar } from "../context-var.js";
|
|
9
9
|
import type {
|
|
10
10
|
DefaultReverseRouteMap,
|
|
11
|
+
DefaultRouteName,
|
|
11
12
|
DefaultVars,
|
|
12
13
|
} from "../types/global-namespace.js";
|
|
13
14
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
@@ -93,6 +94,12 @@ export interface MiddlewareContext<
|
|
|
93
94
|
*/
|
|
94
95
|
header(name: string, value: string): void;
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The matched route name, if available and the route has an explicit name.
|
|
99
|
+
* Undefined for global middleware (runs before route matching) or unnamed routes.
|
|
100
|
+
*/
|
|
101
|
+
routeName?: DefaultRouteName;
|
|
102
|
+
|
|
96
103
|
/**
|
|
97
104
|
* Generate URLs from route names.
|
|
98
105
|
* - `name` — global route, from the named-routes definition
|
package/src/router/middleware.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ResponseHolder,
|
|
20
20
|
} from "./middleware-types.js";
|
|
21
21
|
import { _getRequestContext } from "../server/request-context.js";
|
|
22
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
22
23
|
|
|
23
24
|
// Re-export types and cookie utilities for backward compatibility
|
|
24
25
|
export type {
|
|
@@ -32,6 +33,27 @@ export type {
|
|
|
32
33
|
} from "./middleware-types.js";
|
|
33
34
|
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
34
35
|
|
|
36
|
+
// W5: Deduplicate by function reference so each distinct middleware warns once,
|
|
37
|
+
// regardless of whether it is named or anonymous.
|
|
38
|
+
let warnedRedirectMiddleware = new WeakSet<Function>();
|
|
39
|
+
|
|
40
|
+
function warnCtxSetBeforeRedirect(handler: Function): void {
|
|
41
|
+
if (warnedRedirectMiddleware.has(handler)) return;
|
|
42
|
+
warnedRedirectMiddleware.add(handler);
|
|
43
|
+
const label = handler.name || "(anonymous)";
|
|
44
|
+
console.warn(
|
|
45
|
+
`[rango] Route middleware "${label}" called ctx.set() then returned a ` +
|
|
46
|
+
`redirect. Context variables are per-request and won't be available ` +
|
|
47
|
+
`on the redirect target. Use cookies to persist state across ` +
|
|
48
|
+
`redirects, or move ctx.set() to the target route's middleware.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reset W5 deduplication state (for tests only). */
|
|
53
|
+
export function _resetW5Warnings(): void {
|
|
54
|
+
warnedRedirectMiddleware = new WeakSet();
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
/**
|
|
36
58
|
* Parse a route pattern into regex and param names
|
|
37
59
|
* Supports: *, /path, /path/*, /path/:param, /path/:param/*
|
|
@@ -143,6 +165,15 @@ export function createMiddlewareContext<TEnv>(
|
|
|
143
165
|
searchParams: url.searchParams,
|
|
144
166
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
145
167
|
params,
|
|
168
|
+
// Getter: re-derives from request context on each access so that global
|
|
169
|
+
// middleware sees the matched route name after await next().
|
|
170
|
+
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
171
|
+
const reqCtx = _getRequestContext();
|
|
172
|
+
const raw = reqCtx?._routeName;
|
|
173
|
+
return (
|
|
174
|
+
raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
|
|
175
|
+
) as MiddlewareContext<TEnv>["routeName"];
|
|
176
|
+
},
|
|
146
177
|
|
|
147
178
|
get res(): Response {
|
|
148
179
|
// Before next(): return shared RequestContext stub so headers
|
|
@@ -267,8 +298,8 @@ export async function executeMiddleware<TEnv>(
|
|
|
267
298
|
// End of chain - call actual RSC handler
|
|
268
299
|
const response = await finalHandler();
|
|
269
300
|
|
|
270
|
-
// Merge headers set on stub into the real response
|
|
271
|
-
// Use append for Set-Cookie to preserve multiple cookies
|
|
301
|
+
// Merge headers set on stub into the real response.
|
|
302
|
+
// Use append for Set-Cookie to preserve multiple cookies.
|
|
272
303
|
const mergedHeaders = new Headers(response.headers);
|
|
273
304
|
stubResponse.headers.forEach((value, name) => {
|
|
274
305
|
if (name.toLowerCase() === "set-cookie") {
|
|
@@ -277,7 +308,9 @@ export async function executeMiddleware<TEnv>(
|
|
|
277
308
|
mergedHeaders.set(name, value);
|
|
278
309
|
}
|
|
279
310
|
});
|
|
280
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set())
|
|
311
|
+
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
312
|
+
// Set-Cookie duplication is prevented by createResponseWithMergedHeaders
|
|
313
|
+
// draining Set-Cookie from ctx.res after merging (helpers.ts).
|
|
281
314
|
const reqCtx = _getRequestContext();
|
|
282
315
|
if (reqCtx) {
|
|
283
316
|
reqCtx.res.headers.forEach((value, name) => {
|
|
@@ -309,14 +342,30 @@ export async function executeMiddleware<TEnv>(
|
|
|
309
342
|
reverse,
|
|
310
343
|
);
|
|
311
344
|
|
|
312
|
-
// Track if next() was called and capture its Promise
|
|
313
|
-
//
|
|
345
|
+
// Track if next() was called and capture its Promise.
|
|
346
|
+
// Guard against double-calling: a second call would re-enter the
|
|
347
|
+
// downstream chain and overwrite responseHolder.response.
|
|
314
348
|
let nextPromise: Promise<Response> | null = null;
|
|
315
349
|
const wrappedNext = (): Promise<Response> => {
|
|
350
|
+
if (nextPromise) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`[@rangojs/router] Middleware called next() more than once.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
316
355
|
nextPromise = next();
|
|
317
356
|
return nextPromise;
|
|
318
357
|
};
|
|
319
358
|
|
|
359
|
+
// W5: track whether ctx.set() is called during this middleware
|
|
360
|
+
let ctxSetCalled = false;
|
|
361
|
+
if (process.env.NODE_ENV !== "production") {
|
|
362
|
+
const originalSet = ctx.set;
|
|
363
|
+
ctx.set = ((...args: any[]) => {
|
|
364
|
+
ctxSetCalled = true;
|
|
365
|
+
return (originalSet as Function).apply(ctx, args);
|
|
366
|
+
}) as typeof ctx.set;
|
|
367
|
+
}
|
|
368
|
+
|
|
320
369
|
const result = await entry.handler(ctx, wrappedNext);
|
|
321
370
|
|
|
322
371
|
// Explicit return takes precedence (middleware short-circuit).
|
|
@@ -324,6 +373,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
324
373
|
// RequestContext stub headers (from ctx.setCookie) into the
|
|
325
374
|
// returned Response so they are not lost.
|
|
326
375
|
if (result instanceof Response) {
|
|
376
|
+
// W5: warn if ctx.set() was called but middleware returned a redirect
|
|
377
|
+
if (
|
|
378
|
+
process.env.NODE_ENV !== "production" &&
|
|
379
|
+
ctxSetCalled &&
|
|
380
|
+
result.status >= 300 &&
|
|
381
|
+
result.status < 400
|
|
382
|
+
) {
|
|
383
|
+
warnCtxSetBeforeRedirect(entry.handler);
|
|
384
|
+
}
|
|
385
|
+
|
|
327
386
|
const mergedHeaders = new Headers(result.headers);
|
|
328
387
|
stubResponse.headers.forEach((value, name) => {
|
|
329
388
|
if (name.toLowerCase() === "set-cookie") {
|
|
@@ -365,6 +424,19 @@ export async function executeMiddleware<TEnv>(
|
|
|
365
424
|
// If middleware called next(), await it and return the response
|
|
366
425
|
if (nextPromise) {
|
|
367
426
|
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
|
+
|
|
368
440
|
return responseHolder.response!;
|
|
369
441
|
}
|
|
370
442
|
|
|
@@ -443,7 +515,18 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
443
515
|
reverse,
|
|
444
516
|
);
|
|
445
517
|
|
|
446
|
-
|
|
518
|
+
let nextCalled = false;
|
|
519
|
+
const guardedNext = (): Promise<Response> => {
|
|
520
|
+
if (nextCalled) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`[@rangojs/router] Intercept middleware called next() more than once.`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
nextCalled = true;
|
|
526
|
+
return next();
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const result = await middleware(ctx, guardedNext);
|
|
447
530
|
|
|
448
531
|
if (result instanceof Response) {
|
|
449
532
|
earlyResponse = result;
|
|
@@ -467,12 +550,14 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
467
550
|
});
|
|
468
551
|
|
|
469
552
|
if (hasStubHeaders) {
|
|
470
|
-
// Clone and merge headers from stub into early response
|
|
553
|
+
// Clone and merge headers from stub into early response.
|
|
554
|
+
// Only fill in missing headers — the returned Response's explicit
|
|
555
|
+
// headers take precedence, matching executeMiddleware behavior.
|
|
471
556
|
const mergedHeaders = new Headers(response.headers);
|
|
472
557
|
stubResponse.headers.forEach((value, name) => {
|
|
473
558
|
if (name.toLowerCase() === "set-cookie") {
|
|
474
559
|
mergedHeaders.append(name, value);
|
|
475
|
-
} else {
|
|
560
|
+
} else if (!mergedHeaders.has(name)) {
|
|
476
561
|
mergedHeaders.set(name, value);
|
|
477
562
|
}
|
|
478
563
|
});
|
|
@@ -140,7 +140,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
140
140
|
} else if (segment.type === "param") {
|
|
141
141
|
paramNames.push(segment.value);
|
|
142
142
|
const valuePattern = segment.constraint
|
|
143
|
-
? `(${segment.constraint.join("|")})`
|
|
143
|
+
? `(${segment.constraint.map(escapeRegex).join("|")})`
|
|
144
144
|
: "([^/]+)";
|
|
145
145
|
|
|
146
146
|
if (segment.optional) {
|
|
@@ -388,6 +388,9 @@ export function findMatch<TEnv>(
|
|
|
388
388
|
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
389
389
|
? { pr: true as const }
|
|
390
390
|
: {};
|
|
391
|
+
const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
|
|
392
|
+
? { pt: true as const }
|
|
393
|
+
: {};
|
|
391
394
|
|
|
392
395
|
// Try exact match first
|
|
393
396
|
const match = regex.exec(pathname);
|
|
@@ -419,6 +422,7 @@ export function findMatch<TEnv>(
|
|
|
419
422
|
optionalParams,
|
|
420
423
|
redirectTo: pathname + "/",
|
|
421
424
|
...prFlag,
|
|
425
|
+
...ptFlag,
|
|
422
426
|
};
|
|
423
427
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
424
428
|
// Mode says never have trailing slash, but pathname has it
|
|
@@ -429,10 +433,18 @@ export function findMatch<TEnv>(
|
|
|
429
433
|
optionalParams,
|
|
430
434
|
redirectTo: pathname.slice(0, -1),
|
|
431
435
|
...prFlag,
|
|
436
|
+
...ptFlag,
|
|
432
437
|
};
|
|
433
438
|
}
|
|
434
439
|
|
|
435
|
-
return {
|
|
440
|
+
return {
|
|
441
|
+
entry,
|
|
442
|
+
routeKey,
|
|
443
|
+
params,
|
|
444
|
+
optionalParams,
|
|
445
|
+
...prFlag,
|
|
446
|
+
...ptFlag,
|
|
447
|
+
};
|
|
436
448
|
}
|
|
437
449
|
|
|
438
450
|
// Try alternate pathname (opposite trailing slash)
|
|
@@ -446,7 +458,14 @@ export function findMatch<TEnv>(
|
|
|
446
458
|
// Determine redirect behavior based on mode
|
|
447
459
|
if (trailingSlashMode === "ignore") {
|
|
448
460
|
// Match without redirect
|
|
449
|
-
return {
|
|
461
|
+
return {
|
|
462
|
+
entry,
|
|
463
|
+
routeKey,
|
|
464
|
+
params,
|
|
465
|
+
optionalParams,
|
|
466
|
+
...prFlag,
|
|
467
|
+
...ptFlag,
|
|
468
|
+
};
|
|
450
469
|
} else if (trailingSlashMode === "never") {
|
|
451
470
|
// Redirect to no trailing slash
|
|
452
471
|
if (pathnameHasTrailingSlash) {
|
|
@@ -457,9 +476,17 @@ export function findMatch<TEnv>(
|
|
|
457
476
|
optionalParams,
|
|
458
477
|
redirectTo: alternatePathname,
|
|
459
478
|
...prFlag,
|
|
479
|
+
...ptFlag,
|
|
460
480
|
};
|
|
461
481
|
}
|
|
462
|
-
return {
|
|
482
|
+
return {
|
|
483
|
+
entry,
|
|
484
|
+
routeKey,
|
|
485
|
+
params,
|
|
486
|
+
optionalParams,
|
|
487
|
+
...prFlag,
|
|
488
|
+
...ptFlag,
|
|
489
|
+
};
|
|
463
490
|
} else if (trailingSlashMode === "always") {
|
|
464
491
|
// Redirect to with trailing slash
|
|
465
492
|
if (!pathnameHasTrailingSlash) {
|
|
@@ -470,9 +497,17 @@ export function findMatch<TEnv>(
|
|
|
470
497
|
optionalParams,
|
|
471
498
|
redirectTo: alternatePathname,
|
|
472
499
|
...prFlag,
|
|
500
|
+
...ptFlag,
|
|
473
501
|
};
|
|
474
502
|
}
|
|
475
|
-
return {
|
|
503
|
+
return {
|
|
504
|
+
entry,
|
|
505
|
+
routeKey,
|
|
506
|
+
params,
|
|
507
|
+
optionalParams,
|
|
508
|
+
...prFlag,
|
|
509
|
+
...ptFlag,
|
|
510
|
+
};
|
|
476
511
|
} else {
|
|
477
512
|
// No explicit mode - use pattern-based detection
|
|
478
513
|
// Redirect to canonical form (what the pattern defines)
|
|
@@ -486,6 +521,7 @@ export function findMatch<TEnv>(
|
|
|
486
521
|
optionalParams,
|
|
487
522
|
redirectTo: canonicalPath,
|
|
488
523
|
...prFlag,
|
|
524
|
+
...ptFlag,
|
|
489
525
|
};
|
|
490
526
|
}
|
|
491
527
|
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
createStaticContext,
|
|
12
12
|
createReverseFunction,
|
|
13
13
|
} from "./handler-context.js";
|
|
14
|
+
import { isPrerenderPassthrough } from "../prerender.js";
|
|
15
|
+
import { isRouteRootScoped } from "../route-map-builder.js";
|
|
14
16
|
import { setupBuildUse } from "./loader-resolution.js";
|
|
15
17
|
import { loadManifest } from "./manifest.js";
|
|
16
18
|
import { traverseBack } from "./pattern-matching.js";
|
|
@@ -51,6 +53,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
51
53
|
params: Record<string, string>,
|
|
52
54
|
deps: PrerenderMatchDeps<TEnv>,
|
|
53
55
|
buildVars?: Record<string, any>,
|
|
56
|
+
isPassthroughRoute?: boolean,
|
|
54
57
|
): Promise<{
|
|
55
58
|
segments: SerializedSegmentData[];
|
|
56
59
|
handles: Record<string, SegmentHandleData>;
|
|
@@ -58,6 +61,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
58
61
|
params: Record<string, string>;
|
|
59
62
|
interceptSegments?: SerializedSegmentData[];
|
|
60
63
|
interceptHandles?: Record<string, SegmentHandleData>;
|
|
64
|
+
passthrough?: true;
|
|
61
65
|
} | null> {
|
|
62
66
|
// 1. Find the matching route entry
|
|
63
67
|
const matched = deps.findMatch(pathname);
|
|
@@ -65,6 +69,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
65
69
|
|
|
66
70
|
// Use params from trie match if available, fall back to provided params
|
|
67
71
|
const matchedParams = matched.params ?? params;
|
|
72
|
+
const matchedPassthroughRoute = isPassthroughRoute ?? matched.pt === true;
|
|
68
73
|
|
|
69
74
|
// Build RouterContext for loadManifest/traverseBack
|
|
70
75
|
const routerCtx = deps.buildRouterContext();
|
|
@@ -121,10 +126,12 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
121
126
|
_onResponseCallbacks: [],
|
|
122
127
|
setLocationState() {},
|
|
123
128
|
_locationState: undefined,
|
|
129
|
+
_reportedErrors: new WeakSet<object>(),
|
|
124
130
|
reverse: createReverseFunction(
|
|
125
131
|
deps.mergedRouteMap,
|
|
126
132
|
matched.routeKey,
|
|
127
133
|
matchedParams,
|
|
134
|
+
matched.routeKey ? isRouteRootScoped(matched.routeKey) : undefined,
|
|
128
135
|
),
|
|
129
136
|
};
|
|
130
137
|
|
|
@@ -138,6 +145,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
138
145
|
deps.mergedRouteMap,
|
|
139
146
|
matched.routeKey,
|
|
140
147
|
variables,
|
|
148
|
+
matchedPassthroughRoute,
|
|
141
149
|
);
|
|
142
150
|
|
|
143
151
|
// 7. Wire use() for handles only (loaders throw)
|
|
@@ -154,17 +162,31 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
154
162
|
{ skipLoaders: true },
|
|
155
163
|
);
|
|
156
164
|
|
|
157
|
-
// 9.
|
|
165
|
+
// 9. Detect passthrough sentinel: handler returned ctx.passthrough()
|
|
166
|
+
for (const seg of allSegments) {
|
|
167
|
+
if (isPrerenderPassthrough(seg.component)) {
|
|
168
|
+
return {
|
|
169
|
+
segments: [],
|
|
170
|
+
handles: {},
|
|
171
|
+
routeName: matched.routeKey,
|
|
172
|
+
params: matchedParams,
|
|
173
|
+
passthrough: true as const,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 10. Filter out any loader segments (belt-and-suspenders)
|
|
158
179
|
const nonLoaderSegments = allSegments.filter((s) => s.type !== "loader");
|
|
159
180
|
|
|
160
|
-
//
|
|
181
|
+
// 11. Wait for handles to settle
|
|
182
|
+
handleStore.seal();
|
|
161
183
|
await handleStore.settled;
|
|
162
184
|
|
|
163
|
-
//
|
|
185
|
+
// 12. Serialize segments using the cache serializer
|
|
164
186
|
const { serializeSegments } = await import("../cache/segment-codec.js");
|
|
165
187
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
166
188
|
|
|
167
|
-
//
|
|
189
|
+
// 13. Collect handle data per segment (skip segments with no handle data)
|
|
168
190
|
const handles: Record<string, SegmentHandleData> = {};
|
|
169
191
|
for (const seg of nonLoaderSegments) {
|
|
170
192
|
const segHandles = handleStore.getDataForSegment(seg.id);
|
|
@@ -176,7 +198,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
176
198
|
// Use the trie-level route key (e.g., "docs", "docs.article")
|
|
177
199
|
const routeName = matched.routeKey;
|
|
178
200
|
|
|
179
|
-
//
|
|
201
|
+
// 14. Resolve intercept segments for this route (if any ancestor defines
|
|
180
202
|
// an intercept targeting this route). At build time we skip when()
|
|
181
203
|
// evaluation -- we pre-render all intercepts unconditionally and let
|
|
182
204
|
// runtime matching decide which to serve.
|
|
@@ -332,7 +354,13 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
332
354
|
_onResponseCallbacks: [],
|
|
333
355
|
setLocationState() {},
|
|
334
356
|
_locationState: undefined,
|
|
335
|
-
|
|
357
|
+
_reportedErrors: new WeakSet<object>(),
|
|
358
|
+
reverse: createReverseFunction(
|
|
359
|
+
mergedRouteMap,
|
|
360
|
+
routeName,
|
|
361
|
+
{},
|
|
362
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
363
|
+
),
|
|
336
364
|
};
|
|
337
365
|
|
|
338
366
|
return runWithRequestContext(minimalRequestContext, async () => {
|
|
@@ -122,9 +122,15 @@ export async function previewMatch<TEnv = any>(
|
|
|
122
122
|
undefined,
|
|
123
123
|
false,
|
|
124
124
|
);
|
|
125
|
+
// Recompute middleware from the selected variant's entry tree
|
|
126
|
+
// since different variants can have different middleware chains.
|
|
127
|
+
const variantMiddleware = collectRouteMiddleware(
|
|
128
|
+
traverseBack(negotiateEntry),
|
|
129
|
+
matched.params,
|
|
130
|
+
);
|
|
125
131
|
return {
|
|
126
132
|
routeMiddleware:
|
|
127
|
-
|
|
133
|
+
variantMiddleware.length > 0 ? variantMiddleware : undefined,
|
|
128
134
|
responseType: variant.responseType,
|
|
129
135
|
handler:
|
|
130
136
|
negotiateEntry.type === "route"
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ResolvedSegment, HandlerContext } from "../types";
|
|
8
8
|
import type { ActionContext } from "./types";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
debugLog,
|
|
11
|
+
pushRevalidationTraceEntry,
|
|
12
|
+
isTraceActive,
|
|
13
|
+
} from "./logging.js";
|
|
14
|
+
import type { RevalidationTraceEntry } from "./logging.js";
|
|
15
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
16
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
10
17
|
|
|
11
18
|
function paramsEqual(
|
|
12
19
|
a: Record<string, string>,
|
|
@@ -50,6 +57,8 @@ interface EvaluateRevalidationOptions<TEnv> {
|
|
|
50
57
|
actionContext?: ActionContext;
|
|
51
58
|
/** If true, this is a stale cache revalidation request */
|
|
52
59
|
stale?: boolean;
|
|
60
|
+
/** Trace source hint for the revalidation trace */
|
|
61
|
+
traceSource?: RevalidationTraceEntry["source"];
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
@@ -71,28 +80,54 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
71
80
|
context,
|
|
72
81
|
actionContext,
|
|
73
82
|
stale,
|
|
83
|
+
traceSource,
|
|
74
84
|
} = options;
|
|
75
85
|
const nextParams = segment.params || {};
|
|
76
86
|
const paramsChanged = !paramsEqual(nextParams, prevParams);
|
|
77
87
|
|
|
88
|
+
// Trace helper: push a structured entry to the request-scoped trace buffer.
|
|
89
|
+
// Guarded by isTraceActive() so object construction is skipped in production.
|
|
90
|
+
function pushTrace(
|
|
91
|
+
defaultVal: boolean,
|
|
92
|
+
finalVal: boolean,
|
|
93
|
+
reason: string,
|
|
94
|
+
): void {
|
|
95
|
+
if (!isTraceActive()) return;
|
|
96
|
+
pushRevalidationTraceEntry({
|
|
97
|
+
segmentId: segment.id,
|
|
98
|
+
segmentType: segment.type,
|
|
99
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
100
|
+
source: traceSource ?? "segment-resolution",
|
|
101
|
+
defaultShouldRevalidate: defaultVal,
|
|
102
|
+
finalShouldRevalidate: finalVal,
|
|
103
|
+
reason,
|
|
104
|
+
customRevalidators: revalidations.length || undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
// Calculate default revalidation based on segment type and request method
|
|
79
109
|
let defaultShouldRevalidate: boolean;
|
|
110
|
+
let defaultReason: string;
|
|
80
111
|
|
|
81
112
|
if (request.method === "POST") {
|
|
82
113
|
// Actions: revalidate segments that belong to the route, skip parent chain
|
|
83
114
|
if (segment.type === "route") {
|
|
84
115
|
// Route segment always revalidates on actions
|
|
85
116
|
defaultShouldRevalidate = true;
|
|
117
|
+
defaultReason = "action:route-segment";
|
|
86
118
|
} else if (segment.type === "loader") {
|
|
87
119
|
// Loaders always revalidate on actions - they often contain action-sensitive data
|
|
88
120
|
// (e.g., cart count after add-to-cart action)
|
|
89
121
|
defaultShouldRevalidate = true;
|
|
122
|
+
defaultReason = "action:loader-segment";
|
|
90
123
|
} else if (segment.belongsToRoute) {
|
|
91
124
|
// Segment belongs to route (orphan layouts/parallels) - revalidate
|
|
92
125
|
defaultShouldRevalidate = true;
|
|
126
|
+
defaultReason = "action:belongs-to-route";
|
|
93
127
|
} else {
|
|
94
128
|
// Parent chain segment (shared layouts/parallels) - don't revalidate
|
|
95
129
|
defaultShouldRevalidate = false;
|
|
130
|
+
defaultReason = "action:parent-chain-skip";
|
|
96
131
|
}
|
|
97
132
|
} else {
|
|
98
133
|
// Navigation (GET): Conservative defaults to minimize unnecessary revalidations
|
|
@@ -102,6 +137,9 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
102
137
|
// Route segments revalidate when params change
|
|
103
138
|
// Routes are the primary param-dependent content and always need updates
|
|
104
139
|
defaultShouldRevalidate = paramsChanged;
|
|
140
|
+
defaultReason = paramsChanged
|
|
141
|
+
? "nav:params-changed"
|
|
142
|
+
: "nav:params-unchanged";
|
|
105
143
|
if (paramsChanged) {
|
|
106
144
|
debugLog("revalidation", "route params changed, revalidating", {
|
|
107
145
|
segmentId: segment.id,
|
|
@@ -112,6 +150,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
112
150
|
// Cannot assume these segments depend on params without explicit declaration
|
|
113
151
|
// Use custom revalidation functions to opt-in when needed
|
|
114
152
|
defaultShouldRevalidate = false;
|
|
153
|
+
defaultReason = "nav:non-route-skip";
|
|
115
154
|
debugLog("revalidation", "non-route segment skipped by default", {
|
|
116
155
|
segmentId: segment.id,
|
|
117
156
|
segmentType: segment.type,
|
|
@@ -132,6 +171,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
132
171
|
segmentId: segment.id,
|
|
133
172
|
});
|
|
134
173
|
}
|
|
174
|
+
pushTrace(defaultShouldRevalidate, defaultShouldRevalidate, defaultReason);
|
|
135
175
|
return defaultShouldRevalidate;
|
|
136
176
|
}
|
|
137
177
|
|
|
@@ -142,6 +182,16 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
142
182
|
// Execute revalidation functions with soft/hard decision pattern
|
|
143
183
|
let currentSuggestion = defaultShouldRevalidate;
|
|
144
184
|
|
|
185
|
+
// Compute public route names (filtered: undefined for auto-generated routes)
|
|
186
|
+
const toRouteName =
|
|
187
|
+
routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
|
|
188
|
+
const reqCtx = _getRequestContext();
|
|
189
|
+
const prevRouteKey = reqCtx?._prevRouteKey;
|
|
190
|
+
const fromRouteName =
|
|
191
|
+
prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
|
|
192
|
+
? prevRouteKey
|
|
193
|
+
: undefined;
|
|
194
|
+
|
|
145
195
|
for (const { name, fn } of revalidations) {
|
|
146
196
|
const result = fn({
|
|
147
197
|
currentParams: prevSegment?.params || prevParams, // Use segment params if available, else route params
|
|
@@ -160,7 +210,9 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
160
210
|
actionResult: actionContext?.actionResult,
|
|
161
211
|
formData: actionContext?.formData,
|
|
162
212
|
method: request.method, // GET for navigation, POST for actions
|
|
163
|
-
routeName:
|
|
213
|
+
routeName: toRouteName, // Navigation target route name (filtered)
|
|
214
|
+
fromRouteName, // Navigation source route name (filtered)
|
|
215
|
+
toRouteName, // Navigation target route name (filtered)
|
|
164
216
|
// Stale cache context (only true for background revalidation after stale cache render)
|
|
165
217
|
stale,
|
|
166
218
|
});
|
|
@@ -176,6 +228,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
176
228
|
revalidator: name,
|
|
177
229
|
revalidate: result,
|
|
178
230
|
});
|
|
231
|
+
pushTrace(defaultShouldRevalidate, result, `hard:${name}`);
|
|
179
232
|
return result;
|
|
180
233
|
} else if (
|
|
181
234
|
result &&
|
|
@@ -206,5 +259,11 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
206
259
|
segmentId: segment.id,
|
|
207
260
|
revalidate: currentSuggestion,
|
|
208
261
|
});
|
|
262
|
+
const softNames = revalidations.map((r) => r.name).join(",");
|
|
263
|
+
pushTrace(
|
|
264
|
+
defaultShouldRevalidate,
|
|
265
|
+
currentSuggestion,
|
|
266
|
+
`soft-chain:${softNames}`,
|
|
267
|
+
);
|
|
209
268
|
return currentSuggestion;
|
|
210
269
|
}
|