@rangojs/router 0.0.0-experimental.fa8a383a → 0.0.0-experimental.fb4fdc18
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 +188 -35
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +1884 -537
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +33 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +93 -17
- package/skills/loader/SKILL.md +123 -46
- package/skills/middleware/SKILL.md +36 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +133 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +75 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +19 -1
- package/src/__internal.ts +1 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +95 -7
- package/src/browser/navigation-client.ts +128 -53
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/partial-update.ts +93 -12
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +156 -18
- package/src/browser/prefetch/queue.ts +92 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +72 -8
- package/src/browser/react/NavigationProvider.tsx +82 -21
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +17 -4
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +60 -9
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +46 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +84 -230
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +49 -6
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +28 -2
- package/src/route-definition/dsl-helpers.ts +210 -35
- package/src/route-definition/helpers-types.ts +73 -20
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +102 -25
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +159 -21
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +74 -14
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +112 -9
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +20 -33
- package/src/router/middleware.ts +56 -12
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +15 -1
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +114 -18
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +257 -127
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +55 -7
- package/src/rsc/handler.ts +478 -383
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +20 -1
- package/src/rsc/server-action.ts +12 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +15 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +22 -62
- package/src/server/context.ts +76 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +185 -57
- package/src/ssr/index.tsx +8 -1
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +145 -68
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +36 -4
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +175 -74
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +563 -316
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +63 -11
- package/src/vite/router-discovery.ts +732 -86
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -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
|
|
@@ -94,6 +96,7 @@ import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
|
94
96
|
import { getRouterContext } from "../router-context.js";
|
|
95
97
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
96
98
|
import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
99
|
+
import { treeHasStreaming } from "./segment-resolution.js";
|
|
97
100
|
import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
|
|
98
101
|
import type { HandleStore } from "../../server/handle-store.js";
|
|
99
102
|
import {
|
|
@@ -191,6 +194,16 @@ async function* yieldFromStore<TEnv>(
|
|
|
191
194
|
state.cachedSegments = segments;
|
|
192
195
|
state.cachedMatchedIds = segments.map((s) => s.id);
|
|
193
196
|
|
|
197
|
+
// Set streaming flag (once) and resolve render barrier.
|
|
198
|
+
const reqCtx = handleStoreRef ? undefined : _lazyGetRequestContext?.();
|
|
199
|
+
const barrierReqCtx = reqCtx ?? _getRequestContext();
|
|
200
|
+
if (barrierReqCtx) {
|
|
201
|
+
if (barrierReqCtx._treeHasStreaming === undefined) {
|
|
202
|
+
barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
203
|
+
}
|
|
204
|
+
barrierReqCtx._resolveRenderBarrier(segments);
|
|
205
|
+
}
|
|
206
|
+
|
|
194
207
|
// For partial navigation, nullify components the client already has
|
|
195
208
|
// so parent layouts stay live (client keeps its existing versions).
|
|
196
209
|
// When params changed (e.g., different guide slug), the segments have
|
|
@@ -238,6 +251,7 @@ async function* yieldFromStore<TEnv>(
|
|
|
238
251
|
ctx.url,
|
|
239
252
|
ctx.routeKey,
|
|
240
253
|
ctx.actionContext,
|
|
254
|
+
ctx.stale || undefined,
|
|
241
255
|
),
|
|
242
256
|
);
|
|
243
257
|
state.matchedIds = [
|
|
@@ -261,7 +275,7 @@ async function* yieldFromStore<TEnv>(
|
|
|
261
275
|
depth: 1,
|
|
262
276
|
});
|
|
263
277
|
ms.metrics.push({
|
|
264
|
-
label: "pipeline:cache-
|
|
278
|
+
label: "pipeline:cache-hit",
|
|
265
279
|
duration: loaderEnd - pipelineStart,
|
|
266
280
|
startTime: pipelineStart - ms.requestStart,
|
|
267
281
|
});
|
|
@@ -314,14 +328,15 @@ export function withCacheLookup<TEnv>(
|
|
|
314
328
|
|
|
315
329
|
// Prerender lookup: check build-time cached data before runtime cache.
|
|
316
330
|
// Prerender data is available regardless of runtime cache configuration.
|
|
317
|
-
|
|
331
|
+
// Skip for HMR requests — the dev prerender endpoint reads from a stale
|
|
332
|
+
// RouterRegistry snapshot; rendering fresh ensures edits are visible.
|
|
333
|
+
const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
|
|
334
|
+
if (!ctx.isAction && !isHmr && ctx.matched.pr) {
|
|
318
335
|
await ensurePrerenderDeps();
|
|
319
336
|
if (prerenderStoreInstance) {
|
|
320
337
|
const paramHash = _hashParams!(ctx.matched.params);
|
|
321
338
|
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
322
|
-
(entry) =>
|
|
323
|
-
entry.type === "route" &&
|
|
324
|
-
entry.prerenderDef?.options?.passthrough === true,
|
|
339
|
+
(entry) => entry.type === "route" && entry.isPassthrough === true,
|
|
325
340
|
);
|
|
326
341
|
|
|
327
342
|
if (ctx.isIntercept) {
|
|
@@ -391,9 +406,7 @@ export function withCacheLookup<TEnv>(
|
|
|
391
406
|
if (prerenderStoreInstance) {
|
|
392
407
|
const paramHash = _hashParams!(ctx.matched.params);
|
|
393
408
|
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
394
|
-
(entry) =>
|
|
395
|
-
entry.type === "route" &&
|
|
396
|
-
entry.prerenderDef?.options?.passthrough === true,
|
|
409
|
+
(entry) => entry.type === "route" && entry.isPassthrough === true,
|
|
397
410
|
);
|
|
398
411
|
|
|
399
412
|
if (ctx.isIntercept) {
|
|
@@ -446,7 +459,7 @@ export function withCacheLookup<TEnv>(
|
|
|
446
459
|
yield* source;
|
|
447
460
|
if (ms) {
|
|
448
461
|
ms.metrics.push({
|
|
449
|
-
label: "pipeline:cache-
|
|
462
|
+
label: "pipeline:cache-miss",
|
|
450
463
|
duration: performance.now() - pipelineStart,
|
|
451
464
|
startTime: pipelineStart - ms.requestStart,
|
|
452
465
|
});
|
|
@@ -466,7 +479,7 @@ export function withCacheLookup<TEnv>(
|
|
|
466
479
|
yield* source;
|
|
467
480
|
if (ms) {
|
|
468
481
|
ms.metrics.push({
|
|
469
|
-
label: "pipeline:cache-
|
|
482
|
+
label: "pipeline:cache-miss",
|
|
470
483
|
duration: performance.now() - pipelineStart,
|
|
471
484
|
startTime: pipelineStart - ms.requestStart,
|
|
472
485
|
});
|
|
@@ -518,7 +531,41 @@ export function withCacheLookup<TEnv>(
|
|
|
518
531
|
|
|
519
532
|
// Look up revalidation rules for this segment
|
|
520
533
|
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
534
|
+
|
|
535
|
+
// Even without explicit revalidation rules, route segments and their
|
|
536
|
+
// children must re-render when params or search params change — the
|
|
537
|
+
// handler reads ctx.params/ctx.searchParams so different values produce
|
|
538
|
+
// different content. Matches evaluateRevalidation's default logic.
|
|
539
|
+
const searchChanged = ctx.prevUrl.search !== ctx.url.search;
|
|
540
|
+
const routeParamsChanged = !paramsEqual(
|
|
541
|
+
ctx.matched.params,
|
|
542
|
+
ctx.prevParams,
|
|
543
|
+
);
|
|
544
|
+
const shouldDefaultRevalidate =
|
|
545
|
+
(searchChanged || routeParamsChanged) &&
|
|
546
|
+
(segment.type === "route" ||
|
|
547
|
+
(segment.belongsToRoute &&
|
|
548
|
+
(segment.type === "layout" || segment.type === "parallel")));
|
|
549
|
+
|
|
521
550
|
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
551
|
+
if (shouldDefaultRevalidate) {
|
|
552
|
+
// Params or search params changed — must re-render even without custom rules
|
|
553
|
+
if (isTraceActive()) {
|
|
554
|
+
pushRevalidationTraceEntry({
|
|
555
|
+
segmentId: segment.id,
|
|
556
|
+
segmentType: segment.type,
|
|
557
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
558
|
+
source: "cache-hit",
|
|
559
|
+
defaultShouldRevalidate: true,
|
|
560
|
+
finalShouldRevalidate: true,
|
|
561
|
+
reason: routeParamsChanged
|
|
562
|
+
? "cached-params-changed"
|
|
563
|
+
: "cached-search-changed",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
yield segment;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
522
569
|
// No revalidation rules, use default behavior (skip if client has)
|
|
523
570
|
if (isTraceActive()) {
|
|
524
571
|
pushRevalidationTraceEntry({
|
|
@@ -552,7 +599,7 @@ export function withCacheLookup<TEnv>(
|
|
|
552
599
|
routeKey: ctx.routeKey,
|
|
553
600
|
context: ctx.handlerContext,
|
|
554
601
|
actionContext: ctx.actionContext,
|
|
555
|
-
stale: cacheResult.shouldRevalidate || undefined,
|
|
602
|
+
stale: cacheResult.shouldRevalidate || ctx.stale || undefined,
|
|
556
603
|
traceSource: "cache-hit",
|
|
557
604
|
});
|
|
558
605
|
|
|
@@ -579,6 +626,15 @@ export function withCacheLookup<TEnv>(
|
|
|
579
626
|
yield segment;
|
|
580
627
|
}
|
|
581
628
|
|
|
629
|
+
// Set streaming flag (once) and resolve render barrier.
|
|
630
|
+
const barrierReqCtx = _getRequestContext();
|
|
631
|
+
if (barrierReqCtx) {
|
|
632
|
+
if (barrierReqCtx._treeHasStreaming === undefined) {
|
|
633
|
+
barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
634
|
+
}
|
|
635
|
+
barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
|
|
636
|
+
}
|
|
637
|
+
|
|
582
638
|
// Resolve loaders fresh (loaders are NOT cached by default)
|
|
583
639
|
// This ensures fresh data even on cache hit
|
|
584
640
|
const Store = ctx.Store;
|
|
@@ -615,7 +671,11 @@ export function withCacheLookup<TEnv>(
|
|
|
615
671
|
ctx.url,
|
|
616
672
|
ctx.routeKey,
|
|
617
673
|
ctx.actionContext,
|
|
618
|
-
|
|
674
|
+
// Loaders are never cached in the segment cache, so segment
|
|
675
|
+
// staleness (cacheResult.shouldRevalidate) must not propagate.
|
|
676
|
+
// But browser-sent staleness (ctx.stale) — indicating an action
|
|
677
|
+
// happened in this or another tab — must still reach loaders.
|
|
678
|
+
ctx.stale || undefined,
|
|
619
679
|
),
|
|
620
680
|
);
|
|
621
681
|
|
|
@@ -642,7 +702,7 @@ export function withCacheLookup<TEnv>(
|
|
|
642
702
|
depth: 1,
|
|
643
703
|
});
|
|
644
704
|
ms.metrics.push({
|
|
645
|
-
label: "pipeline:cache-
|
|
705
|
+
label: "pipeline:cache-hit",
|
|
646
706
|
duration: loaderEnd - pipelineStart,
|
|
647
707
|
startTime: pipelineStart - ms.requestStart,
|
|
648
708
|
});
|
|
@@ -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(
|
|
@@ -87,10 +87,49 @@
|
|
|
87
87
|
* if (state.cacheHit) return; // Now we can check
|
|
88
88
|
*/
|
|
89
89
|
import type { ResolvedSegment } from "../../types.js";
|
|
90
|
+
import type { EntryData } from "../../server/context.js";
|
|
91
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
90
92
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
91
93
|
import { getRouterContext } from "../router-context.js";
|
|
92
94
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Check whether any entry in the tree uses loading() (streaming).
|
|
98
|
+
* Matches the router's streaming semantics in fresh.ts: streaming is
|
|
99
|
+
* enabled when `loading` is defined AND not `false`. `loading: false`
|
|
100
|
+
* explicitly disables streaming; `undefined` means no loading at all.
|
|
101
|
+
*/
|
|
102
|
+
export function treeHasStreaming(entries: EntryData[]): boolean {
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (
|
|
105
|
+
"loading" in entry &&
|
|
106
|
+
entry.loading !== undefined &&
|
|
107
|
+
entry.loading !== false
|
|
108
|
+
)
|
|
109
|
+
return true;
|
|
110
|
+
if (entry.layout) {
|
|
111
|
+
if (treeHasStreaming(entry.layout)) return true;
|
|
112
|
+
}
|
|
113
|
+
if (entry.parallel) {
|
|
114
|
+
for (const key in entry.parallel) {
|
|
115
|
+
const parallelEntry = entry.parallel[key as `@${string}`];
|
|
116
|
+
if (parallelEntry) {
|
|
117
|
+
if (
|
|
118
|
+
"loading" in parallelEntry &&
|
|
119
|
+
parallelEntry.loading !== undefined &&
|
|
120
|
+
parallelEntry.loading !== false
|
|
121
|
+
)
|
|
122
|
+
return true;
|
|
123
|
+
if (parallelEntry.layout) {
|
|
124
|
+
if (treeHasStreaming(parallelEntry.layout)) return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
94
133
|
/**
|
|
95
134
|
* Creates segment resolution middleware
|
|
96
135
|
*
|
|
@@ -116,6 +155,7 @@ export function withSegmentResolution<TEnv>(
|
|
|
116
155
|
const ownStart = performance.now();
|
|
117
156
|
|
|
118
157
|
// If cache hit, segments were already yielded by cache lookup
|
|
158
|
+
// (render barrier is resolved on the cache-hit path)
|
|
119
159
|
if (state.cacheHit) {
|
|
120
160
|
if (ms) {
|
|
121
161
|
ms.metrics.push({
|
|
@@ -127,6 +167,11 @@ export function withSegmentResolution<TEnv>(
|
|
|
127
167
|
return;
|
|
128
168
|
}
|
|
129
169
|
|
|
170
|
+
const reqCtx = _getRequestContext();
|
|
171
|
+
if (reqCtx && reqCtx._treeHasStreaming === undefined) {
|
|
172
|
+
reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
173
|
+
}
|
|
174
|
+
|
|
130
175
|
const { resolveAllSegmentsWithRevalidation, resolveAllSegments } =
|
|
131
176
|
getRouterContext<TEnv>();
|
|
132
177
|
|
|
@@ -148,6 +193,10 @@ export function withSegmentResolution<TEnv>(
|
|
|
148
193
|
state.segments = segments;
|
|
149
194
|
state.matchedIds = segments.map((s: { id: string }) => s.id);
|
|
150
195
|
|
|
196
|
+
if (reqCtx) {
|
|
197
|
+
reqCtx._resolveRenderBarrier(segments);
|
|
198
|
+
}
|
|
199
|
+
|
|
151
200
|
// Yield all resolved segments
|
|
152
201
|
for (const segment of segments) {
|
|
153
202
|
yield segment;
|
|
@@ -178,6 +227,10 @@ export function withSegmentResolution<TEnv>(
|
|
|
178
227
|
state.segments = result.segments;
|
|
179
228
|
state.matchedIds = result.matchedIds;
|
|
180
229
|
|
|
230
|
+
if (reqCtx) {
|
|
231
|
+
reqCtx._resolveRenderBarrier(result.segments);
|
|
232
|
+
}
|
|
233
|
+
|
|
181
234
|
// Yield all resolved segments
|
|
182
235
|
for (const segment of result.segments) {
|
|
183
236
|
yield segment;
|
|
@@ -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
|
|
@@ -124,6 +125,69 @@ export async function collectSegments(
|
|
|
124
125
|
return segments;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Deduplicate inherited loader segments by loaderId.
|
|
130
|
+
*
|
|
131
|
+
* When a route has loaders and a child layout has parallel slots, the same
|
|
132
|
+
* loader is resolved twice: once for the route and once inherited into the
|
|
133
|
+
* layout (tagged with `_inherited`). The inherited copy is only needed when
|
|
134
|
+
* the route uses `loading()` — in that case, the loader data is inside a
|
|
135
|
+
* LoaderBoundary/Suspense that parallel slots can't reach through. Without
|
|
136
|
+
* loading(), useLoader() traverses parent contexts and finds the data.
|
|
137
|
+
*/
|
|
138
|
+
function deduplicateLoaderSegments(
|
|
139
|
+
segments: ResolvedSegment[],
|
|
140
|
+
logPrefix: string,
|
|
141
|
+
): ResolvedSegment[] {
|
|
142
|
+
// First pass: collect loaderIds of original (non-inherited) segments
|
|
143
|
+
// and whether their parent entry uses loading()
|
|
144
|
+
const originalLoaders = new Set<string>();
|
|
145
|
+
const loadersWithLoading = new Set<string>();
|
|
146
|
+
for (const s of segments) {
|
|
147
|
+
if (s.type === "loader" && s.loaderId && !s._inherited) {
|
|
148
|
+
originalLoaders.add(s.loaderId);
|
|
149
|
+
// If the segment has a sibling with loading, the parent uses loading()
|
|
150
|
+
// We detect this by checking if any non-loader segment in the same
|
|
151
|
+
// namespace has loading defined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Check if any layout/route segment has loading — if a loader's namespace
|
|
155
|
+
// matches a segment with loading, the inherited copy is needed
|
|
156
|
+
for (const s of segments) {
|
|
157
|
+
if (s.type !== "loader" && s.loading !== undefined && s.loading !== false) {
|
|
158
|
+
// Find loaders in this namespace
|
|
159
|
+
for (const l of segments) {
|
|
160
|
+
if (l.type === "loader" && l.namespace === s.namespace && l.loaderId) {
|
|
161
|
+
loadersWithLoading.add(l.loaderId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result: ResolvedSegment[] = [];
|
|
168
|
+
let dedupCount = 0;
|
|
169
|
+
|
|
170
|
+
for (const s of segments) {
|
|
171
|
+
if (
|
|
172
|
+
s.type === "loader" &&
|
|
173
|
+
s.loaderId &&
|
|
174
|
+
s._inherited &&
|
|
175
|
+
originalLoaders.has(s.loaderId) &&
|
|
176
|
+
!loadersWithLoading.has(s.loaderId)
|
|
177
|
+
) {
|
|
178
|
+
dedupCount++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
result.push(s);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (dedupCount > 0) {
|
|
185
|
+
debugLog(logPrefix, `deduped ${dedupCount} inherited loader segment(s)`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
127
191
|
/**
|
|
128
192
|
* Build the final MatchResult from collected segments and context
|
|
129
193
|
*/
|
|
@@ -168,13 +232,23 @@ export function buildMatchResult<TEnv>(
|
|
|
168
232
|
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
169
233
|
allIds = [...new Set(allIds)];
|
|
170
234
|
|
|
171
|
-
// Filter out segments
|
|
172
|
-
//
|
|
235
|
+
// Filter out null-component segments only when the client already has
|
|
236
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
237
|
+
// it must be included even with null component — it's structurally required
|
|
238
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
239
|
+
// Loader segments are always included as they carry data.
|
|
240
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
173
241
|
segmentsToRender = allSegments.filter(
|
|
174
|
-
(s) =>
|
|
242
|
+
(s) =>
|
|
243
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
175
244
|
);
|
|
176
245
|
}
|
|
177
246
|
|
|
247
|
+
const dedupedSegments = deduplicateLoaderSegments(
|
|
248
|
+
segmentsToRender,
|
|
249
|
+
logPrefix,
|
|
250
|
+
);
|
|
251
|
+
|
|
178
252
|
debugLog(logPrefix, "all segments", {
|
|
179
253
|
segments: allSegments.map((s) => ({
|
|
180
254
|
id: s.id,
|
|
@@ -183,13 +257,42 @@ export function buildMatchResult<TEnv>(
|
|
|
183
257
|
})),
|
|
184
258
|
});
|
|
185
259
|
debugLog(logPrefix, "segments to render", {
|
|
186
|
-
segmentIds:
|
|
260
|
+
segmentIds: dedupedSegments.map((s) => s.id),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Remove deduped loader IDs from matched so the client doesn't treat
|
|
264
|
+
// them as missing segments and trigger a fallback refetch.
|
|
265
|
+
const removedIds = new Set(
|
|
266
|
+
segmentsToRender
|
|
267
|
+
.filter((s) => !dedupedSegments.includes(s))
|
|
268
|
+
.map((s) => s.id),
|
|
269
|
+
);
|
|
270
|
+
const matchedIds =
|
|
271
|
+
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
272
|
+
|
|
273
|
+
// resolvedIds: every segment whose handler actually ran this request.
|
|
274
|
+
// For full-match every segment is fresh; for partial-match we filter by
|
|
275
|
+
// the internal `_handlerRan` flag set in revalidation.ts. Drives the
|
|
276
|
+
// client's handle-bucket cleanup — a slot that re-resolved and pushed
|
|
277
|
+
// nothing must have its previous handle data cleared, but `diff` won't
|
|
278
|
+
// carry it because the segment payload skips null-component cached
|
|
279
|
+
// segments to save bytes.
|
|
280
|
+
const resolvedIds = ctx.isFullMatch
|
|
281
|
+
? allSegments.map((s) => s.id)
|
|
282
|
+
: allSegments.filter((s) => s._handlerRan).map((s) => s.id);
|
|
283
|
+
|
|
284
|
+
// Strip internal-only fields from the segments going on the wire.
|
|
285
|
+
const cleanedSegments = dedupedSegments.map((s) => {
|
|
286
|
+
if (s._handlerRan === undefined) return s;
|
|
287
|
+
const { _handlerRan: _drop, ...rest } = s;
|
|
288
|
+
return rest as ResolvedSegment;
|
|
187
289
|
});
|
|
188
290
|
|
|
189
291
|
return {
|
|
190
|
-
segments:
|
|
191
|
-
matched:
|
|
192
|
-
diff:
|
|
292
|
+
segments: cleanedSegments,
|
|
293
|
+
matched: matchedIds,
|
|
294
|
+
diff: cleanedSegments.map((s) => s.id),
|
|
295
|
+
resolvedIds,
|
|
193
296
|
params: ctx.matched.params,
|
|
194
297
|
routeName: ctx.routeKey,
|
|
195
298
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
package/src/router/metrics.ts
CHANGED
|
@@ -15,7 +15,12 @@ function formatMs(value: number): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
|
|
18
|
-
return [...metrics].sort((a, b) =>
|
|
18
|
+
return [...metrics].sort((a, b) => {
|
|
19
|
+
// handler:total always goes last (it wraps everything)
|
|
20
|
+
if (a.label === "handler:total") return 1;
|
|
21
|
+
if (b.label === "handler:total") return -1;
|
|
22
|
+
return a.startTime - b.startTime;
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface Span {
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
15
15
|
import type { Theme } from "../theme/types.js";
|
|
16
16
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
17
|
+
import type { RequestScope } from "../types/request-scope.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Get variable function type
|
|
@@ -27,8 +28,12 @@ type GetVariableFn = {
|
|
|
27
28
|
* Set variable function type
|
|
28
29
|
*/
|
|
29
30
|
type SetVariableFn = {
|
|
30
|
-
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
31
|
-
<K extends keyof DefaultVars>(
|
|
31
|
+
<T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
|
|
32
|
+
<K extends keyof DefaultVars>(
|
|
33
|
+
key: K,
|
|
34
|
+
value: DefaultVars[K],
|
|
35
|
+
options?: { cache?: boolean },
|
|
36
|
+
): void;
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
/**
|
|
@@ -48,33 +53,15 @@ export interface CookieOptions {
|
|
|
48
53
|
* Context passed to middleware
|
|
49
54
|
*
|
|
50
55
|
* @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
|
|
51
|
-
* @template TParams - URL params type (typed for route middleware,
|
|
56
|
+
* @template TParams - URL params type (typed for route middleware,
|
|
57
|
+
* `Record<string, string | undefined>` for global middleware — absent
|
|
58
|
+
* optional segments are omitted from the params record at runtime, so
|
|
59
|
+
* the index signature must include `undefined`)
|
|
52
60
|
*/
|
|
53
61
|
export interface MiddlewareContext<
|
|
54
62
|
TEnv = any,
|
|
55
|
-
TParams = Record<string, string>,
|
|
56
|
-
> {
|
|
57
|
-
/** Original request */
|
|
58
|
-
request: Request;
|
|
59
|
-
|
|
60
|
-
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
61
|
-
url: URL;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The original request URL with all parameters intact, including
|
|
65
|
-
* internal `_rsc*` transport params.
|
|
66
|
-
*/
|
|
67
|
-
originalUrl: URL;
|
|
68
|
-
|
|
69
|
-
/** URL pathname */
|
|
70
|
-
pathname: string;
|
|
71
|
-
|
|
72
|
-
/** URL search params */
|
|
73
|
-
searchParams: URLSearchParams;
|
|
74
|
-
|
|
75
|
-
/** Platform bindings (Cloudflare, etc.) */
|
|
76
|
-
env: TEnv;
|
|
77
|
-
|
|
63
|
+
TParams = Record<string, string | undefined>,
|
|
64
|
+
> extends RequestScope<TEnv> {
|
|
78
65
|
/** URL params extracted from route/middleware pattern */
|
|
79
66
|
params: TParams;
|
|
80
67
|
|
|
@@ -91,12 +78,6 @@ export interface MiddlewareContext<
|
|
|
91
78
|
/** Set a context variable (shared with route handlers) */
|
|
92
79
|
set: SetVariableFn;
|
|
93
80
|
|
|
94
|
-
/**
|
|
95
|
-
* Middleware-injected variables.
|
|
96
|
-
* Same shared dictionary as `ctx.get()`/`ctx.set()`.
|
|
97
|
-
*/
|
|
98
|
-
var: DefaultVars;
|
|
99
|
-
|
|
100
81
|
/**
|
|
101
82
|
* Set a response header - can be called before or after `next()`.
|
|
102
83
|
*
|
|
@@ -171,7 +152,10 @@ export interface MiddlewareContext<
|
|
|
171
152
|
* router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
|
|
172
153
|
* ```
|
|
173
154
|
*/
|
|
174
|
-
export type MiddlewareFn<
|
|
155
|
+
export type MiddlewareFn<
|
|
156
|
+
TEnv = any,
|
|
157
|
+
TParams = Record<string, string | undefined>,
|
|
158
|
+
> = (
|
|
175
159
|
ctx: MiddlewareContext<TEnv, TParams>,
|
|
176
160
|
next: () => Promise<Response>,
|
|
177
161
|
) => Response | void | Promise<Response | void>;
|
|
@@ -218,5 +202,8 @@ export interface MiddlewareCollectableEntry {
|
|
|
218
202
|
*/
|
|
219
203
|
export interface CollectedMiddleware {
|
|
220
204
|
handler: MiddlewareFn<any, any>;
|
|
205
|
+
// Internal shape only. The user-facing `MiddlewareContext.params` is
|
|
206
|
+
// typed `Record<string, string | undefined>` to reflect that absent
|
|
207
|
+
// optional segments are omitted from the params record at runtime.
|
|
221
208
|
params: Record<string, string>;
|
|
222
209
|
}
|