@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.debug-cache-2383ca26

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.
Files changed (65) hide show
  1. package/AGENTS.md +4 -0
  2. package/dist/bin/rango.js +8 -3
  3. package/dist/vite/index.js +139 -200
  4. package/package.json +15 -14
  5. package/skills/caching/SKILL.md +37 -4
  6. package/skills/parallel/SKILL.md +126 -0
  7. package/src/browser/event-controller.ts +5 -0
  8. package/src/browser/navigation-bridge.ts +1 -3
  9. package/src/browser/navigation-client.ts +60 -27
  10. package/src/browser/navigation-transaction.ts +11 -9
  11. package/src/browser/partial-update.ts +50 -9
  12. package/src/browser/prefetch/cache.ts +57 -5
  13. package/src/browser/prefetch/fetch.ts +30 -21
  14. package/src/browser/prefetch/queue.ts +53 -13
  15. package/src/browser/react/Link.tsx +9 -1
  16. package/src/browser/react/NavigationProvider.tsx +27 -0
  17. package/src/browser/rsc-router.tsx +109 -57
  18. package/src/browser/scroll-restoration.ts +31 -34
  19. package/src/browser/segment-reconciler.ts +6 -1
  20. package/src/browser/types.ts +9 -0
  21. package/src/build/route-types/router-processing.ts +12 -2
  22. package/src/cache/cache-runtime.ts +15 -11
  23. package/src/cache/cache-scope.ts +43 -3
  24. package/src/cache/cf/cf-cache-store.ts +453 -11
  25. package/src/cache/cf/index.ts +5 -1
  26. package/src/cache/document-cache.ts +17 -7
  27. package/src/cache/index.ts +1 -0
  28. package/src/debug.ts +2 -2
  29. package/src/route-definition/dsl-helpers.ts +32 -7
  30. package/src/route-definition/redirect.ts +2 -2
  31. package/src/route-map-builder.ts +7 -1
  32. package/src/router/find-match.ts +4 -2
  33. package/src/router/intercept-resolution.ts +2 -0
  34. package/src/router/lazy-includes.ts +4 -1
  35. package/src/router/logging.ts +5 -2
  36. package/src/router/manifest.ts +9 -3
  37. package/src/router/match-middleware/background-revalidation.ts +30 -2
  38. package/src/router/match-middleware/cache-lookup.ts +66 -9
  39. package/src/router/match-middleware/cache-store.ts +53 -10
  40. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  41. package/src/router/match-middleware/segment-resolution.ts +8 -5
  42. package/src/router/match-result.ts +22 -6
  43. package/src/router/metrics.ts +6 -1
  44. package/src/router/middleware.ts +2 -1
  45. package/src/router/router-context.ts +6 -1
  46. package/src/router/segment-resolution/fresh.ts +122 -15
  47. package/src/router/segment-resolution/loader-cache.ts +1 -0
  48. package/src/router/segment-resolution/revalidation.ts +347 -290
  49. package/src/router/segment-wrappers.ts +2 -0
  50. package/src/router.ts +5 -1
  51. package/src/segment-system.tsx +140 -4
  52. package/src/server/context.ts +90 -13
  53. package/src/server/request-context.ts +10 -4
  54. package/src/ssr/index.tsx +1 -0
  55. package/src/types/handler-context.ts +103 -17
  56. package/src/types/route-entry.ts +7 -0
  57. package/src/types/segments.ts +2 -0
  58. package/src/urls/path-helper.ts +1 -1
  59. package/src/vite/discovery/state.ts +0 -2
  60. package/src/vite/plugin-types.ts +0 -83
  61. package/src/vite/plugins/expose-action-id.ts +1 -3
  62. package/src/vite/plugins/version-plugin.ts +13 -1
  63. package/src/vite/rango.ts +144 -209
  64. package/src/vite/router-discovery.ts +0 -8
  65. package/src/vite/utils/banner.ts +3 -3
