@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1fa245e2

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 (136) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +76 -18
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +558 -319
  5. package/package.json +16 -15
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +45 -4
  8. package/skills/links/SKILL.md +3 -1
  9. package/skills/loader/SKILL.md +53 -43
  10. package/skills/middleware/SKILL.md +2 -0
  11. package/skills/parallel/SKILL.md +126 -0
  12. package/skills/prerender/SKILL.md +110 -68
  13. package/skills/route/SKILL.md +31 -0
  14. package/skills/router-setup/SKILL.md +87 -2
  15. package/skills/typesafety/SKILL.md +10 -0
  16. package/src/__internal.ts +1 -1
  17. package/src/browser/app-version.ts +14 -0
  18. package/src/browser/event-controller.ts +5 -0
  19. package/src/browser/navigation-bridge.ts +19 -13
  20. package/src/browser/navigation-client.ts +115 -58
  21. package/src/browser/navigation-store.ts +43 -8
  22. package/src/browser/navigation-transaction.ts +11 -9
  23. package/src/browser/partial-update.ts +80 -15
  24. package/src/browser/prefetch/cache.ts +57 -5
  25. package/src/browser/prefetch/fetch.ts +38 -23
  26. package/src/browser/prefetch/queue.ts +92 -20
  27. package/src/browser/prefetch/resource-ready.ts +77 -0
  28. package/src/browser/react/Link.tsx +53 -9
  29. package/src/browser/react/NavigationProvider.tsx +40 -4
  30. package/src/browser/react/context.ts +7 -2
  31. package/src/browser/react/use-handle.ts +9 -58
  32. package/src/browser/react/use-router.ts +21 -8
  33. package/src/browser/rsc-router.tsx +134 -59
  34. package/src/browser/scroll-restoration.ts +41 -42
  35. package/src/browser/segment-reconciler.ts +6 -1
  36. package/src/browser/server-action-bridge.ts +8 -6
  37. package/src/browser/types.ts +36 -5
  38. package/src/build/generate-manifest.ts +6 -6
  39. package/src/build/generate-route-types.ts +3 -0
  40. package/src/build/route-types/include-resolution.ts +8 -1
  41. package/src/build/route-types/router-processing.ts +223 -74
  42. package/src/build/route-types/scan-filter.ts +8 -1
  43. package/src/cache/cache-runtime.ts +15 -11
  44. package/src/cache/cache-scope.ts +48 -7
  45. package/src/cache/cf/cf-cache-store.ts +453 -11
  46. package/src/cache/cf/index.ts +5 -1
  47. package/src/cache/document-cache.ts +17 -7
  48. package/src/cache/index.ts +1 -0
  49. package/src/cache/taint.ts +55 -0
  50. package/src/client.tsx +2 -56
  51. package/src/context-var.ts +72 -2
  52. package/src/debug.ts +2 -2
  53. package/src/handle.ts +40 -0
  54. package/src/index.rsc.ts +3 -1
  55. package/src/index.ts +8 -0
  56. package/src/prerender/store.ts +5 -4
  57. package/src/prerender.ts +138 -77
  58. package/src/reverse.ts +22 -1
  59. package/src/route-definition/dsl-helpers.ts +73 -25
  60. package/src/route-definition/helpers-types.ts +10 -6
  61. package/src/route-definition/index.ts +3 -0
  62. package/src/route-definition/redirect.ts +11 -3
  63. package/src/route-definition/resolve-handler-use.ts +149 -0
  64. package/src/route-map-builder.ts +7 -1
  65. package/src/route-types.ts +11 -0
  66. package/src/router/content-negotiation.ts +100 -1
  67. package/src/router/find-match.ts +4 -2
  68. package/src/router/handler-context.ts +79 -23
  69. package/src/router/intercept-resolution.ts +11 -4
  70. package/src/router/lazy-includes.ts +4 -1
  71. package/src/router/loader-resolution.ts +122 -10
  72. package/src/router/logging.ts +5 -2
  73. package/src/router/manifest.ts +9 -3
  74. package/src/router/match-api.ts +124 -189
  75. package/src/router/match-middleware/background-revalidation.ts +30 -2
  76. package/src/router/match-middleware/cache-lookup.ts +88 -16
  77. package/src/router/match-middleware/cache-store.ts +53 -10
  78. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  79. package/src/router/match-middleware/segment-resolution.ts +61 -5
  80. package/src/router/match-result.ts +22 -6
  81. package/src/router/metrics.ts +6 -1
  82. package/src/router/middleware-types.ts +6 -8
  83. package/src/router/middleware.ts +4 -6
  84. package/src/router/navigation-snapshot.ts +182 -0
  85. package/src/router/prerender-match.ts +110 -10
  86. package/src/router/preview-match.ts +30 -102
  87. package/src/router/request-classification.ts +310 -0
  88. package/src/router/route-snapshot.ts +245 -0
  89. package/src/router/router-context.ts +6 -1
  90. package/src/router/router-interfaces.ts +36 -4
  91. package/src/router/router-options.ts +37 -11
  92. package/src/router/segment-resolution/fresh.ts +183 -20
  93. package/src/router/segment-resolution/helpers.ts +29 -24
  94. package/src/router/segment-resolution/loader-cache.ts +1 -0
  95. package/src/router/segment-resolution/revalidation.ts +412 -297
  96. package/src/router/segment-wrappers.ts +2 -0
  97. package/src/router/types.ts +1 -0
  98. package/src/router.ts +59 -6
  99. package/src/rsc/handler.ts +460 -368
  100. package/src/rsc/manifest-init.ts +5 -1
  101. package/src/rsc/progressive-enhancement.ts +4 -0
  102. package/src/rsc/rsc-rendering.ts +5 -0
  103. package/src/rsc/server-action.ts +2 -0
  104. package/src/rsc/ssr-setup.ts +2 -2
  105. package/src/rsc/types.ts +8 -1
  106. package/src/segment-system.tsx +140 -4
  107. package/src/server/context.ts +140 -14
  108. package/src/server/loader-registry.ts +9 -8
  109. package/src/server/request-context.ts +144 -18
  110. package/src/ssr/index.tsx +4 -0
  111. package/src/static-handler.ts +18 -6
  112. package/src/types/cache-types.ts +4 -4
  113. package/src/types/handler-context.ts +137 -33
  114. package/src/types/loader-types.ts +36 -9
  115. package/src/types/route-entry.ts +8 -1
  116. package/src/types/segments.ts +2 -0
  117. package/src/urls/path-helper-types.ts +9 -2
  118. package/src/urls/path-helper.ts +48 -13
  119. package/src/urls/pattern-types.ts +12 -0
  120. package/src/urls/response-types.ts +16 -6
  121. package/src/use-loader.tsx +73 -4
  122. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  123. package/src/vite/discovery/discover-routers.ts +5 -1
  124. package/src/vite/discovery/prerender-collection.ts +14 -1
  125. package/src/vite/discovery/state.ts +13 -6
  126. package/src/vite/index.ts +4 -0
  127. package/src/vite/plugin-types.ts +51 -79
  128. package/src/vite/plugins/expose-action-id.ts +1 -3
  129. package/src/vite/plugins/performance-tracks.ts +88 -0
  130. package/src/vite/plugins/refresh-cmd.ts +88 -26
  131. package/src/vite/plugins/version-plugin.ts +13 -1
  132. package/src/vite/rango.ts +163 -211
  133. package/src/vite/router-discovery.ts +153 -42
  134. package/src/vite/utils/banner.ts +3 -3
  135. package/src/vite/utils/prerender-utils.ts +18 -0
  136. package/src/vite/utils/shared-utils.ts +3 -2
