@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.27
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 +71 -3
- 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,16 +137,29 @@ 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,
|
|
108
146
|
});
|
|
109
147
|
}
|
|
148
|
+
} else if (segment.belongsToRoute && paramsChanged) {
|
|
149
|
+
// Children of the route path (loaders, orphan layouts/parallels)
|
|
150
|
+
// revalidate when params change — they likely depend on route params
|
|
151
|
+
defaultShouldRevalidate = true;
|
|
152
|
+
defaultReason = "nav:route-child-params-changed";
|
|
153
|
+
debugLog("revalidation", "route child revalidating (params changed)", {
|
|
154
|
+
segmentId: segment.id,
|
|
155
|
+
segmentType: segment.type,
|
|
156
|
+
});
|
|
110
157
|
} else {
|
|
111
|
-
//
|
|
158
|
+
// Parent layouts and parallels default to no revalidation
|
|
112
159
|
// Cannot assume these segments depend on params without explicit declaration
|
|
113
160
|
// Use custom revalidation functions to opt-in when needed
|
|
114
161
|
defaultShouldRevalidate = false;
|
|
162
|
+
defaultReason = "nav:non-route-skip";
|
|
115
163
|
debugLog("revalidation", "non-route segment skipped by default", {
|
|
116
164
|
segmentId: segment.id,
|
|
117
165
|
segmentType: segment.type,
|
|
@@ -132,6 +180,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
132
180
|
segmentId: segment.id,
|
|
133
181
|
});
|
|
134
182
|
}
|
|
183
|
+
pushTrace(defaultShouldRevalidate, defaultShouldRevalidate, defaultReason);
|
|
135
184
|
return defaultShouldRevalidate;
|
|
136
185
|
}
|
|
137
186
|
|
|
@@ -142,6 +191,16 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
142
191
|
// Execute revalidation functions with soft/hard decision pattern
|
|
143
192
|
let currentSuggestion = defaultShouldRevalidate;
|
|
144
193
|
|
|
194
|
+
// Compute public route names (filtered: undefined for auto-generated routes)
|
|
195
|
+
const toRouteName =
|
|
196
|
+
routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
|
|
197
|
+
const reqCtx = _getRequestContext();
|
|
198
|
+
const prevRouteKey = reqCtx?._prevRouteKey;
|
|
199
|
+
const fromRouteName =
|
|
200
|
+
prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
|
|
201
|
+
? prevRouteKey
|
|
202
|
+
: undefined;
|
|
203
|
+
|
|
145
204
|
for (const { name, fn } of revalidations) {
|
|
146
205
|
const result = fn({
|
|
147
206
|
currentParams: prevSegment?.params || prevParams, // Use segment params if available, else route params
|
|
@@ -160,7 +219,9 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
160
219
|
actionResult: actionContext?.actionResult,
|
|
161
220
|
formData: actionContext?.formData,
|
|
162
221
|
method: request.method, // GET for navigation, POST for actions
|
|
163
|
-
routeName:
|
|
222
|
+
routeName: toRouteName, // Navigation target route name (filtered)
|
|
223
|
+
fromRouteName, // Navigation source route name (filtered)
|
|
224
|
+
toRouteName, // Navigation target route name (filtered)
|
|
164
225
|
// Stale cache context (only true for background revalidation after stale cache render)
|
|
165
226
|
stale,
|
|
166
227
|
});
|
|
@@ -176,6 +237,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
176
237
|
revalidator: name,
|
|
177
238
|
revalidate: result,
|
|
178
239
|
});
|
|
240
|
+
pushTrace(defaultShouldRevalidate, result, `hard:${name}`);
|
|
179
241
|
return result;
|
|
180
242
|
} else if (
|
|
181
243
|
result &&
|
|
@@ -206,5 +268,11 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
206
268
|
segmentId: segment.id,
|
|
207
269
|
revalidate: currentSuggestion,
|
|
208
270
|
});
|
|
271
|
+
const softNames = revalidations.map((r) => r.name).join(",");
|
|
272
|
+
pushTrace(
|
|
273
|
+
defaultShouldRevalidate,
|
|
274
|
+
currentSuggestion,
|
|
275
|
+
`soft-chain:${softNames}`,
|
|
276
|
+
);
|
|
209
277
|
return currentSuggestion;
|
|
210
278
|
}
|
|
@@ -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,
|