@@ -204,6 +204,7 @@ export function createSegmentWrappers<TEnv = any>(
204
204
  interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
205
205
  localRouteName: string,
206
206
  pathname: string,
207
+ stale?: boolean,
207
208
  ): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
208
209
  return _resolveAllSegmentsWithRevalidation(
209
210
  entries,
@@ -221,6 +222,7 @@ export function createSegmentWrappers<TEnv = any>(
221
222
  localRouteName,
222
223
  pathname,
223
224
  segmentDeps,
225
+ stale,
224
226
  );
225
227
  }
226
228
 
package/src/router.ts CHANGED
@@ -560,6 +560,7 @@ export function createRouter<TEnv = any>(
560
560
  mergedRouteMap,
561
561
  nextMountIndex: () => mountIndex++,
562
562
  getPrecomputedByPrefix,
563
+ routerId,
563
564
  };
564
565
 
565
566
  function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
@@ -689,7 +690,7 @@ export function createRouter<TEnv = any>(
689
690
  errorBoundary: [],
690
691
  notFoundBoundary: [],
691
692
  layout: [],
692
- parallel: [],
693
+ parallel: {},
693
694
  intercept: [],
694
695
  loader: [],
695
696
  };
@@ -751,6 +752,7 @@ export function createRouter<TEnv = any>(
751
752
  trailingSlash: trailingSlashConfig,
752
753
  handler: urlPatterns.handler,
753
754
  mountIndex: currentMountIndex,
755
+ routerId,
754
756
  cacheProfiles: resolvedCacheProfiles,
755
757
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
756
758
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -770,6 +772,7 @@ export function createRouter<TEnv = any>(
770
772
  trailingSlash: trailingSlashConfig,
771
773
  handler: urlPatterns.handler,
772
774
  mountIndex: currentMountIndex,
775
+ routerId,
773
776
  cacheProfiles: resolvedCacheProfiles,
774
777
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
775
778
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -813,6 +816,7 @@ export function createRouter<TEnv = any>(
813
816
  trailingSlash: trailingSlashConfig,
814
817
  handler: urlPatterns.handler,
815
818
  mountIndex: mountIndex++,
819
+ routerId,
816
820
  // Lazy evaluation fields
817
821
  lazy: true,
818
822
  lazyPatterns: lazyInclude.patterns,
@@ -20,6 +20,61 @@ import { RootErrorBoundary } from "./root-error-boundary.js";
20
20
  const ReactViewTransition: any =
21
21
  "ViewTransition" in React ? (React as any).ViewTransition : null;
22
22
 
23
+ function restoreParallelLoaderMarkers(
24
+ segments: ResolvedSegment[],
25
+ ): ResolvedSegment[] {
26
+ const parallelLoadingByNamespace = new Map<string, ReactNode>();
27
+ let nextSegments: ResolvedSegment[] | null = null;
28
+
29
+ for (let i = 0; i < segments.length; i++) {
30
+ const segment = segments[i];
31
+
32
+ if (segment.type === "parallel") {
33
+ if (
34
+ segment.namespace &&
35
+ segment.loading !== undefined &&
36
+ segment.loading !== null &&
37
+ segment.loading !== false
38
+ ) {
39
+ parallelLoadingByNamespace.set(segment.namespace, segment.loading);
40
+ }
41
+ continue;
42
+ }
43
+
44
+ if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
45
+ continue;
46
+ }
47
+
48
+ const parallelLoading = segment.namespace
49
+ ? parallelLoadingByNamespace.get(segment.namespace)
50
+ : undefined;
51
+ if (parallelLoading === undefined) {
52
+ continue;
53
+ }
54
+
55
+ if (!nextSegments) {
56
+ nextSegments = segments.slice();
57
+ }
58
+ nextSegments[i] = { ...segment, parallelLoading };
59
+ }
60
+
61
+ return nextSegments ?? segments;
62
+ }
63
+
64
+ function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
65
+ if (!a || a.length !== b.length) {
66
+ return false;
67
+ }
68
+
69
+ for (let i = 0; i < a.length; i++) {
70
+ if (a[i] !== b[i]) {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ return true;
76
+ }
77
+
23
78
  /**
24
79
  * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
25
80
  */
@@ -143,6 +198,10 @@ export async function renderSegments(
143
198
  } = options || {};
144
199
 
145
200
  const temporalLazyRefs: Promise<any>[] = [];
201
+ const normalizedSegments = restoreParallelLoaderMarkers(segments);
202
+ const normalizedInterceptSegments = interceptSegments
203
+ ? restoreParallelLoaderMarkers(interceptSegments)
204
+ : undefined;
146
205
 
147
206
  /**
148
207
  * Registers promises from lazy/async components for awaiting.
@@ -167,7 +226,7 @@ export async function renderSegments(
167
226
  );
168
227
  }
169
228
  // Separate segments by type, passing intercept segments for explicit injection
170
- const tree = segmentTreeWalk(segments, interceptSegments);
229
+ const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
171
230
  // Render content segments as siblings
172
231
  let content: ReactNode = null;
173
232
  for (const node of tree) {
@@ -284,13 +343,90 @@ export async function renderSegments(
284
343
  children: nodeContent,
285
344
  });
286
345
  } else {
287
- // Has loaders but no loading skeleton - await loaders and render directly
288
- const resolvedData = await loaderDataPromise;
346
+ // Has loaders but no loading skeleton.
347
+ // Split: parallel-owned loaders stream (their parallel has loading()),
348
+ // layout-owned loaders are awaited (they gate the layout content).
349
+ const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
350
+ const parallelOwnedLoaders = loaderEntries.filter(
351
+ (l) => !!l.parallelLoading,
352
+ );
353
+
354
+ // Await only layout-owned loaders
355
+ const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
356
+ const layoutLoaderDataPromise =
357
+ layoutLoaders.length > 0
358
+ ? Promise.all(
359
+ layoutLoaders.map((l) =>
360
+ l.loaderData instanceof Promise
361
+ ? l.loaderData
362
+ : Promise.resolve(l.loaderData),
363
+ ),
364
+ )
365
+ : Promise.resolve([]);
366
+ const resolvedData = await layoutLoaderDataPromise;
289
367
  const { loaderData, errorFallback } = resolveLoaderData(
290
368
  resolvedData,
291
- loaderIds,
369
+ layoutLoaderIds,
292
370
  );
293
371
 
372
+ // Parallel-owned loaders: attach to their owning parallel segment
373
+ // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
374
+ if (parallelOwnedLoaders.length > 0) {
375
+ const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
376
+
377
+ for (const loader of parallelOwnedLoaders) {
378
+ if (!loader.namespace) {
379
+ continue;
380
+ }
381
+ const existing = loadersByParallelNamespace.get(loader.namespace);
382
+ if (existing) {
383
+ existing.push(loader);
384
+ } else {
385
+ loadersByParallelNamespace.set(loader.namespace, [loader]);
386
+ }
387
+ }
388
+
389
+ for (const p of node.parallel) {
390
+ if (!p.loading || !p.namespace) {
391
+ continue;
392
+ }
393
+
394
+ const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
395
+ if (!ownedLoaders || ownedLoaders.length === 0) {
396
+ continue;
397
+ }
398
+
399
+ const parallelLoaderIds = ownedLoaders.map((l) => l.loaderId!);
400
+ const parallelLoaderSources = ownedLoaders.map((l) => l.loaderData);
401
+ p.loaderIds = parallelLoaderIds;
402
+
403
+ const shouldReuseParallelPromise =
404
+ p.loaderDataPromise !== undefined &&
405
+ hasSameReferences(p.parallelLoaderSources, parallelLoaderSources);
406
+
407
+ const parallelLoaderDataPromise = shouldReuseParallelPromise
408
+ ? p.loaderDataPromise
409
+ : forceAwait || isAction
410
+ ? await Promise.all(
411
+ ownedLoaders.map((l) =>
412
+ l.loaderData instanceof Promise
413
+ ? l.loaderData
414
+ : Promise.resolve(l.loaderData),
415
+ ),
416
+ )
417
+ : Promise.all(
418
+ ownedLoaders.map((l) =>
419
+ l.loaderData instanceof Promise
420
+ ? l.loaderData
421
+ : Promise.resolve(l.loaderData),
422
+ ),
423
+ );
424
+
425
+ p.loaderDataPromise = parallelLoaderDataPromise;
426
+ p.parallelLoaderSources = parallelLoaderSources;
427
+ }
428
+ }
429
+
294
430
  content = createElement(OutletProvider, {
295
431
  key,
296
432
  content: outletContent,
@@ -157,10 +157,24 @@ export type InterceptEntry = {
157
157
  when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
158
158
  };
159
159
 
160
+ export interface ParallelEntryData
161
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
162
+ type: "parallel";
163
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
+ loading?: ReactNode | false;
165
+ transition?: TransitionConfig;
166
+ /** Set when any parallel slot is a Static definition */
167
+ isStaticPrerender?: true;
168
+ /** Per-slot static handler $$ids for build-time store lookup */
169
+ staticHandlerIds?: Record<string, string>;
170
+ }
171
+
172
+ export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
+
160
174
  export type EntryPropSegments = {
161
175
  loader: LoaderEntry[];
162
176
  layout: EntryData[];
163
- parallel: EntryData[]; // type: "parallel" entries with their own loaders/revalidate/loading
177
+ parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
164
178
  intercept: InterceptEntry[]; // intercept definitions for soft navigation
165
179
  };
166
180
 
@@ -200,18 +214,7 @@ export type EntryData =
200
214
  } & EntryPropCommon &
201
215
  EntryPropDatas &
202
216
  EntryPropSegments)
