@rangojs/router 0.0.0-experimental.29 → 0.0.0-experimental.2a0dea97

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 (156) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +78 -19
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +853 -435
  5. package/package.json +17 -16
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +45 -4
  9. package/skills/handler-use/SKILL.md +362 -0
  10. package/skills/hooks/SKILL.md +22 -4
  11. package/skills/intercept/SKILL.md +20 -0
  12. package/skills/layout/SKILL.md +22 -0
  13. package/skills/links/SKILL.md +3 -1
  14. package/skills/loader/SKILL.md +71 -21
  15. package/skills/middleware/SKILL.md +34 -3
  16. package/skills/migrate-nextjs/SKILL.md +560 -0
  17. package/skills/migrate-react-router/SKILL.md +764 -0
  18. package/skills/parallel/SKILL.md +185 -0
  19. package/skills/prerender/SKILL.md +110 -68
  20. package/skills/rango/SKILL.md +24 -22
  21. package/skills/route/SKILL.md +56 -2
  22. package/skills/router-setup/SKILL.md +87 -2
  23. package/skills/typesafety/SKILL.md +33 -21
  24. package/src/__internal.ts +92 -0
  25. package/src/browser/app-version.ts +14 -0
  26. package/src/browser/event-controller.ts +5 -0
  27. package/src/browser/link-interceptor.ts +4 -0
  28. package/src/browser/navigation-bridge.ts +125 -16
  29. package/src/browser/navigation-client.ts +142 -57
  30. package/src/browser/navigation-store.ts +43 -8
  31. package/src/browser/navigation-transaction.ts +11 -9
  32. package/src/browser/partial-update.ts +94 -17
  33. package/src/browser/prefetch/cache.ts +82 -12
  34. package/src/browser/prefetch/fetch.ts +98 -27
  35. package/src/browser/prefetch/policy.ts +6 -0
  36. package/src/browser/prefetch/queue.ts +92 -20
  37. package/src/browser/prefetch/resource-ready.ts +77 -0
  38. package/src/browser/react/Link.tsx +88 -9
  39. package/src/browser/react/NavigationProvider.tsx +40 -4
  40. package/src/browser/react/context.ts +7 -2
  41. package/src/browser/react/use-handle.ts +9 -58
  42. package/src/browser/react/use-router.ts +21 -8
  43. package/src/browser/rsc-router.tsx +134 -59
  44. package/src/browser/scroll-restoration.ts +41 -42
  45. package/src/browser/segment-reconciler.ts +72 -10
  46. package/src/browser/server-action-bridge.ts +8 -6
  47. package/src/browser/types.ts +55 -5
  48. package/src/build/generate-manifest.ts +6 -6
  49. package/src/build/generate-route-types.ts +3 -0
  50. package/src/build/route-trie.ts +50 -24
  51. package/src/build/route-types/include-resolution.ts +8 -1
  52. package/src/build/route-types/router-processing.ts +223 -74
  53. package/src/build/route-types/scan-filter.ts +8 -1
  54. package/src/cache/cache-runtime.ts +15 -11
  55. package/src/cache/cache-scope.ts +48 -7
  56. package/src/cache/cf/cf-cache-store.ts +453 -11
  57. package/src/cache/cf/index.ts +5 -1
  58. package/src/cache/document-cache.ts +17 -7
  59. package/src/cache/index.ts +1 -0
  60. package/src/cache/taint.ts +55 -0
  61. package/src/client.rsc.tsx +2 -0
  62. package/src/client.tsx +6 -66
  63. package/src/context-var.ts +72 -2
  64. package/src/debug.ts +2 -2
  65. package/src/handle.ts +40 -0
  66. package/src/handles/breadcrumbs.ts +66 -0
  67. package/src/handles/index.ts +1 -0
  68. package/src/index.rsc.ts +6 -36
  69. package/src/index.ts +50 -43
  70. package/src/prerender/store.ts +5 -4
  71. package/src/prerender.ts +138 -77
  72. package/src/reverse.ts +25 -1
  73. package/src/route-definition/dsl-helpers.ts +224 -37
  74. package/src/route-definition/helpers-types.ts +67 -19
  75. package/src/route-definition/index.ts +3 -0
  76. package/src/route-definition/redirect.ts +11 -3
  77. package/src/route-definition/resolve-handler-use.ts +149 -0
  78. package/src/route-map-builder.ts +7 -1
  79. package/src/route-types.ts +11 -0
  80. package/src/router/content-negotiation.ts +100 -1
  81. package/src/router/find-match.ts +4 -2
  82. package/src/router/handler-context.ts +111 -25
  83. package/src/router/intercept-resolution.ts +11 -4
  84. package/src/router/lazy-includes.ts +4 -1
  85. package/src/router/loader-resolution.ts +156 -21
  86. package/src/router/logging.ts +5 -2
  87. package/src/router/manifest.ts +9 -3
  88. package/src/router/match-api.ts +125 -190
  89. package/src/router/match-middleware/background-revalidation.ts +30 -2
  90. package/src/router/match-middleware/cache-lookup.ts +94 -17
  91. package/src/router/match-middleware/cache-store.ts +53 -10
  92. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  93. package/src/router/match-middleware/segment-resolution.ts +61 -5
  94. package/src/router/match-result.ts +104 -10
  95. package/src/router/metrics.ts +6 -1
  96. package/src/router/middleware-types.ts +16 -22
  97. package/src/router/middleware.ts +24 -30
  98. package/src/router/navigation-snapshot.ts +182 -0
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/route-snapshot.ts +245 -0
  103. package/src/router/router-context.ts +6 -1
  104. package/src/router/router-interfaces.ts +36 -4
  105. package/src/router/router-options.ts +37 -11
  106. package/src/router/segment-resolution/fresh.ts +198 -20
  107. package/src/router/segment-resolution/helpers.ts +30 -25
  108. package/src/router/segment-resolution/loader-cache.ts +1 -0
  109. package/src/router/segment-resolution/revalidation.ts +438 -300
  110. package/src/router/segment-wrappers.ts +2 -0
  111. package/src/router/types.ts +1 -0
  112. package/src/router.ts +59 -6
  113. package/src/rsc/handler.ts +472 -372
  114. package/src/rsc/loader-fetch.ts +23 -3
  115. package/src/rsc/manifest-init.ts +5 -1
  116. package/src/rsc/progressive-enhancement.ts +14 -2
  117. package/src/rsc/rsc-rendering.ts +12 -1
  118. package/src/rsc/server-action.ts +8 -0
  119. package/src/rsc/ssr-setup.ts +2 -2
  120. package/src/rsc/types.ts +9 -1
  121. package/src/segment-content-promise.ts +33 -0
  122. package/src/segment-system.tsx +164 -23
  123. package/src/server/context.ts +140 -14
  124. package/src/server/handle-store.ts +19 -0
  125. package/src/server/loader-registry.ts +9 -8
  126. package/src/server/request-context.ts +204 -28
  127. package/src/ssr/index.tsx +4 -0
  128. package/src/static-handler.ts +18 -6
  129. package/src/types/cache-types.ts +4 -4
  130. package/src/types/handler-context.ts +149 -49
  131. package/src/types/loader-types.ts +36 -9
  132. package/src/types/route-entry.ts +8 -1
  133. package/src/types/segments.ts +6 -0
  134. package/src/urls/path-helper-types.ts +39 -6
  135. package/src/urls/path-helper.ts +48 -13
  136. package/src/urls/pattern-types.ts +12 -0
  137. package/src/urls/response-types.ts +16 -6
  138. package/src/use-loader.tsx +77 -5
  139. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  140. package/src/vite/discovery/discover-routers.ts +5 -1
  141. package/src/vite/discovery/prerender-collection.ts +128 -74
  142. package/src/vite/discovery/state.ts +13 -6
  143. package/src/vite/index.ts +4 -0
  144. package/src/vite/plugin-types.ts +51 -79
  145. package/src/vite/plugins/expose-action-id.ts +1 -3
  146. package/src/vite/plugins/expose-id-utils.ts +12 -0
  147. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  148. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  149. package/src/vite/plugins/performance-tracks.ts +88 -0
  150. package/src/vite/plugins/refresh-cmd.ts +88 -26
  151. package/src/vite/plugins/version-plugin.ts +13 -1
  152. package/src/vite/rango.ts +163 -211
  153. package/src/vite/router-discovery.ts +178 -45
  154. package/src/vite/utils/banner.ts +3 -3
  155. package/src/vite/utils/prerender-utils.ts +37 -5
  156. package/src/vite/utils/shared-utils.ts +3 -2
