@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
|
@@ -16,6 +16,7 @@ export interface ParsedSegment {
|
|
|
16
16
|
value: string; // static text, param name, or "*"
|
|
17
17
|
optional: boolean;
|
|
18
18
|
constraint?: string[]; // enum values like ["en", "gb"]
|
|
19
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -39,11 +40,21 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
39
40
|
// - :param(a|b)?
|
|
40
41
|
// - *
|
|
41
42
|
const segmentRegex =
|
|
42
|
-
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)
|
|
43
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
43
44
|
|
|
44
45
|
let match;
|
|
45
46
|
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
46
|
-
const [
|
|
47
|
+
const [
|
|
48
|
+
,
|
|
49
|
+
,
|
|
50
|
+
paramName,
|
|
51
|
+
,
|
|
52
|
+
constraint,
|
|
53
|
+
optional,
|
|
54
|
+
suffix,
|
|
55
|
+
wildcard,
|
|
56
|
+
staticText,
|
|
57
|
+
] = match;
|
|
47
58
|
|
|
48
59
|
if (wildcard) {
|
|
49
60
|
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
@@ -53,6 +64,7 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
53
64
|
value: paramName,
|
|
54
65
|
optional: optional === "?",
|
|
55
66
|
constraint: constraint ? constraint.split("|") : undefined,
|
|
67
|
+
suffix: suffix || undefined,
|
|
56
68
|
});
|
|
57
69
|
} else if (staticText) {
|
|
58
70
|
segments.push({ type: "static", value: staticText, optional: false });
|
|
@@ -139,16 +151,19 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
139
151
|
regexPattern += "/(.*)";
|
|
140
152
|
} else if (segment.type === "param") {
|
|
141
153
|
paramNames.push(segment.value);
|
|
154
|
+
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
142
155
|
const valuePattern = segment.constraint
|
|
143
|
-
? `(${segment.constraint.join("|")})`
|
|
144
|
-
:
|
|
156
|
+
? `(${segment.constraint.map(escapeRegex).join("|")})`
|
|
157
|
+
: segment.suffix
|
|
158
|
+
? "([^/]+?)"
|
|
159
|
+
: "([^/]+)";
|
|
145
160
|
|
|
146
161
|
if (segment.optional) {
|
|
147
162
|
optionalParams.add(segment.value);
|
|
148
163
|
// Optional: make the whole /segment optional
|
|
149
|
-
regexPattern += `(?:/${valuePattern})?`;
|
|
164
|
+
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
150
165
|
} else {
|
|
151
|
-
regexPattern += `/${valuePattern}`;
|
|
166
|
+
regexPattern += `/${valuePattern}${suffixPattern}`;
|
|
152
167
|
}
|
|
153
168
|
} else {
|
|
154
169
|
// Static segment
|
|
@@ -388,6 +403,9 @@ export function findMatch<TEnv>(
|
|
|
388
403
|
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
389
404
|
? { pr: true as const }
|
|
390
405
|
: {};
|
|
406
|
+
const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
|
|
407
|
+
? { pt: true as const }
|
|
408
|
+
: {};
|
|
391
409
|
|
|
392
410
|
// Try exact match first
|
|
393
411
|
const match = regex.exec(pathname);
|
|
@@ -419,6 +437,7 @@ export function findMatch<TEnv>(
|
|
|
419
437
|
optionalParams,
|
|
420
438
|
redirectTo: pathname + "/",
|
|
421
439
|
...prFlag,
|
|
440
|
+
...ptFlag,
|
|
422
441
|
};
|
|
423
442
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
424
443
|
// Mode says never have trailing slash, but pathname has it
|
|
@@ -429,10 +448,18 @@ export function findMatch<TEnv>(
|
|
|
429
448
|
optionalParams,
|
|
430
449
|
redirectTo: pathname.slice(0, -1),
|
|
431
450
|
...prFlag,
|
|
451
|
+
...ptFlag,
|
|
432
452
|
};
|
|
433
453
|
}
|
|
434
454
|
|
|
435
|
-
return {
|
|
455
|
+
return {
|
|
456
|
+
entry,
|
|
457
|
+
routeKey,
|
|
458
|
+
params,
|
|
459
|
+
optionalParams,
|
|
460
|
+
...prFlag,
|
|
461
|
+
...ptFlag,
|
|
462
|
+
};
|
|
436
463
|
}
|
|
437
464
|
|
|
438
465
|
// Try alternate pathname (opposite trailing slash)
|
|
@@ -446,7 +473,14 @@ export function findMatch<TEnv>(
|
|
|
446
473
|
// Determine redirect behavior based on mode
|
|
447
474
|
if (trailingSlashMode === "ignore") {
|
|
448
475
|
// Match without redirect
|
|
449
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
entry,
|
|
478
|
+
routeKey,
|
|
479
|
+
params,
|
|
480
|
+
optionalParams,
|
|
481
|
+
...prFlag,
|
|
482
|
+
...ptFlag,
|
|
483
|
+
};
|
|
450
484
|
} else if (trailingSlashMode === "never") {
|
|
451
485
|
// Redirect to no trailing slash
|
|
452
486
|
if (pathnameHasTrailingSlash) {
|
|
@@ -457,9 +491,17 @@ export function findMatch<TEnv>(
|
|
|
457
491
|
optionalParams,
|
|
458
492
|
redirectTo: alternatePathname,
|
|
459
493
|
...prFlag,
|
|
494
|
+
...ptFlag,
|
|
460
495
|
};
|
|
461
496
|
}
|
|
462
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
entry,
|
|
499
|
+
routeKey,
|
|
500
|
+
params,
|
|
501
|
+
optionalParams,
|
|
502
|
+
...prFlag,
|
|
503
|
+
...ptFlag,
|
|
504
|
+
};
|
|
463
505
|
} else if (trailingSlashMode === "always") {
|
|
464
506
|
// Redirect to with trailing slash
|
|
465
507
|
if (!pathnameHasTrailingSlash) {
|
|
@@ -470,9 +512,17 @@ export function findMatch<TEnv>(
|
|
|
470
512
|
optionalParams,
|
|
471
513
|
redirectTo: alternatePathname,
|
|
472
514
|
...prFlag,
|
|
515
|
+
...ptFlag,
|
|
473
516
|
};
|
|
474
517
|
}
|
|
475
|
-
return {
|
|
518
|
+
return {
|
|
519
|
+
entry,
|
|
520
|
+
routeKey,
|
|
521
|
+
params,
|
|
522
|
+
optionalParams,
|
|
523
|
+
...prFlag,
|
|
524
|
+
...ptFlag,
|
|
525
|
+
};
|
|
476
526
|
} else {
|
|
477
527
|
// No explicit mode - use pattern-based detection
|
|
478
528
|
// Redirect to canonical form (what the pattern defines)
|
|
@@ -486,6 +536,7 @@ export function findMatch<TEnv>(
|
|
|
486
536
|
optionalParams,
|
|
487
537
|
redirectTo: canonicalPath,
|
|
488
538
|
...prFlag,
|
|
539
|
+
...ptFlag,
|
|
489
540
|
};
|
|
490
541
|
}
|
|
491
542
|
}
|
|
@@ -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();
|
|
@@ -110,6 +115,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
110
115
|
setCookie: () => {},
|
|
111
116
|
deleteCookie: () => {},
|
|
112
117
|
header: () => {},
|
|
118
|
+
setStatus: () => {},
|
|
113
119
|
use: (() => {
|
|
114
120
|
throw new Error("use() not available during pre-rendering");
|
|
115
121
|
}) as any,
|
|
@@ -120,10 +126,12 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
120
126
|
_onResponseCallbacks: [],
|
|
121
127
|
setLocationState() {},
|
|
122
128
|
_locationState: undefined,
|
|
129
|
+
_reportedErrors: new WeakSet<object>(),
|
|
123
130
|
reverse: createReverseFunction(
|
|
124
131
|
deps.mergedRouteMap,
|
|
125
132
|
matched.routeKey,
|
|
126
133
|
matchedParams,
|
|
134
|
+
matched.routeKey ? isRouteRootScoped(matched.routeKey) : undefined,
|
|
127
135
|
),
|
|
128
136
|
};
|
|
129
137
|
|
|
@@ -137,6 +145,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
137
145
|
deps.mergedRouteMap,
|
|
138
146
|
matched.routeKey,
|
|
139
147
|
variables,
|
|
148
|
+
matchedPassthroughRoute,
|
|
140
149
|
);
|
|
141
150
|
|
|
142
151
|
// 7. Wire use() for handles only (loaders throw)
|
|
@@ -153,17 +162,31 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
153
162
|
{ skipLoaders: true },
|
|
154
163
|
);
|
|
155
164
|
|
|
156
|
-
// 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)
|
|
157
179
|
const nonLoaderSegments = allSegments.filter((s) => s.type !== "loader");
|
|
158
180
|
|
|
159
|
-
//
|
|
181
|
+
// 11. Wait for handles to settle
|
|
182
|
+
handleStore.seal();
|
|
160
183
|
await handleStore.settled;
|
|
161
184
|
|
|
162
|
-
//
|
|
185
|
+
// 12. Serialize segments using the cache serializer
|
|
163
186
|
const { serializeSegments } = await import("../cache/segment-codec.js");
|
|
164
187
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
165
188
|
|
|
166
|
-
//
|
|
189
|
+
// 13. Collect handle data per segment (skip segments with no handle data)
|
|
167
190
|
const handles: Record<string, SegmentHandleData> = {};
|
|
168
191
|
for (const seg of nonLoaderSegments) {
|
|
169
192
|
const segHandles = handleStore.getDataForSegment(seg.id);
|
|
@@ -175,7 +198,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
175
198
|
// Use the trie-level route key (e.g., "docs", "docs.article")
|
|
176
199
|
const routeName = matched.routeKey;
|
|
177
200
|
|
|
178
|
-
//
|
|
201
|
+
// 14. Resolve intercept segments for this route (if any ancestor defines
|
|
179
202
|
// an intercept targeting this route). At build time we skip when()
|
|
180
203
|
// evaluation -- we pre-render all intercepts unconditionally and let
|
|
181
204
|
// runtime matching decide which to serve.
|
|
@@ -320,6 +343,7 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
320
343
|
setCookie: () => {},
|
|
321
344
|
deleteCookie: () => {},
|
|
322
345
|
header: () => {},
|
|
346
|
+
setStatus: () => {},
|
|
323
347
|
use: (() => {
|
|
324
348
|
throw new Error("use() not available during static pre-rendering");
|
|
325
349
|
}) as any,
|
|
@@ -330,7 +354,13 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
330
354
|
_onResponseCallbacks: [],
|
|
331
355
|
setLocationState() {},
|
|
332
356
|
_locationState: undefined,
|
|
333
|
-
|
|
357
|
+
_reportedErrors: new WeakSet<object>(),
|
|
358
|
+
reverse: createReverseFunction(
|
|
359
|
+
mergedRouteMap,
|
|
360
|
+
routeName,
|
|
361
|
+
{},
|
|
362
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
363
|
+
),
|
|
334
364
|
};
|
|
335
365
|
|
|
336
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
|
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ShouldRevalidateFn,
|
|
19
19
|
} from "../types.js";
|
|
20
20
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
21
|
+
import type { TelemetrySink } from "./telemetry.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Revalidation context passed to segment resolution
|
|
@@ -79,6 +80,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
79
80
|
routeMap?: Record<string, string>,
|
|
80
81
|
routeName?: string,
|
|
81
82
|
responseType?: string,
|
|
83
|
+
isPassthroughRoute?: boolean,
|
|
82
84
|
) => HandlerContext<any, TEnv>;
|
|
83
85
|
|
|
84
86
|
// Loader setup
|
|
@@ -181,6 +183,12 @@ export interface RouterContext<TEnv = any> {
|
|
|
181
183
|
context: HandlerContext<any, TEnv>;
|
|
182
184
|
actionContext?: any;
|
|
183
185
|
stale?: boolean;
|
|
186
|
+
traceSource?:
|
|
187
|
+
| "segment-resolution"
|
|
188
|
+
| "cache-hit"
|
|
189
|
+
| "loader"
|
|
190
|
+
| "parallel"
|
|
191
|
+
| "orphan-layout";
|
|
184
192
|
}) => Promise<boolean>;
|
|
185
193
|
|
|
186
194
|
// Request context
|
|
@@ -234,6 +242,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
234
242
|
nextUrl: URL,
|
|
235
243
|
routeKey: string,
|
|
236
244
|
actionContext?: any,
|
|
245
|
+
stale?: boolean,
|
|
237
246
|
) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
|
|
238
247
|
|
|
239
248
|
// Entry revalidation map
|
|
@@ -241,6 +250,12 @@ export interface RouterContext<TEnv = any> {
|
|
|
241
250
|
entries: EntryData[],
|
|
242
251
|
) => Map<string, { revalidate: ShouldRevalidateFn[] }>;
|
|
243
252
|
|
|
253
|
+
// Telemetry sink (optional, no-op when undefined)
|
|
254
|
+
telemetry?: TelemetrySink;
|
|
255
|
+
|
|
256
|
+
// Request ID for telemetry span correlation (set per-request in match handlers)
|
|
257
|
+
requestId?: string;
|
|
258
|
+
|
|
244
259
|
// Intercept loaders only (for cache hit + intercept scenarios)
|
|
245
260
|
resolveInterceptLoadersOnly?: (
|
|
246
261
|
intercept: InterceptEntry,
|