203
- | ({
204
- type: "parallel";
205
- handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
206
- loading?: ReactNode | false;
207
- transition?: TransitionConfig;
208
- /** Set when any parallel slot is a Static definition */
209
- isStaticPrerender?: true;
210
- /** Per-slot static handler $$ids for build-time store lookup */
211
- staticHandlerIds?: Record<string, string>;
212
- } & EntryPropCommon &
213
- EntryPropDatas &
214
- EntryPropSegments)
217
+ | ParallelEntryData
215
218
  | ({
216
219
  type: "cache";
217
220
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
@@ -553,6 +556,80 @@ export function getRootScoped(): boolean {
553
556
  // Export HelperContext type for use in other modules
554
557
  export type { HelperContext };
555
558
 
559
+ /**
560
+ * Return an isolated copy of a lazy include's captured parent entry.
561
+ *
562
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
563
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
564
+ * parent, so without isolation one include's loaders/middleware leak into
565
+ * every other route that shares that root.
566
+ *
567
+ * The clone is shallow: only the mutable arrays are copied so each
568
+ * include pushes to its own list. The rest of the entry (id, shortCode,
569
+ * parent pointer, handler) stays shared, which is correct and cheap.
570
+ */
571
+ export function getIsolatedLazyParent(
572
+ captured: EntryData | null | undefined,
573
+ ): EntryData | null {
574
+ if (!captured) return null;
575
+ return {
576
+ ...captured,
577
+ loader: [...captured.loader],
578
+ middleware: [...captured.middleware],
579
+ revalidate: [...captured.revalidate],
580
+ errorBoundary: [...captured.errorBoundary],
581
+ notFoundBoundary: [...captured.notFoundBoundary],
582
+ layout: [...captured.layout],
583
+ parallel: { ...captured.parallel },
584
+ intercept: [...captured.intercept],
585
+ };
586
+ }
587
+
588
+ export function getParallelEntries(
589
+ parallels: ParallelEntries | EntryData[] | undefined,
590
+ ): ParallelEntryData[] {
591
+ if (!parallels) return [];
592
+ if (Array.isArray(parallels)) {
593
+ return parallels.filter(
594
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
595
+ );
596
+ }
597
+ return Object.values(parallels).filter(
598
+ (entry): entry is ParallelEntryData => !!entry,
599
+ );
600
+ }
601
+
602
+ export function getParallelSlotEntries(
603
+ parallels: ParallelEntries | EntryData[] | undefined,
604
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
605
+ if (!parallels) return [];
606
+
607
+ if (Array.isArray(parallels)) {
608
+ return getParallelEntries(parallels).flatMap((entry) =>
609
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
610
+ slot,
611
+ entry,
612
+ })),
613
+ );
614
+ }
615
+
616
+ return Object.entries(parallels)
617
+ .filter(([, entry]) => !!entry)
618
+ .map(([slot, entry]) => ({
619
+ slot: slot as `@${string}`,
620
+ entry: entry!,
621
+ }));
622
+ }
623
+
624
+ export function getParallelSlotCount(
625
+ parallels: ParallelEntries | EntryData[] | undefined,
626
+ ): number {
627
+ if (!parallels) return 0;
628
+ return Array.isArray(parallels)
629
+ ? parallels.filter((entry) => entry?.type === "parallel").length
630
+ : Object.keys(parallels).length;
631
+ }
632
+
556
633
  // ============================================================================
