@rangojs/router 0.0.0-experimental.56cb65a7 → 0.0.0-experimental.57
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/dist/bin/rango.js +128 -46
- package/dist/vite/index.js +211 -47
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +67 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +16 -3
- package/src/browser/navigation-client.ts +64 -40
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +37 -4
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +44 -8
- package/src/browser/react/NavigationProvider.tsx +13 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +3 -0
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +9 -1
- package/src/router/handler-context.ts +36 -17
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +9 -2
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +50 -7
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-result.ts +11 -5
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +2 -5
- package/src/router/prerender-match.ts +2 -2
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +25 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +47 -16
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +50 -21
- package/src/router/types.ts +1 -0
- package/src/router.ts +41 -4
- package/src/rsc/handler.ts +11 -2
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/rsc/types.ts +8 -1
- package/src/server/context.ts +36 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +50 -12
- package/src/ssr/index.tsx +3 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +125 -31
- package/src/types/loader-types.ts +4 -5
- package/src/urls/pattern-types.ts +12 -0
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/rango.ts +17 -1
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -228,11 +228,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
228
228
|
* revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
229
229
|
* ])
|
|
230
230
|
*
|
|
231
|
-
* //
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* }
|
|
231
|
+
* // Consume in client components with useLoader()
|
|
232
|
+
* // (preferred — cache-safe, always fresh)
|
|
233
|
+
* function ProductDetails() {
|
|
234
|
+
* const { data } = useLoader(ProductLoader);
|
|
235
|
+
* return <div>{data.name}</div>;
|
|
236
|
+
* }
|
|
236
237
|
* ```
|
|
237
238
|
* @param loaderDef - Loader created with createLoader()
|
|
238
239
|
* @param use - Optional callback for loader-specific revalidation rules
|
|
@@ -2,6 +2,7 @@ import type { LocationStateEntry } from "../browser/react/location-state-shared.
|
|
|
2
2
|
import {
|
|
3
3
|
requireRequestContext,
|
|
4
4
|
getRequestContext,
|
|
5
|
+
_getRequestContext,
|
|
5
6
|
} from "../server/request-context.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -83,10 +84,17 @@ export function redirect(
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Auto-prefix root-relative URLs with basename for app-local redirects.
|
|
88
|
+
const bn = _getRequestContext()?._basename;
|
|
89
|
+
let resolvedUrl = url;
|
|
90
|
+
if (bn && url.startsWith("/") && !url.startsWith(bn + "/") && url !== bn) {
|
|
91
|
+
resolvedUrl = url === "/" ? bn : bn + url;
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
return new Response(null, {
|
|
87
95
|
status,
|
|
88
96
|
headers: {
|
|
89
|
-
Location:
|
|
97
|
+
Location: resolvedUrl,
|
|
90
98
|
"X-RSC-Redirect": "soft",
|
|
91
99
|
},
|
|
92
100
|
});
|
|
@@ -8,7 +8,13 @@ import type { HandlerContext, InternalHandlerContext } from "../types";
|
|
|
8
8
|
import { _getRequestContext } from "../server/request-context.js";
|
|
9
9
|
import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
|
|
10
10
|
import { parseSearchParams, serializeSearchParams } from "../search-params.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
contextGet,
|
|
13
|
+
contextSet,
|
|
14
|
+
isNonCacheable,
|
|
15
|
+
type ContextSetOptions,
|
|
16
|
+
} from "../context-var.js";
|
|
17
|
+
import { isInsideCacheScope } from "../server/context.js";
|
|
12
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
13
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
14
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
@@ -201,7 +207,7 @@ export function createHandlerContext<TEnv>(
|
|
|
201
207
|
// Get variables from request context - this is the unified context
|
|
202
208
|
// shared between middleware and route handlers
|
|
203
209
|
const requestContext = _getRequestContext();
|
|
204
|
-
const variables: any = requestContext?.
|
|
210
|
+
const variables: any = requestContext?._variables ?? {};
|
|
205
211
|
|
|
206
212
|
// If route has a search schema, parse URLSearchParams into typed object
|
|
207
213
|
const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
|
|
@@ -213,7 +219,7 @@ export function createHandlerContext<TEnv>(
|
|
|
213
219
|
const stubResponse =
|
|
214
220
|
requestContext?.res ?? new Response(null, { status: 200 });
|
|
215
221
|
|
|
216
|
-
// Guard mutating Headers methods so they throw inside "use cache"
|
|
222
|
+
// Guard mutating Headers methods so they throw inside "use cache" or cache() scope.
|
|
217
223
|
// Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
|
|
218
224
|
// is stamped by cache-runtime, not the shared request context.
|
|
219
225
|
const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
|
|
@@ -225,6 +231,13 @@ export function createHandlerContext<TEnv>(
|
|
|
225
231
|
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
226
232
|
return (...args: any[]) => {
|
|
227
233
|
assertNotInsideCacheExec(ctx, "headers");
|
|
234
|
+
if (isInsideCacheScope()) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`ctx.headers.${String(prop)}() cannot be called inside a cache() boundary. ` +
|
|
237
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
238
|
+
`Move header mutations to a middleware or layout outside the cache() scope.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
228
241
|
return value.apply(target, args);
|
|
229
242
|
};
|
|
230
243
|
}
|
|
@@ -244,14 +257,24 @@ export function createHandlerContext<TEnv>(
|
|
|
244
257
|
url,
|
|
245
258
|
originalUrl: new URL(request.url),
|
|
246
259
|
env: bindings,
|
|
247
|
-
|
|
248
|
-
get: ((keyOrVar: any) =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
260
|
+
_variables: variables,
|
|
261
|
+
get: ((keyOrVar: any) => {
|
|
262
|
+
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
263
|
+
// Works for both ContextVar tokens and string keys.
|
|
264
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
267
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
268
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return contextGet(variables, keyOrVar);
|
|
272
|
+
}) as HandlerContext<any, TEnv>["get"],
|
|
273
|
+
set: ((keyOrVar: any, value: any, options?: ContextSetOptions) => {
|
|
253
274
|
assertNotInsideCacheExec(ctx, "set");
|
|
254
|
-
|
|
275
|
+
// Write is dumb: store value + non-cacheable metadata.
|
|
276
|
+
// Enforcement happens at read time via ctx.get().
|
|
277
|
+
contextSet(variables, keyOrVar, value, options);
|
|
255
278
|
}) as HandlerContext<any, TEnv>["set"],
|
|
256
279
|
res: stubResponse, // Stub response for setting headers
|
|
257
280
|
headers: guardedHeaders, // Guarded shorthand for res.headers
|
|
@@ -297,7 +320,7 @@ export function createHandlerContext<TEnv>(
|
|
|
297
320
|
*
|
|
298
321
|
* Returns an InternalHandlerContext where params, pathname, url, searchParams,
|
|
299
322
|
* search, reverse, and use(handle) work. Request-time properties
|
|
300
|
-
* (request, env, headers, cookies,
|
|
323
|
+
* (request, env, headers, cookies, get, set, res) throw with a clear error.
|
|
301
324
|
*/
|
|
302
325
|
export function createPrerenderContext<TEnv>(
|
|
303
326
|
params: Record<string, string>,
|
|
@@ -331,9 +354,7 @@ export function createPrerenderContext<TEnv>(
|
|
|
331
354
|
get env(): TEnv {
|
|
332
355
|
return throwUnavailable("env");
|
|
333
356
|
},
|
|
334
|
-
|
|
335
|
-
return throwUnavailable("var");
|
|
336
|
-
},
|
|
357
|
+
_variables: variables,
|
|
337
358
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
338
359
|
set: ((keyOrVar: any, value: any) => {
|
|
339
360
|
contextSet(variables, keyOrVar, value);
|
|
@@ -415,9 +436,7 @@ export function createStaticContext<TEnv>(
|
|
|
415
436
|
get env(): TEnv {
|
|
416
437
|
return throwUnavailable("env");
|
|
417
438
|
},
|
|
418
|
-
|
|
419
|
-
return throwUnavailable("var");
|
|
420
|
-
},
|
|
439
|
+
_variables: variables,
|
|
421
440
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
422
441
|
set: ((keyOrVar: any, value: any) => {
|
|
423
442
|
contextSet(variables, keyOrVar, value);
|
|
@@ -11,7 +11,11 @@ import type {
|
|
|
11
11
|
InterceptEntry,
|
|
12
12
|
InterceptSelectorContext,
|
|
13
13
|
} from "../server/context";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
HandlerContext,
|
|
16
|
+
InternalHandlerContext,
|
|
17
|
+
ResolvedSegment,
|
|
18
|
+
} from "../types";
|
|
15
19
|
import { evaluateRevalidation } from "./revalidation.js";
|
|
16
20
|
import { getRequestContext } from "../server/request-context.js";
|
|
17
21
|
import { executeInterceptMiddleware } from "./middleware.js";
|
|
@@ -20,6 +24,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
|
20
24
|
import { handleHandlerResult } from "./segment-resolution.js";
|
|
21
25
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
22
26
|
import { debugLog } from "./logging.js";
|
|
27
|
+
import { runInsideLoaderScope } from "../server/context.js";
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Check if an intercept's when conditions are satisfied.
|
|
@@ -133,7 +138,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
133
138
|
context.request,
|
|
134
139
|
context.env,
|
|
135
140
|
params,
|
|
136
|
-
context
|
|
141
|
+
(context as InternalHandlerContext<any, TEnv>)._variables,
|
|
137
142
|
requestCtx.res,
|
|
138
143
|
createReverseFunction(getGlobalRouteMap()),
|
|
139
144
|
);
|
|
@@ -207,7 +212,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
207
212
|
loaderIds.push(loader.$$id);
|
|
208
213
|
loaderPromises.push(
|
|
209
214
|
deps.wrapLoaderPromise(
|
|
210
|
-
context.use(loader),
|
|
215
|
+
runInsideLoaderScope(() => context.use(loader)),
|
|
211
216
|
parentEntry,
|
|
212
217
|
segmentId,
|
|
213
218
|
context.pathname,
|
|
@@ -374,7 +379,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
374
379
|
loaderIds.push(loader.$$id);
|
|
375
380
|
loaderPromises.push(
|
|
376
381
|
deps.wrapLoaderPromise(
|
|
377
|
-
context.use(loader),
|
|
382
|
+
runInsideLoaderScope(() => context.use(loader)),
|
|
378
383
|
parentEntry,
|
|
379
384
|
segmentId,
|
|
380
385
|
context.pathname,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import { track } from "../server/context";
|
|
9
9
|
import type { EntryData } from "../server/context";
|
|
10
|
+
import { contextGet } from "../context-var.js";
|
|
10
11
|
import type {
|
|
11
12
|
ResolvedSegment,
|
|
12
13
|
HandlerContext,
|
|
@@ -241,6 +242,12 @@ function createLoaderExecutor<TEnv>(
|
|
|
241
242
|
pendingLoaders.add(loader.$$id);
|
|
242
243
|
|
|
243
244
|
const currentLoaderId = loader.$$id;
|
|
245
|
+
const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
|
|
246
|
+
// Loader functions are always fresh (never cached), so they get an
|
|
247
|
+
// unguarded get that bypasses non-cacheable read guards. This applies
|
|
248
|
+
// to ALL loaders — DSL and handler-called — because the loader
|
|
249
|
+
// function itself always re-executes. Also handles nested deps
|
|
250
|
+
// (loaderA → use(loaderB)) since all share this unguarded get.
|
|
244
251
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
245
252
|
params: ctx.params,
|
|
246
253
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
@@ -250,8 +257,8 @@ function createLoaderExecutor<TEnv>(
|
|
|
250
257
|
pathname: ctx.pathname,
|
|
251
258
|
url: ctx.url,
|
|
252
259
|
env: ctx.env,
|
|
253
|
-
|
|
254
|
-
|
|
260
|
+
get: ((keyOrVar: any) =>
|
|
261
|
+
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
255
262
|
use: <TDep, TDepParams = any>(
|
|
256
263
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
257
264
|
): Promise<TDep> => {
|
|
@@ -149,6 +149,13 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
149
149
|
: undefined;
|
|
150
150
|
|
|
151
151
|
requestCtx?.waitUntil(async () => {
|
|
152
|
+
// Prevent background metrics from polluting foreground timeline.
|
|
153
|
+
// The foreground uses its own metricsStore reference directly (via
|
|
154
|
+
// appendMetric), so nulling Store.metrics only affects track() calls
|
|
155
|
+
// inside this background Store.run() scope.
|
|
156
|
+
const savedMetrics = ctx.Store.metrics;
|
|
157
|
+
ctx.Store.metrics = undefined;
|
|
158
|
+
|
|
152
159
|
const start = performance.now();
|
|
153
160
|
debugLog("backgroundRevalidation", "revalidating stale route", {
|
|
154
161
|
pathname: ctx.pathname,
|
|
@@ -179,7 +186,9 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
179
186
|
setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
|
|
180
187
|
|
|
181
188
|
// Resolve all segments fresh (without revalidation logic)
|
|
182
|
-
// to ensure complete components for caching
|
|
189
|
+
// to ensure complete components for caching.
|
|
190
|
+
// Skip DSL loaders — they are never cached (cacheRoute filters them)
|
|
191
|
+
// and are always resolved fresh on each request.
|
|
183
192
|
const freshSegments = await ctx.Store.run(() =>
|
|
184
193
|
resolveAllSegments(
|
|
185
194
|
ctx.entries,
|
|
@@ -187,6 +196,7 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
187
196
|
ctx.matched.params,
|
|
188
197
|
freshHandlerContext,
|
|
189
198
|
freshLoaderPromises,
|
|
199
|
+
{ skipLoaders: true },
|
|
190
200
|
),
|
|
191
201
|
);
|
|
192
202
|
|
|
@@ -234,6 +244,7 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
234
244
|
});
|
|
235
245
|
} finally {
|
|
236
246
|
requestCtx._handleStore = originalHandleStore;
|
|
247
|
+
ctx.Store.metrics = savedMetrics;
|
|
237
248
|
}
|
|
238
249
|
});
|
|
239
250
|
};
|
|
@@ -70,9 +70,11 @@
|
|
|
70
70
|
* - No segments yielded from this middleware
|
|
71
71
|
*
|
|
72
72
|
* Loaders:
|
|
73
|
-
* - NEVER cached
|
|
73
|
+
* - NEVER cached in the segment cache
|
|
74
74
|
* - Always resolved fresh on every request
|
|
75
75
|
* - Ensures data freshness even with cached UI components
|
|
76
|
+
* - Segment cache staleness does NOT propagate to loader revalidation;
|
|
77
|
+
* loaders use their own revalidation rules (actionId, user-defined)
|
|
76
78
|
*
|
|
77
79
|
*
|
|
78
80
|
* REVALIDATION RULES
|
|
@@ -261,7 +263,7 @@ async function* yieldFromStore<TEnv>(
|
|
|
261
263
|
depth: 1,
|
|
262
264
|
});
|
|
263
265
|
ms.metrics.push({
|
|
264
|
-
label: "pipeline:cache-
|
|
266
|
+
label: "pipeline:cache-hit",
|
|
265
267
|
duration: loaderEnd - pipelineStart,
|
|
266
268
|
startTime: pipelineStart - ms.requestStart,
|
|
267
269
|
});
|
|
@@ -314,7 +316,10 @@ export function withCacheLookup<TEnv>(
|
|
|
314
316
|
|
|
315
317
|
// Prerender lookup: check build-time cached data before runtime cache.
|
|
316
318
|
// Prerender data is available regardless of runtime cache configuration.
|
|
317
|
-
|
|
319
|
+
// Skip for HMR requests — the dev prerender endpoint reads from a stale
|
|
320
|
+
// RouterRegistry snapshot; rendering fresh ensures edits are visible.
|
|
321
|
+
const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
|
|
322
|
+
if (!ctx.isAction && !isHmr && ctx.matched.pr) {
|
|
318
323
|
await ensurePrerenderDeps();
|
|
319
324
|
if (prerenderStoreInstance) {
|
|
320
325
|
const paramHash = _hashParams!(ctx.matched.params);
|
|
@@ -446,7 +451,7 @@ export function withCacheLookup<TEnv>(
|
|
|
446
451
|
yield* source;
|
|
447
452
|
if (ms) {
|
|
448
453
|
ms.metrics.push({
|
|
449
|
-
label: "pipeline:cache-
|
|
454
|
+
label: "pipeline:cache-miss",
|
|
450
455
|
duration: performance.now() - pipelineStart,
|
|
451
456
|
startTime: pipelineStart - ms.requestStart,
|
|
452
457
|
});
|
|
@@ -466,7 +471,7 @@ export function withCacheLookup<TEnv>(
|
|
|
466
471
|
yield* source;
|
|
467
472
|
if (ms) {
|
|
468
473
|
ms.metrics.push({
|
|
469
|
-
label: "pipeline:cache-
|
|
474
|
+
label: "pipeline:cache-miss",
|
|
470
475
|
duration: performance.now() - pipelineStart,
|
|
471
476
|
startTime: pipelineStart - ms.requestStart,
|
|
472
477
|
});
|
|
@@ -518,7 +523,41 @@ export function withCacheLookup<TEnv>(
|
|
|
518
523
|
|
|
519
524
|
// Look up revalidation rules for this segment
|
|
520
525
|
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
526
|
+
|
|
527
|
+
// Even without explicit revalidation rules, route segments and their
|
|
528
|
+
// children must re-render when params or search params change — the
|
|
529
|
+
// handler reads ctx.params/ctx.searchParams so different values produce
|
|
530
|
+
// different content. Matches evaluateRevalidation's default logic.
|
|
531
|
+
const searchChanged = ctx.prevUrl.search !== ctx.url.search;
|
|
532
|
+
const routeParamsChanged = !paramsEqual(
|
|
533
|
+
ctx.matched.params,
|
|
534
|
+
ctx.prevParams,
|
|
535
|
+
);
|
|
536
|
+
const shouldDefaultRevalidate =
|
|
537
|
+
(searchChanged || routeParamsChanged) &&
|
|
538
|
+
(segment.type === "route" ||
|
|
539
|
+
(segment.belongsToRoute &&
|
|
540
|
+
(segment.type === "layout" || segment.type === "parallel")));
|
|
541
|
+
|
|
521
542
|
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
543
|
+
if (shouldDefaultRevalidate) {
|
|
544
|
+
// Params or search params changed — must re-render even without custom rules
|
|
545
|
+
if (isTraceActive()) {
|
|
546
|
+
pushRevalidationTraceEntry({
|
|
547
|
+
segmentId: segment.id,
|
|
548
|
+
segmentType: segment.type,
|
|
549
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
550
|
+
source: "cache-hit",
|
|
551
|
+
defaultShouldRevalidate: true,
|
|
552
|
+
finalShouldRevalidate: true,
|
|
553
|
+
reason: routeParamsChanged
|
|
554
|
+
? "cached-params-changed"
|
|
555
|
+
: "cached-search-changed",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
yield segment;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
522
561
|
// No revalidation rules, use default behavior (skip if client has)
|
|
523
562
|
if (isTraceActive()) {
|
|
524
563
|
pushRevalidationTraceEntry({
|
|
@@ -615,7 +654,11 @@ export function withCacheLookup<TEnv>(
|
|
|
615
654
|
ctx.url,
|
|
616
655
|
ctx.routeKey,
|
|
617
656
|
ctx.actionContext,
|
|
618
|
-
|
|
657
|
+
// Loaders are never cached in the segment cache, so segment
|
|
658
|
+
// staleness (cacheResult.shouldRevalidate) must not propagate.
|
|
659
|
+
// But browser-sent staleness (ctx.stale) — indicating an action
|
|
660
|
+
// happened in this or another tab — must still reach loaders.
|
|
661
|
+
ctx.stale || undefined,
|
|
619
662
|
),
|
|
620
663
|
);
|
|
621
664
|
|
|
@@ -642,7 +685,7 @@ export function withCacheLookup<TEnv>(
|
|
|
642
685
|
depth: 1,
|
|
643
686
|
});
|
|
644
687
|
ms.metrics.push({
|
|
645
|
-
label: "pipeline:cache-
|
|
688
|
+
label: "pipeline:cache-hit",
|
|
646
689
|
duration: loaderEnd - pipelineStart,
|
|
647
690
|
startTime: pipelineStart - ms.requestStart,
|
|
648
691
|
});
|
|
@@ -165,10 +165,14 @@ export function withCacheStore<TEnv>(
|
|
|
165
165
|
// Combine main segments with intercept segments
|
|
166
166
|
const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
|
|
167
167
|
|
|
168
|
-
// Check if any non-loader segments have null components
|
|
169
|
-
//
|
|
168
|
+
// Check if any non-loader segments have null components from revalidation
|
|
169
|
+
// skip (client already had them). Segments where the handler intentionally
|
|
170
|
+
// returned null are not revalidation skips — re-rendering them will still
|
|
171
|
+
// produce null, so proactive caching would be wasted work.
|
|
172
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
170
173
|
const hasNullComponents = allSegmentsToCache.some(
|
|
171
|
-
(s) =>
|
|
174
|
+
(s) =>
|
|
175
|
+
s.component === null && s.type !== "loader" && clientIdSet.has(s.id),
|
|
172
176
|
);
|
|
173
177
|
|
|
174
178
|
const requestCtx = getRequestContext();
|
|
@@ -195,6 +199,10 @@ export function withCacheStore<TEnv>(
|
|
|
195
199
|
// Proactive caching: render all segments fresh in background
|
|
196
200
|
// This ensures cache has complete components for future requests
|
|
197
201
|
requestCtx.waitUntil(async () => {
|
|
202
|
+
// Prevent background metrics from polluting foreground timeline.
|
|
203
|
+
const savedMetrics = ctx.Store.metrics;
|
|
204
|
+
ctx.Store.metrics = undefined;
|
|
205
|
+
|
|
198
206
|
const start = performance.now();
|
|
199
207
|
debugLog("cacheStore", "proactive caching started", {
|
|
200
208
|
pathname: ctx.pathname,
|
|
@@ -225,7 +233,9 @@ export function withCacheStore<TEnv>(
|
|
|
225
233
|
// Use normal loader access so handle data is captured
|
|
226
234
|
setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
|
|
227
235
|
|
|
228
|
-
// Re-resolve ALL segments without revalidation
|
|
236
|
+
// Re-resolve ALL segments without revalidation.
|
|
237
|
+
// Skip DSL loaders — they are never cached (cacheRoute filters them)
|
|
238
|
+
// and are always resolved fresh on each request.
|
|
229
239
|
const Store = ctx.Store;
|
|
230
240
|
const freshSegments = await Store.run(() =>
|
|
231
241
|
resolveAllSegments(
|
|
@@ -234,6 +244,7 @@ export function withCacheStore<TEnv>(
|
|
|
234
244
|
ctx.matched.params,
|
|
235
245
|
proactiveHandlerContext,
|
|
236
246
|
proactiveLoaderPromises,
|
|
247
|
+
{ skipLoaders: true },
|
|
237
248
|
),
|
|
238
249
|
);
|
|
239
250
|
|
|
@@ -285,11 +296,17 @@ export function withCacheStore<TEnv>(
|
|
|
285
296
|
});
|
|
286
297
|
} finally {
|
|
287
298
|
requestCtx._handleStore = originalHandleStore;
|
|
299
|
+
ctx.Store.metrics = savedMetrics;
|
|
288
300
|
}
|
|
289
301
|
});
|
|
290
302
|
} else {
|
|
291
303
|
// All segments have components - cache directly
|
|
292
304
|
// Schedule caching in waitUntil since cacheRoute is now async (key resolution)
|
|
305
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
306
|
+
console.log(
|
|
307
|
+
`[RSC CacheStore][req:${reqId}] Direct cache path: scheduling cacheRoute for ${ctx.pathname} (${allSegmentsToCache.length} segments, hasNullComponents=${hasNullComponents})`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
293
310
|
requestCtx.waitUntil(async () => {
|
|
294
311
|
const start = performance.now();
|
|
295
312
|
await cacheScope.cacheRoute(
|
|
@@ -67,10 +67,11 @@
|
|
|
67
67
|
* Keep if:
|
|
68
68
|
* - component !== null (needs rendering)
|
|
69
69
|
* - type === "loader" (carries data even with null component)
|
|
70
|
+
* - client doesn't have the segment (structurally required parent node)
|
|
70
71
|
*
|
|
71
72
|
* Skip if:
|
|
72
|
-
* - component === null AND type !== "loader"
|
|
73
|
-
* - (
|
|
73
|
+
* - component === null AND type !== "loader" AND client has it cached
|
|
74
|
+
* - (Revalidation skip — client already has this segment's UI)
|
|
74
75
|
*
|
|
75
76
|
*
|
|
76
77
|
* INTERCEPT HANDLING
|
|
@@ -168,10 +169,15 @@ export function buildMatchResult<TEnv>(
|
|
|
168
169
|
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
169
170
|
allIds = [...new Set(allIds)];
|
|
170
171
|
|
|
171
|
-
// Filter out segments
|
|
172
|
-
//
|
|
172
|
+
// Filter out null-component segments only when the client already has
|
|
173
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
174
|
+
// it must be included even with null component — it's structurally required
|
|
175
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
176
|
+
// Loader segments are always included as they carry data.
|
|
177
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
173
178
|
segmentsToRender = allSegments.filter(
|
|
174
|
-
(s) =>
|
|
179
|
+
(s) =>
|
|
180
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
175
181
|
);
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -27,8 +27,12 @@ type GetVariableFn = {
|
|
|
27
27
|
* Set variable function type
|
|
28
28
|
*/
|
|
29
29
|
type SetVariableFn = {
|
|
30
|
-
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
31
|
-
<K extends keyof DefaultVars>(
|
|
30
|
+
<T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
|
|
31
|
+
<K extends keyof DefaultVars>(
|
|
32
|
+
key: K,
|
|
33
|
+
value: DefaultVars[K],
|
|
34
|
+
options?: { cache?: boolean },
|
|
35
|
+
): void;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -91,12 +95,6 @@ export interface MiddlewareContext<
|
|
|
91
95
|
/** Set a context variable (shared with route handlers) */
|
|
92
96
|
set: SetVariableFn;
|
|
93
97
|
|
|
94
|
-
/**
|
|
95
|
-
* Middleware-injected variables.
|
|
96
|
-
* Same shared dictionary as `ctx.get()`/`ctx.set()`.
|
|
97
|
-
*/
|
|
98
|
-
var: DefaultVars;
|
|
99
|
-
|
|
100
98
|
/**
|
|
101
99
|
* Set a response header - can be called before or after `next()`.
|
|
102
100
|
*
|
package/src/router/middleware.ts
CHANGED
|
@@ -204,12 +204,9 @@ export function createMiddlewareContext<TEnv>(
|
|
|
204
204
|
get: ((keyOrVar: any) =>
|
|
205
205
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
206
206
|
|
|
207
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
208
|
-
contextSet(variables, keyOrVar, value);
|
|
207
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
208
|
+
contextSet(variables, keyOrVar, value, options);
|
|
209
209
|
}) as MiddlewareContext<TEnv>["set"],
|
|
210
|
-
|
|
211
|
-
var: variables as MiddlewareContext<TEnv>["var"],
|
|
212
|
-
|
|
213
210
|
header(name: string, value: string): void {
|
|
214
211
|
// Before next(): delegate to shared RequestContext stub
|
|
215
212
|
if (isPreNext()) {
|
|
@@ -104,7 +104,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
104
104
|
originalUrl: new URL("http://prerender" + pathname),
|
|
105
105
|
pathname,
|
|
106
106
|
searchParams: new URLSearchParams(),
|
|
107
|
-
|
|
107
|
+
_variables: variables,
|
|
108
108
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
109
109
|
set: ((keyOrVar: any, value: any) => {
|
|
110
110
|
contextSet(variables, keyOrVar, value);
|
|
@@ -336,7 +336,7 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
336
336
|
originalUrl: syntheticUrl,
|
|
337
337
|
pathname: "/",
|
|
338
338
|
searchParams: syntheticUrl.searchParams,
|
|
339
|
-
|
|
339
|
+
_variables: {},
|
|
340
340
|
get: () => undefined as any,
|
|
341
341
|
set: () => {},
|
|
342
342
|
params: {},
|
|
@@ -210,6 +210,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
210
210
|
params: Record<string, string>,
|
|
211
211
|
handlerContext: HandlerContext<any, TEnv>,
|
|
212
212
|
loaderPromises: Map<string, Promise<any>>,
|
|
213
|
+
options?: { skipLoaders?: boolean },
|
|
213
214
|
) => Promise<ResolvedSegment[]>;
|
|
214
215
|
|
|
215
216
|
// Generator-based simple resolution
|
|
@@ -2,6 +2,7 @@ import type { ComponentType, ReactNode } from "react";
|
|
|
2
2
|
import type { SerializedManifest } from "../debug.js";
|
|
3
3
|
import type { ReverseFunction } from "../reverse.js";
|
|
4
4
|
import type { UrlPatterns } from "../urls.js";
|
|
5
|
+
import type { UrlBuilder } from "../urls/pattern-types.js";
|
|
5
6
|
import type { EntryData } from "../server/context";
|
|
6
7
|
import type { ErrorInfo, MatchResult } from "../types";
|
|
7
8
|
import type { NonceProvider } from "../rsc/types.js";
|
|
@@ -68,12 +69,24 @@ export interface RSCRouter<
|
|
|
68
69
|
readonly id: string;
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
+
* URL prefix applied to all routes. Undefined when no basename is configured.
|
|
73
|
+
*/
|
|
74
|
+
readonly basename: string | undefined;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Register routes using URL patterns from urls() or a builder function
|
|
72
78
|
*
|
|
73
79
|
* @example
|
|
74
80
|
* ```typescript
|
|
75
|
-
*
|
|
76
|
-
*
|
|
81
|
+
* // With urls()
|
|
82
|
+
* createRouter({}).routes(urlpatterns)
|
|
83
|
+
*
|
|
84
|
+
* // With builder function (urls() is implicit)
|
|
85
|
+
* createRouter({}).routes(({ path, layout }) => [
|
|
86
|
+
* layout(RootLayout, () => [
|
|
87
|
+
* path("/", HomePage),
|
|
88
|
+
* ]),
|
|
89
|
+
* ])
|
|
77
90
|
* ```
|
|
78
91
|
*/
|
|
79
92
|
routes<T extends UrlPatterns<TEnv, any>>(
|
|
@@ -85,6 +98,7 @@ export interface RSCRouter<
|
|
|
85
98
|
? MergeRoutesWithResponses<NonNullable<T["_routes"]>, T["_responses"]>
|
|
86
99
|
: Record<string, string>)
|
|
87
100
|
>;
|
|
101
|
+
routes(builder: UrlBuilder<TEnv>): RSCRouter<TEnv, TRoutes>;
|
|
88
102
|
|
|
89
103
|
/**
|
|
90
104
|
* Add global middleware that runs on all routes
|
|
@@ -188,8 +202,11 @@ export interface RSCRouterInternal<
|
|
|
188
202
|
*/
|
|
189
203
|
readonly id: string;
|
|
190
204
|
|
|
205
|
+
/** URL prefix applied to all routes. */
|
|
206
|
+
readonly basename: string | undefined;
|
|
207
|
+
|
|
191
208
|
/**
|
|
192
|
-
* Register routes using URL patterns from urls()
|
|
209
|
+
* Register routes using URL patterns from urls() or a builder function
|
|
193
210
|
*/
|
|
194
211
|
routes<T extends UrlPatterns<TEnv, any>>(
|
|
195
212
|
patterns: T,
|
|
@@ -200,6 +217,7 @@ export interface RSCRouterInternal<
|
|
|
200
217
|
? MergeRoutesWithResponses<NonNullable<T["_routes"]>, T["_responses"]>
|
|
201
218
|
: Record<string, string>)
|
|
202
219
|
>;
|
|
220
|
+
routes(builder: UrlBuilder<TEnv>): RSCRouter<TEnv, TRoutes>;
|
|
203
221
|
|
|
204
222
|
/**
|
|
205
223
|
* Add global middleware that runs on all routes
|
|
@@ -338,6 +356,9 @@ export interface RSCRouterInternal<
|
|
|
338
356
|
*/
|
|
339
357
|
readonly __sourceFile?: string;
|
|
340
358
|
|
|
359
|
+
/** @internal basename for runtime manifest generation */
|
|
360
|
+
readonly __basename?: string;
|
|
361
|
+
|
|
341
362
|
match(
|
|
342
363
|
request: Request,
|
|
343
364
|
input?: RouterRequestInput<TEnv>,
|