@ivogt/rsc-router 0.0.0-experimental.1
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 +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Pattern Matching
|
|
3
|
+
*
|
|
4
|
+
* Route pattern compilation and matching utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
8
|
+
import type { EntryData } from "../server/context";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parsed segment info
|
|
12
|
+
*/
|
|
13
|
+
interface ParsedSegment {
|
|
14
|
+
type: "static" | "param" | "wildcard";
|
|
15
|
+
value: string; // static text, param name, or "*"
|
|
16
|
+
optional: boolean;
|
|
17
|
+
constraint?: string[]; // enum values like ["en", "gb"]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a route pattern into segments
|
|
22
|
+
*
|
|
23
|
+
* Supports:
|
|
24
|
+
* - Static: /blog, /about
|
|
25
|
+
* - Params: /:slug, /:id
|
|
26
|
+
* - Optional: /:locale?, /:page?
|
|
27
|
+
* - Constrained: /:locale(en|gb), /:type(post|page)
|
|
28
|
+
* - Optional + Constrained: /:locale(en|gb)?
|
|
29
|
+
* - Wildcard: /*
|
|
30
|
+
*/
|
|
31
|
+
function parsePattern(pattern: string): ParsedSegment[] {
|
|
32
|
+
const segments: ParsedSegment[] = [];
|
|
33
|
+
// Match: /segment where segment can be:
|
|
34
|
+
// - static text
|
|
35
|
+
// - :param
|
|
36
|
+
// - :param?
|
|
37
|
+
// - :param(a|b)
|
|
38
|
+
// - :param(a|b)?
|
|
39
|
+
// - *
|
|
40
|
+
const segmentRegex = /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?|(\*)|([^/]+))/g;
|
|
41
|
+
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
44
|
+
const [, , paramName, , constraint, optional, wildcard, staticText] = match;
|
|
45
|
+
|
|
46
|
+
if (wildcard) {
|
|
47
|
+
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
48
|
+
} else if (paramName) {
|
|
49
|
+
segments.push({
|
|
50
|
+
type: "param",
|
|
51
|
+
value: paramName,
|
|
52
|
+
optional: optional === "?",
|
|
53
|
+
constraint: constraint ? constraint.split("|") : undefined,
|
|
54
|
+
});
|
|
55
|
+
} else if (staticText) {
|
|
56
|
+
segments.push({ type: "static", value: staticText, optional: false });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return segments;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compile a route pattern to regex
|
|
65
|
+
*
|
|
66
|
+
* Supports:
|
|
67
|
+
* - Static segments: /blog, /about
|
|
68
|
+
* - Dynamic params: /:slug, /:id
|
|
69
|
+
* - Optional params: /:locale?, /:page?
|
|
70
|
+
* - Constrained params: /:locale(en|gb)
|
|
71
|
+
* - Optional + constrained: /:locale(en|gb)?
|
|
72
|
+
* - Wildcard: /*
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* compilePattern("/blog/:slug") // matches /blog/hello
|
|
76
|
+
* compilePattern("/:locale?/blog") // matches /blog or /en/blog
|
|
77
|
+
* compilePattern("/:locale(en|gb)/blog") // matches /en/blog or /gb/blog
|
|
78
|
+
* compilePattern("/:locale(en|gb)?/blog") // matches /blog, /en/blog, or /gb/blog
|
|
79
|
+
*/
|
|
80
|
+
export function compilePattern(pattern: string): {
|
|
81
|
+
regex: RegExp;
|
|
82
|
+
paramNames: string[];
|
|
83
|
+
optionalParams: Set<string>;
|
|
84
|
+
hasTrailingSlash: boolean;
|
|
85
|
+
} {
|
|
86
|
+
// Detect if pattern has trailing slash (but not just "/")
|
|
87
|
+
const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
88
|
+
// Remove trailing slash for parsing (we'll add it back to regex if needed)
|
|
89
|
+
const normalizedPattern = hasTrailingSlash ? pattern.slice(0, -1) : pattern;
|
|
90
|
+
|
|
91
|
+
const segments = parsePattern(normalizedPattern);
|
|
92
|
+
const paramNames: string[] = [];
|
|
93
|
+
const optionalParams = new Set<string>();
|
|
94
|
+
|
|
95
|
+
let regexPattern = "";
|
|
96
|
+
|
|
97
|
+
for (const segment of segments) {
|
|
98
|
+
if (segment.type === "wildcard") {
|
|
99
|
+
paramNames.push("*");
|
|
100
|
+
regexPattern += "/(.*)";
|
|
101
|
+
} else if (segment.type === "param") {
|
|
102
|
+
paramNames.push(segment.value);
|
|
103
|
+
const valuePattern = segment.constraint
|
|
104
|
+
? `(${segment.constraint.join("|")})`
|
|
105
|
+
: "([^/]+)";
|
|
106
|
+
|
|
107
|
+
if (segment.optional) {
|
|
108
|
+
optionalParams.add(segment.value);
|
|
109
|
+
// Optional: make the whole /segment optional
|
|
110
|
+
regexPattern += `(?:/${valuePattern})?`;
|
|
111
|
+
} else {
|
|
112
|
+
regexPattern += `/${valuePattern}`;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// Static segment
|
|
116
|
+
regexPattern += `/${escapeRegex(segment.value)}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle root path
|
|
121
|
+
if (regexPattern === "") {
|
|
122
|
+
regexPattern = "/";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Add trailing slash to regex if pattern has one
|
|
126
|
+
if (hasTrailingSlash) {
|
|
127
|
+
regexPattern += "/";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
regex: new RegExp(`^${regexPattern}$`),
|
|
132
|
+
paramNames,
|
|
133
|
+
optionalParams,
|
|
134
|
+
hasTrailingSlash,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Escape special regex characters in a string
|
|
140
|
+
*/
|
|
141
|
+
function escapeRegex(str: string): string {
|
|
142
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Match a pathname against registered routes
|
|
147
|
+
*
|
|
148
|
+
* Note: Optional params that are absent in the path will have empty string value.
|
|
149
|
+
* Use the pattern definition to determine if a param is optional.
|
|
150
|
+
*
|
|
151
|
+
* Trailing slash handling (priority order):
|
|
152
|
+
* 1. Per-route `trailingSlash` config from route()
|
|
153
|
+
* 2. Pattern-based detection (pattern ending with `/`)
|
|
154
|
+
*
|
|
155
|
+
* Modes:
|
|
156
|
+
* - "never": Redirect to no trailing slash
|
|
157
|
+
* - "always": Redirect to with trailing slash
|
|
158
|
+
* - "ignore": Match both, no redirect
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* Result of a route match
|
|
162
|
+
*/
|
|
163
|
+
export interface RouteMatchResult<TEnv = any> {
|
|
164
|
+
entry: RouteEntry<TEnv>;
|
|
165
|
+
routeKey: string;
|
|
166
|
+
params: Record<string, string>;
|
|
167
|
+
optionalParams: Set<string>;
|
|
168
|
+
redirectTo?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function findMatch<TEnv>(
|
|
172
|
+
pathname: string,
|
|
173
|
+
routesEntries: RouteEntry<TEnv>[]
|
|
174
|
+
): RouteMatchResult<TEnv> | null {
|
|
175
|
+
const pathnameHasTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
|
|
176
|
+
// Try alternate pathname for redirect matching
|
|
177
|
+
const alternatePathname = pathnameHasTrailingSlash
|
|
178
|
+
? pathname.slice(0, -1)
|
|
179
|
+
: pathname + "/";
|
|
180
|
+
|
|
181
|
+
for (const entry of routesEntries) {
|
|
182
|
+
const routeEntries = Object.entries(entry.routes);
|
|
183
|
+
|
|
184
|
+
for (const [routeKey, pattern] of routeEntries) {
|
|
185
|
+
// Join prefix and pattern, handling edge cases
|
|
186
|
+
let fullPattern: string;
|
|
187
|
+
if (entry.prefix === "" || entry.prefix === "/") {
|
|
188
|
+
fullPattern = pattern;
|
|
189
|
+
} else if (pattern === "/" || pattern === "") {
|
|
190
|
+
fullPattern = entry.prefix;
|
|
191
|
+
} else {
|
|
192
|
+
fullPattern = entry.prefix + pattern;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { regex, paramNames, optionalParams, hasTrailingSlash } = compilePattern(fullPattern);
|
|
196
|
+
|
|
197
|
+
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
198
|
+
const trailingSlashMode: TrailingSlashMode | undefined = entry.trailingSlash?.[routeKey];
|
|
199
|
+
|
|
200
|
+
// Try exact match first
|
|
201
|
+
const match = regex.exec(pathname);
|
|
202
|
+
if (match) {
|
|
203
|
+
const params: Record<string, string> = {};
|
|
204
|
+
paramNames.forEach((name, index) => {
|
|
205
|
+
params[name] = match[index + 1] ?? "";
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Check if trailing slash mode requires redirect even on exact match
|
|
209
|
+
if (trailingSlashMode === "always" && !pathnameHasTrailingSlash && pathname !== "/") {
|
|
210
|
+
// Mode says always have trailing slash, but pathname doesn't have it
|
|
211
|
+
return { entry, routeKey, params, optionalParams, redirectTo: pathname + "/" };
|
|
212
|
+
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
213
|
+
// Mode says never have trailing slash, but pathname has it
|
|
214
|
+
return { entry, routeKey, params, optionalParams, redirectTo: pathname.slice(0, -1) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { entry, routeKey, params, optionalParams };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Try alternate pathname (opposite trailing slash)
|
|
221
|
+
const altMatch = regex.exec(alternatePathname);
|
|
222
|
+
if (altMatch) {
|
|
223
|
+
const params: Record<string, string> = {};
|
|
224
|
+
paramNames.forEach((name, index) => {
|
|
225
|
+
params[name] = altMatch[index + 1] ?? "";
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Determine redirect behavior based on mode
|
|
229
|
+
if (trailingSlashMode === "ignore") {
|
|
230
|
+
// Match without redirect
|
|
231
|
+
return { entry, routeKey, params, optionalParams };
|
|
232
|
+
} else if (trailingSlashMode === "never") {
|
|
233
|
+
// Redirect to no trailing slash
|
|
234
|
+
if (pathnameHasTrailingSlash) {
|
|
235
|
+
return { entry, routeKey, params, optionalParams, redirectTo: alternatePathname };
|
|
236
|
+
}
|
|
237
|
+
return { entry, routeKey, params, optionalParams };
|
|
238
|
+
} else if (trailingSlashMode === "always") {
|
|
239
|
+
// Redirect to with trailing slash
|
|
240
|
+
if (!pathnameHasTrailingSlash) {
|
|
241
|
+
return { entry, routeKey, params, optionalParams, redirectTo: alternatePathname };
|
|
242
|
+
}
|
|
243
|
+
return { entry, routeKey, params, optionalParams };
|
|
244
|
+
} else {
|
|
245
|
+
// No explicit mode - use pattern-based detection
|
|
246
|
+
// Redirect to canonical form (what the pattern defines)
|
|
247
|
+
const canonicalPath = hasTrailingSlash ? alternatePathname : pathname.slice(0, -1);
|
|
248
|
+
return { entry, routeKey, params, optionalParams, redirectTo: canonicalPath };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Traverse from entry to bottom to top, yielding each EntryData
|
|
259
|
+
* e.g. {child -> parent -> grandparent ...}
|
|
260
|
+
*/
|
|
261
|
+
export function* traverseBack(entry: EntryData): Generator<EntryData> {
|
|
262
|
+
let current: EntryData | null = entry;
|
|
263
|
+
const items = [] as EntryData[];
|
|
264
|
+
while (current !== null) {
|
|
265
|
+
items.push(current); // Move up to next parent
|
|
266
|
+
current = current.parent;
|
|
267
|
+
}
|
|
268
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
269
|
+
yield items[i];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Revalidation Logic
|
|
3
|
+
*
|
|
4
|
+
* Evaluates whether segments should revalidate based on params, actions, and custom functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ResolvedSegment, HandlerContext } from "../types";
|
|
8
|
+
import type { ActionContext } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for revalidation evaluation
|
|
12
|
+
*/
|
|
13
|
+
interface EvaluateRevalidationOptions<TEnv> {
|
|
14
|
+
/** Current segment to evaluate */
|
|
15
|
+
segment: ResolvedSegment;
|
|
16
|
+
/** Previous route params (from route match, not segment) */
|
|
17
|
+
prevParams: Record<string, string>;
|
|
18
|
+
/** Lazy function to get previous segment if needed */
|
|
19
|
+
getPrevSegment: (() => Promise<ResolvedSegment | undefined>) | null;
|
|
20
|
+
/** Current request */
|
|
21
|
+
request: Request;
|
|
22
|
+
/** Previous URL */
|
|
23
|
+
prevUrl: URL;
|
|
24
|
+
/** Next URL */
|
|
25
|
+
nextUrl: URL;
|
|
26
|
+
/** Custom revalidation functions */
|
|
27
|
+
revalidations: Array<{ name: string; fn: any }>;
|
|
28
|
+
/** Current route key */
|
|
29
|
+
routeKey: string;
|
|
30
|
+
/** Handler context */
|
|
31
|
+
context: HandlerContext<any, TEnv>;
|
|
32
|
+
/** Action context if triggered by action */
|
|
33
|
+
actionContext?: ActionContext;
|
|
34
|
+
/** If true, this is a stale cache revalidation request */
|
|
35
|
+
stale?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate if a segment should revalidate using soft/hard decision pattern
|
|
40
|
+
* Optimized to use prevParams directly and avoid building previous segments
|
|
41
|
+
*/
|
|
42
|
+
export async function evaluateRevalidation<TEnv>(
|
|
43
|
+
options: EvaluateRevalidationOptions<TEnv>
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
const {
|
|
46
|
+
segment,
|
|
47
|
+
prevParams,
|
|
48
|
+
getPrevSegment,
|
|
49
|
+
request,
|
|
50
|
+
prevUrl,
|
|
51
|
+
nextUrl,
|
|
52
|
+
revalidations,
|
|
53
|
+
routeKey,
|
|
54
|
+
context,
|
|
55
|
+
actionContext,
|
|
56
|
+
stale,
|
|
57
|
+
} = options;
|
|
58
|
+
const nextParams = segment.params || {};
|
|
59
|
+
const paramsChanged =
|
|
60
|
+
Object.keys(nextParams).length !== Object.keys(prevParams).length ||
|
|
61
|
+
Object.keys(nextParams).some(
|
|
62
|
+
(key) => nextParams[key] !== prevParams[key]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Calculate default revalidation based on segment type and request method
|
|
66
|
+
let defaultShouldRevalidate: boolean;
|
|
67
|
+
|
|
68
|
+
if (request.method === "POST") {
|
|
69
|
+
// Actions: revalidate segments that belong to the route, skip parent chain
|
|
70
|
+
if (segment.type === "route") {
|
|
71
|
+
// Route segment always revalidates on actions
|
|
72
|
+
defaultShouldRevalidate = true;
|
|
73
|
+
} else if (segment.type === "loader") {
|
|
74
|
+
// Loaders always revalidate on actions - they often contain action-sensitive data
|
|
75
|
+
// (e.g., cart count after add-to-cart action)
|
|
76
|
+
defaultShouldRevalidate = true;
|
|
77
|
+
} else if (segment.belongsToRoute) {
|
|
78
|
+
// Segment belongs to route (orphan layouts/parallels) - revalidate
|
|
79
|
+
defaultShouldRevalidate = true;
|
|
80
|
+
} else {
|
|
81
|
+
// Parent chain segment (shared layouts/parallels) - don't revalidate
|
|
82
|
+
defaultShouldRevalidate = false;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Navigation (GET): Conservative defaults to minimize unnecessary revalidations
|
|
86
|
+
// Only the route segment revalidates by default - all others require explicit opt-in
|
|
87
|
+
|
|
88
|
+
if (segment.type === "route") {
|
|
89
|
+
// Route segments revalidate when params change
|
|
90
|
+
// Routes are the primary param-dependent content and always need updates
|
|
91
|
+
defaultShouldRevalidate = paramsChanged;
|
|
92
|
+
if (paramsChanged) {
|
|
93
|
+
console.log(
|
|
94
|
+
`[Router.evaluateRevalidation] ${segment.id}: ROUTE - params changed, revalidating`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Layouts and parallels default to no revalidation
|
|
99
|
+
// Cannot assume these segments depend on params without explicit declaration
|
|
100
|
+
// Use custom revalidation functions to opt-in when needed
|
|
101
|
+
defaultShouldRevalidate = false;
|
|
102
|
+
console.log(
|
|
103
|
+
`[Router.evaluateRevalidation] ${
|
|
104
|
+
segment.id
|
|
105
|
+
}: ${segment.type.toUpperCase()} segment - skipping (override with custom revalidation if needed)`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// No custom revalidations defined - return default behavior without prev segment
|
|
111
|
+
if (revalidations.length === 0) {
|
|
112
|
+
if (defaultShouldRevalidate) {
|
|
113
|
+
console.log(
|
|
114
|
+
`[Router.evaluateRevalidation] ${segment.id}: PARAMS CHANGED (default) - revalidating`,
|
|
115
|
+
{ prev: prevParams, next: nextParams }
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(
|
|
119
|
+
`[Router.evaluateRevalidation] ${segment.id}: UNCHANGED (default) - skipping`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return defaultShouldRevalidate;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Custom revalidations exist - may need full prev segment
|
|
126
|
+
// Lazy load prev segment only if getPrevSegment provided
|
|
127
|
+
const prevSegment = getPrevSegment ? await getPrevSegment() : null;
|
|
128
|
+
|
|
129
|
+
// Execute revalidation functions with soft/hard decision pattern
|
|
130
|
+
let currentSuggestion = defaultShouldRevalidate;
|
|
131
|
+
|
|
132
|
+
for (const { name, fn } of revalidations) {
|
|
133
|
+
const result = fn({
|
|
134
|
+
currentParams: prevSegment?.params || prevParams, // Use segment params if available, else route params
|
|
135
|
+
currentUrl: prevUrl,
|
|
136
|
+
nextParams,
|
|
137
|
+
nextUrl,
|
|
138
|
+
defaultShouldRevalidate: currentSuggestion,
|
|
139
|
+
context,
|
|
140
|
+
// Segment metadata (which segment is being evaluated)
|
|
141
|
+
segmentType: segment.type,
|
|
142
|
+
layoutName: segment.layoutName,
|
|
143
|
+
slotName: segment.slot,
|
|
144
|
+
// Action context (only populated when triggered by server action)
|
|
145
|
+
actionId: actionContext?.actionId,
|
|
146
|
+
actionUrl: actionContext?.actionUrl,
|
|
147
|
+
actionResult: actionContext?.actionResult,
|
|
148
|
+
formData: actionContext?.formData,
|
|
149
|
+
method: request.method, // GET for navigation, POST for actions
|
|
150
|
+
routeName: routeKey, // User-friendly route name (e.g., "products.detail")
|
|
151
|
+
// Stale cache context (only true for background revalidation after stale cache render)
|
|
152
|
+
stale,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Check return type:
|
|
156
|
+
// - boolean: hard decision, short-circuit immediately
|
|
157
|
+
// - { defaultShouldRevalidate: boolean }: soft decision, update suggestion and continue
|
|
158
|
+
// - null/undefined: use default behavior (equivalent to returning { defaultShouldRevalidate })
|
|
159
|
+
if (typeof result === "boolean") {
|
|
160
|
+
// Hard decision - short-circuit
|
|
161
|
+
console.log(
|
|
162
|
+
`[Router.evaluateRevalidation] ${segment.id}: REVALIDATE (${name}) HARD: ${result}`
|
|
163
|
+
);
|
|
164
|
+
return result;
|
|
165
|
+
} else if (
|
|
166
|
+
result &&
|
|
167
|
+
typeof result === "object" &&
|
|
168
|
+
"defaultShouldRevalidate" in result
|
|
169
|
+
) {
|
|
170
|
+
// Soft decision - update suggestion and continue
|
|
171
|
+
currentSuggestion = result.defaultShouldRevalidate;
|
|
172
|
+
console.log(
|
|
173
|
+
`[Router.evaluateRevalidation] ${segment.id}: REVALIDATE (${name}) SOFT: ${currentSuggestion}`
|
|
174
|
+
);
|
|
175
|
+
} else if (result === null || result === undefined) {
|
|
176
|
+
// Defer to default - equivalent to { defaultShouldRevalidate: currentSuggestion }
|
|
177
|
+
// This means "I don't care, use whatever the default is"
|
|
178
|
+
console.log(
|
|
179
|
+
`[Router.evaluateRevalidation] ${segment.id}: REVALIDATE (${name}) DEFER to default: ${currentSuggestion}`
|
|
180
|
+
);
|
|
181
|
+
// currentSuggestion stays the same, continue to next function
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// All revalidators completed - use final suggestion
|
|
186
|
+
console.log(
|
|
187
|
+
`[Router.evaluateRevalidation] ${segment.id}: Final decision: ${currentSuggestion}`
|
|
188
|
+
);
|
|
189
|
+
return currentSuggestion;
|
|
190
|
+
}
|