@@ -8,6 +8,7 @@ import type {
8
8
  import type { NonceProvider } from "../rsc/types.js";
9
9
  import type { ExecutionContext } from "../server/request-context.js";
10
10
  import type { UrlPatterns } from "../urls.js";
11
+ import type { UrlBuilder } from "../urls/pattern-types.js";
11
12
  import type { NamedRouteEntry } from "./content-negotiation.js";
12
13
  import type { TelemetrySink } from "./telemetry.js";
13
14
  import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
@@ -95,6 +96,28 @@ export interface RSCRouterOptions<TEnv = any> {
95
96
  */
96
97
  $$sourceFile?: string;
97
98
 
99
+ /**
100
+ * URL prefix applied to all routes registered with this router.
101
+ *
102
+ * Useful when the app is served under a sub-path (e.g. `/admin` or `/v2`).
103
+ * All `path()` patterns are automatically prefixed and `reverse()` returns
104
+ * full paths including the basename. Route names are NOT prefixed.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const router = createRouter({
109
+ * basename: "/admin",
110
+ * }).routes(({ path }) => [
111
+ * path("/", Dashboard, { name: "home" }), // matches /admin
112
+ * path("/users", Users, { name: "users" }), // matches /admin/users
113
+ * ]);
114
+ *
115
+ * router.reverse("home"); // "/admin"
116
+ * router.reverse("users"); // "/admin/users"
117
+ * ```
118
+ */
119
+ basename?: string;
120
+
98
121
  /**
99
122
  * Enable performance metrics collection
100
123
  * When enabled, metrics are output to console and available via Server-Timing header
@@ -337,25 +360,28 @@ export interface RSCRouterOptions<TEnv = any> {
337
360
  /**
338
361
  * URL patterns to register with the router.
339
362
  *
340
- * Alternative to calling `.routes()` method - allows passing patterns
341
- * directly in the config for a more concise setup.
363
+ * Accepts either a `UrlPatterns` object from `urls()` or a builder function
364
+ * directly (urls() is called implicitly).
342
365
  *
343
366
  * @example
344
367
  * ```typescript
345
- * import { urls } from "@rangojs/router/server";
346
- *
347
- * const urlpatterns = urls(({ path, layout }) => [
348
- * path("/", HomePage, { name: "home" }),
349
- * path("/about", AboutPage, { name: "about" }),
350
- * ]);
351
- *
352
- * const router = createRouter<AppEnv>({
368
+ * // With urls()
369
+ * createRouter<AppEnv>({
353
370
  * document: Document,
354
371
  * urls: urlpatterns,
355
372
  * });
373
+ *
374
+ * // With builder function
375
+ * createRouter<AppEnv>({
376
+ * document: Document,
377
+ * urls: ({ path }) => [
378
+ * path("/", HomePage, { name: "home" }),
379
+ * path("/about", AboutPage, { name: "about" }),
380
+ * ],
381
+ * });
356
382
  * ```
357
383
  */
358
- urls?: UrlPatterns<TEnv, any>;
384
+ urls?: UrlPatterns<TEnv, any> | UrlBuilder<TEnv>;
359
385
 
360
386
  /**
361
387
  * Injected by the Vite transform at compile time.
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { ReactNode } from "react";
9
9
  import { invariant } from "../../errors";
10
- import type { EntryData } from "../../server/context";
10
+ import {
11
+ getParallelEntries,
12
+ getParallelSlotEntries,
13
+ type EntryData,
14
+ } from "../../server/context";
11
15
  import type {
12
16
  HandlerContext,
13
17
  InternalHandlerContext,
@@ -15,6 +19,8 @@ import type {
15
19
  } from "../../types";
16
20
  import type { SegmentResolutionDeps } from "../types.js";
17
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
18
24
  import {
19
25
  handleHandlerResult,
20
26
  tryStaticHandler,
@@ -24,7 +30,11 @@ import {
24
30
  } from "./helpers.js";
25
31
  import { getRouterContext } from "../router-context.js";
26
32
  import { resolveSink, safeEmit } from "../telemetry.js";
27
- import { track } from "../../server/context.js";
33
+ import {
34
+ track,
35
+ RSCRouterContext,
36
+ runInsideLoaderScope,
37
+ } from "../../server/context.js";
28
38
 
29
39
  // ---------------------------------------------------------------------------
30
40
  // Streamed handler telemetry
@@ -90,9 +100,11 @@ export async function resolveLoaders<TEnv>(
90
100
  const shortCode = shortCodeOverride ?? entry.shortCode;
91
101
  const hasLoading = "loading" in entry && entry.loading !== undefined;
92
102
  const loadingDisabled = hasLoading && entry.loading === false;
103
+ const ms = _getRequestContext()?._metricsStore;
93
104
 
94
105
  if (!loadingDisabled) {
95
- return loaderEntries.map((loaderEntry, i) => {
106
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
107
+ const segments = loaderEntries.map((loaderEntry, i) => {
96
108
  const { loader } = loaderEntry;
97
109
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
98
110
  return {
@@ -104,7 +116,9 @@ export async function resolveLoaders<TEnv>(
104
116
  params: ctx.params,
105
117
  loaderId: loader.$$id,
106
118
  loaderData: deps.wrapLoaderPromise(
107
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
119
+ runInsideLoaderScope(() =>
120
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
+ ),
108
122
  entry,
109
123
  segmentId,
110
124
  ctx.pathname,
@@ -112,18 +126,38 @@ export async function resolveLoaders<TEnv>(
112
126
  belongsToRoute,
113
127
  };
114
128
  });
129
+
130
+ return segments;
115
131
  }
116
132
 
117
133
  // Loading disabled: still start all loaders in parallel, but only emit
118
134
  // settled promises so handlers don't stream loading placeholders.
119
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
120
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
- );
122
- await Promise.all(pendingLoaderData);
135
+ const pendingLoaderData = loaderEntries.map((loaderEntry) => {
136
+ const start = performance.now();
137
+ const promise = runInsideLoaderScope(() =>
138
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
139
+ );
140
+ return { promise, start, loaderId: loaderEntry.loader.$$id };
141
+ });
142
+ await Promise.all(pendingLoaderData.map((p) => p.promise));
123
143
 
124
144
  return loaderEntries.map((loaderEntry, i) => {
125
145
  const { loader } = loaderEntry;
126
146
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
147
+ const pending = pendingLoaderData[i]!;
148
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
149
+ // All loaders ran in parallel via Promise.all — each span covers
150
+ // from its own kickoff to the batch settlement, giving a ceiling
151
+ // on that loader's contribution to the overall wait.
152
+ const batchEnd = performance.now();
153
+ appendMetric(
154
+ ms,
155
+ `loader:${loader.$$id}`,
156
+ pending.start,
157
+ batchEnd - pending.start,
158
+ 2,
159
+ );
160
+ }
127
161
  return {
128
162
  id: segmentId,
129
163
  namespace: entry.id,
@@ -133,7 +167,7 @@ export async function resolveLoaders<TEnv>(
133
167
  params: ctx.params,
134
168
  loaderId: loader.$$id,
135
169
  loaderData: deps.wrapLoaderPromise(
136
- pendingLoaderData[i]!,
170
+ pending.promise,
137
171
  entry,
138
172
  segmentId,
139
173
  ctx.pathname,
@@ -197,7 +231,10 @@ export async function resolveSegment<TEnv>(
197
231
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
198
232
  });
199
233
 
200
- for (const parallelEntry of entry.parallel) {
234
+ const resolvedParallelEntries = new Set<string>();
235
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
236
+ entry.parallel,
237
+ )) {
201
238
  const parallelSegments = await resolveParallelEntry(
202
239
  parallelEntry,
203
240
  params,
@@ -207,8 +244,11 @@ export async function resolveSegment<TEnv>(
207
244
  deps,
208
245
  options,
209
246
  routeKey,
247
+ [slot],
248
+ !resolvedParallelEntries.has(parallelEntry.id),
210
249
  );
211
250
  segments.push(...parallelSegments);
251
+ resolvedParallelEntries.add(parallelEntry.id);
212
252
  }
213
253
 
214
254
  for (const orphan of entry.layout) {
@@ -244,9 +284,14 @@ export async function resolveSegment<TEnv>(
244
284
  entry.shortCode,
245
285
  );
246
286
  if (component === undefined) {
287
+ // For Passthrough routes at runtime, use the live handler instead of
288
+ // the build handler. At build time (context.build === true), always
289
+ // use the build handler from entry.handler.
290
+ const handler =
291
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
247
292
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
248
293
  if (entry.loading) {
249
- const result = handleHandlerResult(entry.handler(context));
294
+ const result = handleHandlerResult(handler(context));
250
295
  if (result instanceof Promise) {
251
296
  result.finally(doneRouteHandler).catch(() => {});
252
297
  const tracked = deps.trackHandler(result, {
@@ -267,7 +312,7 @@ export async function resolveSegment<TEnv>(
267
312
  component = result;
268
313
  }
269
314
  } else {
270
- component = handleHandlerResult(await entry.handler(context));
315
+ component = handleHandlerResult(await handler(context));
271
316
  doneRouteHandler();
272
317
  }
273
318
  }
@@ -282,11 +327,15 @@ export async function resolveSegment<TEnv>(
282
327
  deps,
283
328
  options,
284
329
  routeKey,
330
+ entry,
285
331
  );
286
332
  segments.push(...orphanSegments);
287
333
  }
288
334
 
289
- for (const parallelEntry of entry.parallel) {
335
+ const resolvedParallelEntries = new Set<string>();
336
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
337
+ entry.parallel,
338
+ )) {
290
339
  const parallelSegments = await resolveParallelEntry(
291
340
  parallelEntry,
292
341
  params,
@@ -296,8 +345,11 @@ export async function resolveSegment<TEnv>(
296
345
  deps,
297
346
  options,
298
347
  routeKey,
348
+ [slot],
349
+ !resolvedParallelEntries.has(parallelEntry.id),
299
350
  );
300
351
  segments.push(...parallelSegments);
352
+ resolvedParallelEntries.add(parallelEntry.id);
301
353
  }
302
354
 
303
355
  segments.push({
@@ -305,7 +357,7 @@ export async function resolveSegment<TEnv>(
305
357
  namespace: entry.id,
306
358
  type: "route",
307
359
  index: 0,
308
- component,
360
+ component: component ?? null,
309
361
  loading: entry.loading === false ? null : entry.loading,
310
362
  transition: entry.transition,
311
363
  params,
@@ -331,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
331
383
  deps: SegmentResolutionDeps<TEnv>,
332
384
  options?: ResolveSegmentOptions,
333
385
  routeKey?: string,
386
+ /** Parent route entry — its loaders are inherited by the layout so
387
+ * parallel slots inside this layout can access them via useLoader(). */
388
+ parentRouteEntry?: EntryData,
334
389
  ): Promise<ResolvedSegment[]> {
335
390
  invariant(
336
391
  orphan.type === "layout" || orphan.type === "cache",
@@ -346,6 +401,26 @@ export async function resolveOrphanLayout<TEnv>(
346
401
  deps,
347
402
  );
348
403
  segments.push(...loaderSegments);
404
+
405
+ // Inherit parent route's loaders so parallel slots inside this layout
406
+ // can access them via useLoader(). Without this, the route's loaders
407
+ // are only in the route's OutletProvider (rendered as <Outlet /> content),
408
+ // which is a child — not a parent — of the layout's context.
409
+ if (
410
+ parentRouteEntry &&
411
+ parentRouteEntry.loader &&
412
+ parentRouteEntry.loader.length > 0 &&
413
+ Object.keys(orphan.parallel).length > 0
414
+ ) {
415
+ const inheritedLoaders = await resolveLoaders(
416
+ parentRouteEntry,
417
+ context,
418
+ belongsToRoute,
419
+ deps,
420
+ orphan.shortCode,
421
+ );
422
+ segments.push(...inheritedLoaders);
423
+ }
349
424
  }
350
425
 
351
426
  // Handler-first: orphan layout handler executes before its parallels
@@ -368,7 +443,10 @@ export async function resolveOrphanLayout<TEnv>(
368
443
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
369
444
  });
370
445
 
371
- for (const parallelEntry of orphan.parallel) {
446
+ const resolvedParallelEntries = new Set<string>();
447
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
448
+ orphan.parallel,
449
+ )) {
372
450
  const parallelSegments = await resolveParallelEntry(
373
451
  parallelEntry,
374
452
  params,
@@ -378,8 +456,11 @@ export async function resolveOrphanLayout<TEnv>(
378
456
  deps,
379
457
  options,
380
458
  routeKey,
459
+ [slot],
460
+ !resolvedParallelEntries.has(parallelEntry.id),
381
461
  );
382
462
  segments.push(...parallelSegments);
463
+ resolvedParallelEntries.add(parallelEntry.id);
383
464
  }
384
465
 
385
466
  return segments;
@@ -397,6 +478,8 @@ export async function resolveParallelEntry<TEnv>(
397
478
  deps: SegmentResolutionDeps<TEnv>,
398
479
  options?: ResolveSegmentOptions,
399
480
  routeKey?: string,
481
+ slotNames?: `@${string}`[],
482
+ includeLoaders: boolean = true,
400
483
  ): Promise<ResolvedSegment[]> {
401
484
  invariant(
402
485
  parallelEntry.type === "parallel",
@@ -411,7 +494,12 @@ export async function resolveParallelEntry<TEnv>(
411
494
  | ReactNode
412
495
  >;
413
496
 
414
- for (const [slot, handler] of Object.entries(slots)) {
497
+ const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
498
+
499
+ for (const slot of slotsToResolve) {
500
+ // Try static lookup first — in production, handler bodies are evicted
501
+ // and replaced with stubs that have no .handler property (undefined).
502
+ // The static store holds the pre-rendered component for these slots.
415
503
  let component: ReactNode | undefined = await tryStaticSlot(
416
504
  parallelEntry,
417
505
  slot,
@@ -419,6 +507,10 @@ export async function resolveParallelEntry<TEnv>(
419
507
  );
420
508
 
421
509
  if (component === undefined) {
510
+ const handler = slots[slot];
511
+ if (handler === undefined) {
512
+ continue;
513
+ }
422
514
  const doneParallelHandler = track(
423
515
  `handler:${parallelEntry.id}.${slot}`,
424
516
  2,
@@ -472,7 +564,7 @@ export async function resolveParallelEntry<TEnv>(
472
564
  });
473
565
  }
474
566
 
475
- if (!parallelEntry.loading && !options?.skipLoaders) {
567
+ if (!options?.skipLoaders && includeLoaders) {
476
568
  const loaderSegments = await resolveLoaders(
477
569
  parallelEntry,
478
570
  context,
@@ -480,6 +572,15 @@ export async function resolveParallelEntry<TEnv>(
480
572
  deps,
481
573
  parentShortCode,
482
574
  );
575
+ // Tag parallel-owned loaders so renderSegments can stream them
576
+ // using the parallel's loading() instead of awaiting on the layout
577
+ const parallelLoading =
578
+ parallelEntry.loading === false ? undefined : parallelEntry.loading;
579
+ if (parallelLoading) {
580
+ for (const seg of loaderSegments) {
581
+ seg.parallelLoading = parallelLoading;
582
+ }
583
+ }
483
584
  segments.push(...loaderSegments);
484
585
  }
485
586
 
@@ -515,6 +616,13 @@ export async function resolveAllSegments<TEnv>(
515
616
  } catch {}
516
617
 
517
618
  for (const entry of entries) {
619
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
620
+ // can guard non-cacheable variable reads. Also guards response-level
621
+ // side effects (headers.set). Persists for all descendant entries.
622
+ if (entry.type === "cache") {
623
+ const store = RSCRouterContext.getStore();
624
+ if (store) store.insideCacheScope = true;
625
+ }
518
626
  const doneEntry = track(`segment:${entry.id}`, 1);
519
627
  const resolvedSegments = await resolveWithErrorBoundary(
520
628
  entry,
@@ -559,11 +667,66 @@ export async function resolveLoadersOnly<TEnv>(
559
667
  deps: SegmentResolutionDeps<TEnv>,
560
668
  ): Promise<ResolvedSegment[]> {
561
669
  const loaderSegments: ResolvedSegment[] = [];
670
+ const seenIds = new Set<string>();
671
+
672
+ async function collectEntryLoaders(
673
+ entry: EntryData,
674
+ belongsToRoute: boolean,
675
+ shortCodeOverride?: string,
676
+ ): Promise<void> {
677
+ // Skip if all loaders from this entry have already been resolved
678
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
679
+ const entryLoaders = entry.loader ?? [];
680
+ const sc = shortCodeOverride ?? entry.shortCode;
681
+ const allAlreadySeen =
682
+ entryLoaders.length > 0 &&
683
+ entryLoaders.every((le, i) =>
684
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
685
+ );
686
+ if (!allAlreadySeen) {
687
+ const segments = await resolveLoaders(
688
+ entry,
689
+ context,
690
+ belongsToRoute,
691
+ deps,
692
+ shortCodeOverride,
693
+ );
694
+ for (const seg of segments) {
695
+ if (!seenIds.has(seg.id)) {
696
+ seenIds.add(seg.id);
697
+ loaderSegments.push(seg);
698
+ }
699
+ }
700
+ }
701
+
702
+ const seenParallelEntryIds = new Set<string>();
703
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
704
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
705
+ seenParallelEntryIds.add(parallelEntry.id);
706
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
707
+ }
708
+
709
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
710
+ for (const layoutEntry of entry.layout) {
711
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
712
+ // Inherit route loaders for orphan layouts with parallels
713
+ if (
714
+ entry.type === "route" &&
715
+ entry.loader &&
716
+ entry.loader.length > 0 &&
717
+ Object.keys(layoutEntry.parallel).length > 0
718
+ ) {
719
+ await collectEntryLoaders(
720
+ entry,
721
+ childBelongsToRoute,
722
+ layoutEntry.shortCode,
723
+ );
724
+ }
725
+ }
726
+ }
562
727
 
563
728
  for (const entry of entries) {
564
- const belongsToRoute = entry.type === "route";
565
- const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
566
- loaderSegments.push(...segments);
729
+ await collectEntryLoaders(entry, entry.type === "route");
567
730
  }
568
731
 
569
732
  return loaderSegments;
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
147
147
  }
148
148
 
149
149
  const loaderId = loaderEntry.loader.$$id;
150
+
150
151
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
152
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
153
  const swr = swrWindow || undefined;