557
634
  // Performance Metrics Helpers
558
635
  // ============================================================================
@@ -30,7 +30,10 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
30
30
  import { THEME_COOKIE } from "../theme/constants.js";
31
31
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
32
32
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
33
- import { createReverseFunction } from "../router/handler-context.js";
33
+ import {
34
+ createReverseFunction,
35
+ stripInternalParams,
36
+ } from "../router/handler-context.js";
34
37
  import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
35
38
  import { invariant } from "../errors.js";
36
39
  import { isAutoGeneratedRouteName } from "../route-name.js";
@@ -58,7 +61,7 @@ export interface RequestContext<
58
61
  originalUrl: URL;
59
62
  /** URL pathname */
60
63
  pathname: string;
61
- /** URL search params (system params like _rsc* are NOT filtered here) */
64
+ /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
62
65
  searchParams: URLSearchParams;
63
66
  /** Variables set by middleware (same as ctx.var) */
64
67
  var: Record<string, any>;
@@ -555,14 +558,17 @@ export function createRequestContext<TEnv>(
555
558
  invalidateResponseCookieCache();
556
559
  };
557
560
 
561
+ // Strip internal _rsc* params so userland sees a clean URL.
562
+ const cleanUrl = stripInternalParams(url);
563
+
558
564
  // Build the context object first (without use), then add use