@@ -168,8 +168,19 @@ export async function handleLoaderFetch<TEnv>(
168
168
  loaderResult: unknown;
169
169
  }
170
170
  const loaderPayload: LoaderPayload = { loaderResult: result };
171
- const rscStream =
172
- ctx.renderToReadableStream<LoaderPayload>(loaderPayload);
171
+ const rscStream = ctx.renderToReadableStream<LoaderPayload>(
172
+ loaderPayload,
173
+ {
174
+ onError: (error: unknown) => {
175
+ ctx.callOnError(error, "rendering", {
176
+ request,
177
+ url,
178
+ env,
179
+ loaderName: loaderId,
180
+ });
181
+ },
182
+ },
183
+ );
173
184
 
174
185
  return createResponseWithMergedHeaders(rscStream, {
175
186
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -199,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
199
210
  name: err.name,
200
211
  },
201
212
  };
202
- const rscStream = ctx.renderToReadableStream(errorPayload);
213
+ const rscStream = ctx.renderToReadableStream(errorPayload, {
214
+ onError: (error: unknown) => {
215
+ ctx.callOnError(error, "rendering", {
216
+ request,
217
+ url,
218
+ env,
219
+ loaderName: loaderId,
220
+ });
221
+ },
222
+ });
203
223
 
204
224
  return createResponseWithMergedHeaders(rscStream, {
205
225
  status: 500,
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
31
31
  ): Promise<void> {
32
32
  const { generateManifestFull } =
33
33
  await import("../build/generate-manifest.js");
34
- const generated = generateManifestFull(router.urlpatterns);
34
+ const generated = generateManifestFull(
35
+ router.urlpatterns,
36
+ undefined,
37
+ router.basename ? { urlPrefix: router.basename } : undefined,
38
+ );
35
39
  if (
36
40
  generated._routeAncestry &&
37
41
  Object.keys(generated._routeAncestry).length > 0
@@ -243,6 +243,8 @@ export async function handleProgressiveEnhancement<TEnv>(
243
243
  const payload: RscPayload = {
244
244
  metadata: {
245
245
  pathname: url.pathname,
246
+ routerId: ctx.router.id,
247
+ basename: ctx.router.basename,
246
248
  segments: match.segments,
247
249
  matched: match.matched,
248
250
  diff: match.diff,
@@ -257,7 +259,11 @@ export async function handleProgressiveEnhancement<TEnv>(
257
259
  formState: actionResult,
258
260
  };
259
261
 
260
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
262
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
263
+ onError: (error: unknown) => {
264
+ ctx.callOnError(error, "rendering", { request, url, env });
265
+ },
266
+ });
261
267
  // metricsStore=undefined is safe: the handler already stashed the early
262
268
  // SSR setup promise on request variables, so getSSRSetup returns it
263
269
  // without falling back to a fresh startSSRSetup.
@@ -342,6 +348,8 @@ async function renderPeErrorBoundary<TEnv>(
342
348
  const payload: RscPayload = {
343
349
  metadata: {
344
350
  pathname: url.pathname,
351
+ routerId: ctx.router.id,
352
+ basename: ctx.router.basename,
345
353
  segments: errorResult.segments,
346
354
  matched: errorResult.matched,
347
355
  diff: errorResult.diff,
@@ -356,7 +364,11 @@ async function renderPeErrorBoundary<TEnv>(
356
364
  },
357
365
  };
358
366
 
359
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
367
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
368
+ onError: (error: unknown) => {
369
+ ctx.callOnError(error, "rendering", { request, url, env });
370
+ },
371
+ });
360
372
  // metricsStore=undefined is safe: the handler already stashed the early
361
373
  // SSR setup promise on request variables, so getSSRSetup returns it
362
374
  // without falling back to a fresh startSSRSetup.
@@ -54,6 +54,8 @@ export async function handleRscRendering<TEnv>(
54
54
  payload = {
55
55
  metadata: {
56
56
  pathname: url.pathname,
57
+ routerId: ctx.router.id,
58
+ basename: ctx.router.basename,
57
59
  segments: match.segments,
58
60
  matched: match.matched,
59
61
  diff: match.diff,
@@ -75,6 +77,7 @@ export async function handleRscRendering<TEnv>(
75
77
  payload = {
76
78
  metadata: {
77
79
  pathname: url.pathname,
80
+ routerId: ctx.router.id,
78
81
  segments: result.segments,
79
82
  matched: result.matched,
80
83
  diff: result.diff,
@@ -83,6 +86,7 @@ export async function handleRscRendering<TEnv>(
83
86
  slots: result.slots,
84
87
  handles: handleStore.stream(),
85
88
  version: ctx.version,
89
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
86
90
  },
87
91
  };
88
92
  }
@@ -135,6 +139,8 @@ export async function handleRscRendering<TEnv>(
135
139
 
136
140
  metadata: {
137
141
  pathname: url.pathname,
142
+ routerId: ctx.router.id,
143
+ basename: ctx.router.basename,
138
144
  segments: match.segments,
139
145
  matched: match.matched,
140
146
  diff: match.diff,
@@ -143,6 +149,7 @@ export async function handleRscRendering<TEnv>(
143
149
  rootLayout: ctx.router.rootLayout,
144
150
  handles: handleStore.stream(),
145
151
  version: ctx.version,
152
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
146
153
  themeConfig: ctx.router.themeConfig,
147
154
  initialTheme: reqCtx.theme,
148
155
  },
@@ -166,7 +173,11 @@ export async function handleRscRendering<TEnv>(
166
173
 
167
174
  // Serialize to RSC stream
168
175
  const rscSerializeStart = performance.now();
169
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
176
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
177
+ onError: (error: unknown) => {
178
+ ctx.callOnError(error, "rendering", { request, url, env });
179
+ },
180
+ });
170
181
  const rscSerializeDur = performance.now() - rscSerializeStart;
171
182
  // This measures synchronous stream creation, not end-to-end stream consumption.
172
183
  appendMetric(
@@ -208,6 +208,7 @@ export async function executeServerAction<TEnv>(
208
208
  const payload: RscPayload = {
209
209
  metadata: {
210
210
  pathname: url.pathname,
211
+ routerId: ctx.router.id,
211
212
  segments: errorResult.segments,
212
213
  isPartial: true,
213
214
  matched: errorResult.matched,
@@ -225,6 +226,9 @@ export async function executeServerAction<TEnv>(
225
226
 
226
227
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
227
228
  temporaryReferences,
229
+ onError: (error: unknown) => {
230
+ ctx.callOnError(error, "rendering", { request, url, env });
231
+ },
228
232
  });
229
233
 
230
234
  return createResponseWithMergedHeaders(rscStream, {
@@ -314,6 +318,7 @@ export async function revalidateAfterAction<TEnv>(
314
318
  const payload: RscPayload = {
315
319
  metadata: {
316
320
  pathname: url.pathname,
321
+ routerId: ctx.router.id,
317
322
  segments: matchResult.segments,
318
323
  isPartial: true,
319
324
  matched: matchResult.matched,
@@ -330,6 +335,9 @@ export async function revalidateAfterAction<TEnv>(
330
335
  const renderStart = performance.now();
331
336
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
332
337
  temporaryReferences,
338
+ onError: (error: unknown) => {
339
+ ctx.callOnError(error, "rendering", { request, url, env });
340
+ },
333
341
  });
334
342
  const rscSerializeDur = performance.now() - renderStart;
335
343
  // This measures synchronous stream creation, not end-to-end stream consumption.
@@ -77,7 +77,7 @@ export function getSSRSetup<TEnv>(
77
77
  url: URL,
78
78
  metricsStore: MetricsStore | undefined,
79
79
  ): Promise<SSRSetup> {
80
- const early = _getRequestContext()?.var?.[SSR_SETUP_VAR] as
80
+ const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
81
81
  | Promise<SSRSetup>
82
82
  | undefined;
83
83
  if (early) return early;
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
98
98
  * the isRscRequest decision in rsc-rendering.ts.
99
99
  *
100
100
  * Note: response/mime routes are excluded by the caller — this function
101
- * runs after previewMatch() classifies the route type.
101
+ * runs after classifyRequest() determines the request mode.
102
102
  */
103
103
  export function mayNeedSSR(request: Request, url: URL): boolean {
104
104
  if (
package/src/rsc/types.ts CHANGED
@@ -19,6 +19,9 @@ export interface RscPayload {
19
19
  metadata?: {
20
20
  pathname: string;
21
21
  segments: ResolvedSegment[];
22
+ /** Router instance ID. When this changes between navigations, the client
23
+ * discards cached segments and does a full tree replacement (app switch). */
24
+ routerId?: string;
22
25
  isPartial?: boolean;
23
26
  isError?: boolean;
24
27
  matched?: string[];
@@ -38,6 +41,8 @@ export interface RscPayload {
38
41
  themeConfig?: ResolvedThemeConfig | null;
39
42
  /** Initial theme from cookie (for SSR hydration) */
40
43
  initialTheme?: Theme;
44
+ /** URL prefix for all routes (from createRouter({ basename })). */
45
+ basename?: string;
41
46
  /** Whether connection warmup is enabled */
42
47
  warmupEnabled?: boolean;
43
48
  /** Server-side redirect with optional state (for partial requests) */
@@ -63,7 +68,10 @@ export interface RSCDependencies {
63
68
  */
64
69
  renderToReadableStream: <T>(
65
70
  payload: T,
66
- options?: { temporaryReferences?: unknown },
71
+ options?: {
72
+ temporaryReferences?: unknown;
73
+ onError?: (error: unknown) => string | void;
74
+ },
67
75
  ) => ReadableStream<Uint8Array>;
68
76
 
69
77
  /**
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ResolvedSegment } from "./types.js";
3
+
4
+ /**
5
+ * Return a stable Promise wrapping `component`, memoized on `segment`.
6
+ *
7
+ * A fresh `Promise.resolve(component)` each render would suspend for one
8
+ * microtask and briefly commit the loading fallback inside Suspender — the
9
+ * intercept / parallel-slot flicker this indirection prevents. Reusing the
10
+ * same Promise ref keeps React's `use()` in "known fulfilled" state after
11
+ * the first observation. `component` is separate from `segment.component`
12
+ * so action renders can feed in the awaited value.
13
+ *
14
+ * @internal
15
+ */
16
+ export function getMemoizedContentPromise(
17
+ segment: ResolvedSegment,
18
+ component: ReactNode,
19
+ ): Promise<ReactNode> {
20
+ if (component instanceof Promise) {
21
+ return component as Promise<ReactNode>;
22
+ }
23
+ if (
24
+ segment.contentPromise !== undefined &&
25
+ segment.contentSource === component
26
+ ) {
27
+ return segment.contentPromise;
28
+ }
29
+ const promise = Promise.resolve(component);
30
+ segment.contentPromise = promise;
31
+ segment.contentSource = component;
32
+ return promise;
33
+ }
@@ -2,11 +2,7 @@ import * as React from "react";
2
2
  import { createElement, type ReactNode, type ComponentType } from "react";
3
3
  import { OutletProvider } from "./client.js";
4
4
  import { MountContextProvider } from "./browser/react/mount-context.js";
5
- import type {
6
- ResolvedSegment,
7
- LoaderDataResult,
8
- RootLayoutProps,
9
- } from "./types.js";
5
+ import type { ResolvedSegment, RootLayoutProps } from "./types.js";
10
6
  import { isLoaderDataResult } from "./types.js";
11
7
  import { invariant } from "./errors.js";
12
8
  import {
@@ -14,12 +10,103 @@ import {
14
10
  LoaderBoundary,
15
11
  } from "./route-content-wrapper.js";
16
12
  import { RootErrorBoundary } from "./root-error-boundary.js";
13
+ import { getMemoizedContentPromise } from "./segment-content-promise.js";
17
14
 
18
15
  // ViewTransition is only available in React experimental.
19
16
  // Access via namespace import to avoid compile-time errors on stable React.
20
17
  const ReactViewTransition: any =
21
18
  "ViewTransition" in React ? (React as any).ViewTransition : null;
22
19
 
20
+ function restoreParallelLoaderMarkers(
21
+ segments: ResolvedSegment[],
22
+ ): ResolvedSegment[] {
23
+ const parallelLoadingByNamespace = new Map<string, ReactNode>();
24
+ let nextSegments: ResolvedSegment[] | null = null;
25
+
26
+ for (let i = 0; i < segments.length; i++) {
27
+ const segment = segments[i];
28
+
29
+ if (segment.type === "parallel") {
30
+ if (
31
+ segment.namespace &&
32
+ segment.loading !== undefined &&
33
+ segment.loading !== null &&
34
+ segment.loading !== false
35
+ ) {
36
+ parallelLoadingByNamespace.set(segment.namespace, segment.loading);
37
+ }
38
+ continue;
39
+ }
40
+
41
+ if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
42
+ continue;
43
+ }
44
+
45
+ const parallelLoading = segment.namespace
46
+ ? parallelLoadingByNamespace.get(segment.namespace)
47
+ : undefined;
48
+ if (parallelLoading === undefined) {
49
+ continue;
50
+ }
51
+
52
+ if (!nextSegments) {
53
+ nextSegments = segments.slice();
54
+ }
55
+ nextSegments[i] = { ...segment, parallelLoading };
56
+ }
57
+
58
+ return nextSegments ?? segments;
59
+ }
60
+
61
+ function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
62
+ if (!a || a.length !== b.length) {
63
+ return false;
64
+ }
65
+
66
+ for (let i = 0; i < a.length; i++) {
67
+ if (a[i] !== b[i]) {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ return true;
73
+ }
74
+
75
+ /**
76
+ * Memoize an aggregate Promise.all for a segment's owned loaders on the
77
+ * segment itself. Reusing the same aggregate across renders — invalidated
78
+ * only when an underlying loader.loaderData ref changes — keeps React's
79
+ * use() in "known fulfilled" state and prevents a fresh Promise.all from
80
+ * suspending (and briefly committing the Suspense fallback) on every
81
+ * partial update that doesn't actually change loader data.
82
+ */
83
+ function memoizeLoaderPromise(
84
+ target: ResolvedSegment,
85
+ loaders: ResolvedSegment[],
86
+ sourcesKey: "layoutLoaderSources" | "parallelLoaderSources",
87
+ ): Promise<any[]> | any[] {
88
+ const sources = loaders.map((loader) => loader.loaderData);
89
+ if (
90
+ target.loaderDataPromise !== undefined &&
91
+ hasSameReferences(target[sourcesKey], sources)
92
+ ) {
93
+ return target.loaderDataPromise;
94
+ }
95
+ const promise: Promise<any[]> | any[] =
96
+ loaders.length > 0
97
+ ? Promise.all(
98
+ loaders.map((loader) =>
99
+ loader.loaderData instanceof Promise
100
+ ? loader.loaderData
101
+ : Promise.resolve(loader.loaderData),
102
+ ),
103
+ )
104
+ : Promise.resolve([]);
105
+ target.loaderDataPromise = promise;
106
+ target[sourcesKey] = sources;
107
+ return promise;
108
+ }
109
+
23
110
  /**
24
111
  * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
25
112
  */
@@ -143,6 +230,10 @@ export async function renderSegments(
143
230
  } = options || {};
144
231
 
145
232
  const temporalLazyRefs: Promise<any>[] = [];
233
+ const normalizedSegments = restoreParallelLoaderMarkers(segments);
234
+ const normalizedInterceptSegments = interceptSegments
235
+ ? restoreParallelLoaderMarkers(interceptSegments)
236
+ : undefined;
146
237
 
147
238
  /**
148
239
  * Registers promises from lazy/async components for awaiting.
@@ -167,7 +258,7 @@ export async function renderSegments(
167
258
  );
168
259
  }
169
260
  // Separate segments by type, passing intercept segments for explicit injection
170
- const tree = segmentTreeWalk(segments, interceptSegments);
261
+ const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
171
262
  // Render content segments as siblings
172
263
  let content: ReactNode = null;
173
264
  for (const node of tree) {
@@ -219,10 +310,7 @@ export async function renderSegments(
219
310
  loading !== null && loading !== undefined && loading !== false
220
311
  ? createElement(RouteContentWrapper, {
221
312
  key: `suspense-loading-${id}`,
222
- content:
223
- resolvedComponent instanceof Promise
224
- ? resolvedComponent
225
- : Promise.resolve(resolvedComponent),
313
+ content: getMemoizedContentPromise(node.segment, resolvedComponent),
226
314
  fallback: loading,
227
315
  segmentId: id,
228
316
  })
@@ -246,16 +334,11 @@ export async function renderSegments(
246
334
 
247
335
  // Prepare loader data if there are loaders
248
336
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
249
- const loaderDataPromise =
250
- loaderEntries.length > 0
251
- ? Promise.all(
252
- loaderEntries.map((loader) =>
253
- loader.loaderData instanceof Promise
254
- ? loader.loaderData
255
- : Promise.resolve(loader.loaderData),
256
- ),
257
- )
258
- : Promise.resolve([]);
337
+ const loaderDataPromise = memoizeLoaderPromise(
338
+ node.segment,
339
+ loaderEntries,
340
+ "layoutLoaderSources",
341
+ );
259
342
 
260
343
  // Use LoaderBoundary when loading is defined to maintain consistent tree structure
261
344
  // This ensures cached segments (which may not have loader segments) have the same
@@ -284,13 +367,71 @@ export async function renderSegments(
284
367
  children: nodeContent,
285
368
  });
286
369
  } else {
287
- // Has loaders but no loading skeleton - await loaders and render directly
288
- const resolvedData = await loaderDataPromise;
370
+ // Has loaders but no loading skeleton.
371
+ // Split: parallel-owned loaders stream (their parallel has loading()),
372
+ // layout-owned loaders are awaited (they gate the layout content).
373
+ const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
374
+ const parallelOwnedLoaders = loaderEntries.filter(
375
+ (l) => !!l.parallelLoading,
376
+ );
377
+
378
+ // Await only layout-owned loaders
379
+ const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
380
+ const layoutLoaderDataPromise =
381
+ layoutLoaders.length > 0
382
+ ? Promise.all(
383
+ layoutLoaders.map((l) =>
384
+ l.loaderData instanceof Promise
385
+ ? l.loaderData
386
+ : Promise.resolve(l.loaderData),
387
+ ),
388
+ )
389
+ : Promise.resolve([]);
390
+ const resolvedData = await layoutLoaderDataPromise;
289
391
  const { loaderData, errorFallback } = resolveLoaderData(
290
392
  resolvedData,
291
- loaderIds,
393
+ layoutLoaderIds,
292
394
  );
293
395
 
396
+ // Parallel-owned loaders: attach to their owning parallel segment
397
+ // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
398
+ if (parallelOwnedLoaders.length > 0) {
399
+ const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
400
+
401
+ for (const loader of parallelOwnedLoaders) {
402
+ if (!loader.namespace) {
403
+ continue;
404
+ }
405
+ const existing = loadersByParallelNamespace.get(loader.namespace);
406
+ if (existing) {
407
+ existing.push(loader);
408
+ } else {
409
+ loadersByParallelNamespace.set(loader.namespace, [loader]);
410
+ }
411
+ }
412
+
413
+ for (const p of node.parallel) {
414
+ if (!p.loading || !p.namespace) {
415
+ continue;
416
+ }
417
+
418
+ const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
419
+ if (!ownedLoaders || ownedLoaders.length === 0) {
420
+ continue;
421
+ }
422
+
423
+ p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
424
+ const aggregated = memoizeLoaderPromise(
425
+ p,
426
+ ownedLoaders,
427
+ "parallelLoaderSources",
428
+ );
429
+ if ((forceAwait || isAction) && aggregated instanceof Promise) {
430
+ p.loaderDataPromise = await aggregated;
431
+ }
432
+ }
433
+ }
434
+
294
435
  content = createElement(OutletProvider, {
295
436
  key,
296
437
  content: outletContent,