559
565
  const ctx: RequestContext<TEnv> = {
560
566
  env,
561
567
  request,
562
- url,
568
+ url: cleanUrl,
563
569
  originalUrl: new URL(request.url),
564
570
  pathname: url.pathname,
565
- searchParams: url.searchParams,
571
+ searchParams: cleanUrl.searchParams,
566
572
  var: variables,
567
573
  get: ((keyOrVar: any) =>
568
574
  contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
package/src/ssr/index.tsx CHANGED
@@ -168,6 +168,7 @@ function createSsrEventController(opts: {
168
168
  const state: DerivedNavigationState = {
169
169
  state: "idle",
170
170
  isStreaming: false,
171
+ isNavigating: false,
171
172
  location,
172
173
  pendingUrl: null,
173
174
  inflightActions: [],
@@ -289,8 +289,12 @@ export type HandlerContext<
289
289
  /**
290
290
  * Access loader data or push handle data.
291
291
  *
292
+ * Available in route handlers, layout handlers, middleware, server actions,
293
+ * and server components rendered within the request context.
294
+ *
292
295
  * For loaders: Returns a promise that resolves to the loader data.
293
- * Loaders are executed in parallel and memoized per request.
296
+ * Loaders are executed in parallel and memoized per request — calling
297
+ * `ctx.use(SameLoader)` multiple times returns the same promise.
294
298
  *
295
299
  * For handles: Returns a push function to add data for this segment.
296
300
  * Handle data accumulates across all matched route segments.
@@ -519,30 +523,112 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
519
523
  * })
520
524
  * ```
521
525
  */
526
+ /**
527
+ * Revalidation function called during client-side navigation to decide whether
528
+ * a segment (layout, route, parallel slot, or loader) should be re-rendered.
529
+ *
530
+ * Return `true` to re-render, `false` to skip (keep client's current version),
531
+ * or `{ defaultShouldRevalidate: boolean }` to override the default for
532
+ * downstream segments.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * // Re-render only when a cart action happened or browser signals staleness
537
+ * revalidate(({ actionId, stale }) =>
538
+ * actionId?.includes("cart") || stale || false
539
+ * )
540
+ *
541
+ * // Always re-render when params change (default behavior made explicit)
542
+ * revalidate(({ defaultShouldRevalidate }) => defaultShouldRevalidate)
543
+ * ```
544
+ */
522
545
  export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
546
+ /** Route params from the page being navigated away from. */
523
547
  currentParams: TParams;
548
+ /** Full URL of the page being navigated away from. */
524
549
  currentUrl: URL;
550
+ /** Route params for the navigation target. */
525
551
  nextParams: TParams;
552
+ /** Full URL of the navigation target. */
526
553
  nextUrl: URL;
554
+ /**
555
+ * The router's default revalidation decision for this segment.
556
+ * `true` when params changed or the segment is new to the client.
557
+ * Return this when you want default behavior plus your own conditions.
558
+ */
527
559
  defaultShouldRevalidate: boolean;
560
+ /** Full handler context — access to `ctx.use()`, `ctx.env`, `ctx.params`, etc. */
528
561
  context: HandlerContext<TParams, TEnv>;
529
- // Segment metadata (which segment is being evaluated):
562
+
563
+ // ── Segment metadata (which segment is being evaluated) ──────────────
564
+
565
+ /** The type of segment being revalidated. */
530
566
  segmentType: "layout" | "route" | "parallel";
531
- layoutName?: string; // Layout name (e.g., "root", "shop", "auth") - only for layouts
532
- slotName?: string; // Slot name (e.g., "@sidebar", "@modal") - only for parallels
533
- // Action context (populated when revalidation triggered by server action):
534
- actionId?: string; // Action identifier (e.g., "src/actions.ts#addToCart")
535
- actionUrl?: URL; // URL where action was executed
536
- actionResult?: any; // Return value from action execution
537
- formData?: FormData; // FormData from action request
538
- method?: string; // Request method: 'GET' for navigation, 'POST' for actions
539
- routeName?: DefaultRouteName; // Route name of the navigation target (alias for toRouteName)
540
- // Named-route identity for both ends of a navigation transition.
541
- // Undefined for unnamed internal routes (those without a `name` option).
542
- fromRouteName?: DefaultRouteName; // Route name being navigated away from
543
- toRouteName?: DefaultRouteName; // Route name being navigated to
544
- // Stale cache revalidation (SWR pattern):
545
- stale?: boolean; // True if this is a stale cache revalidation request
567
+ /** Layout name (e.g., `"root"`, `"shop"`, `"auth"`). Only set for layout segments. */
568
+ layoutName?: string;
569
+ /** Slot name (e.g., `"@sidebar"`, `"@modal"`). Only set for parallel segments. */
570
+ slotName?: string;
571
+
572
+ // ── Action context (populated when revalidation is triggered by a server action) ──
573
+
574
+ /**
575
+ * Identifier of the server action that triggered revalidation.
576
+ * `undefined` during normal navigation (no action involved).
577
+ *
578
+ * Format: `"src/<path>#<exportName>"` the file path is the source path
579
+ * relative to the project root, followed by `#` and the exported function name.
580
+ *
581
+ * This is stable and can be used for path-based matching to revalidate
582
+ * when any action in a module or directory fires:
583
+ *
584
+ * @example
585
+ * ```ts
586
+ * // Match a specific action
587
+ * revalidate(({ actionId }) => actionId === "src/actions/cart.ts#addToCart")
588
+ *
589
+ * // Match any action in the cart module
590
+ * revalidate(({ actionId }) => actionId?.includes("cart") ?? false)
591
+ *
592
+ * // Match any action under src/apps/store/actions/
593
+ * revalidate(({ actionId }) => actionId?.startsWith("src/apps/store/actions/") ?? false)
594
+ * ```
595
+ */
596
+ actionId?: string;
597
+ /** URL where the action was executed (the page the user was on when they triggered the action). */
598
+ actionUrl?: URL;
599
+ /** Return value from the action execution. Can be used to conditionally revalidate based on the action's outcome. */
600
+ actionResult?: any;
601
+ /** FormData from the action request body. Only set for form-based actions (not inline `"use server"` actions). */
602
+ formData?: FormData;
603
+ /** HTTP method: `"GET"` for navigation, `"POST"` for server actions. */
604
+ method?: string;
605
+
606
+ // ── Route identity ───────────────────────────────────────────────────
607
+
608
+ /** Route name of the navigation target. Alias for `toRouteName`. */
609
+ routeName?: DefaultRouteName;
610
+ /**
611
+ * Route name being navigated away from.
612
+ * `undefined` for unnamed internal routes (those without a `name` option).
613
+ */
614
+ fromRouteName?: DefaultRouteName;
615
+ /**
616
+ * Route name being navigated to.
617
+ * `undefined` for unnamed internal routes (those without a `name` option).
618
+ */
619
+ toRouteName?: DefaultRouteName;
620
+
621
+ // ── Staleness signal ─────────────────────────────────────────────────
622
+
623
+ /**
624
+ * `true` when the browser signals that data may be stale — typically because
625
+ * a server action was executed in this or another tab (`_rsc_stale` header).
626
+ *
627
+ * This is NOT segment cache staleness (loaders are never segment-cached).
628
+ * Use this to decide whether loader data should be re-fetched after an
629
+ * action that may have mutated backend state.
630
+ */
631
+ stale?: boolean;
546
632
  }) => boolean | { defaultShouldRevalidate: boolean };
547
633
 
548
634
  // MiddlewareFn is imported from "../router/middleware.js" and re-exported
@@ -55,6 +55,13 @@ export interface RouteEntry<TEnv = any> {
55
55
  | Promise<() => Array<AllUseItems>>;
56
56
  mountIndex: number;
57
57
 
58
+ /**
59
+ * Router ID that owns this entry. Used to namespace the manifest cache
60
+ * so multi-router setups (host routing) don't share cached EntryData
61
+ * across routers with overlapping mountIndex + routeKey combinations.
62
+ */
63
+ routerId?: string;
64
+
58
65
  /**
59
66
  * Route keys in this entry that have pre-render handlers.
60
67
  * Used by the non-trie match path to set the `pr` flag.
@@ -51,9 +51,11 @@ export interface ResolvedSegment {
51
51
  // Loader-specific fields
52
52
  loaderId?: string; // For loaders: the loader $$id identifier
53
53
  loaderData?: any; // For loaders: the resolved data from loader execution
54
+ parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
54
55
  // Intercept loader fields (for streaming loader data in parallel segments)
55
56
  loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
56
57
  loaderIds?: string[]; // IDs ($$id) of loaders for this segment
58
+ parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
57
59
  // Error-specific fields
58
60
  error?: ErrorInfo; // For error segments: the error information
59
61
  // NotFound-specific fields
@@ -199,7 +199,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
199
199
  errorBoundary: [],
200
200
  notFoundBoundary: [],
201
201
  layout: [],
202
- parallel: [],
202
+ parallel: {},
203
203
  intercept: [],
204
204
  loader: [],
205
205
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
@@ -13,8 +13,6 @@ export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
13
13
  export interface PluginOptions {
14
14
  enableBuildPrerender?: boolean;
15
15
  staticRouteTypesGeneration?: boolean;
16
- include?: string[];
17
- exclude?: string[];
18
16
  // Mutable ref for deferred auto-discovery (node preset).
19
17
  // The auto-discover config() hook populates this before configResolved.
20
18
  routerPathRef?: { path?: string };