@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,216 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+ import {
3
+ mergeSegmentLoaders,
4
+ needsLoaderMerge,
5
+ insertMissingDiffSegments,
6
+ } from "./merge-segment-loaders.js";
7
+ import { assertSegmentStructure } from "./segment-structure-assert.js";
8
+ import { splitInterceptSegments } from "./intercept-utils.js";
9
+
10
+ /**
11
+ * Determines the merging behavior for segment reconciliation.
12
+ *
13
+ * - 'action': From server-action-bridge's own merge. Always merges loaders,
14
+ * always preserves cached loading (even undefined), never clears cached
15
+ * segment loading.
16
+ * - 'navigation': From partial-update during normal navigation. Does NOT merge
17
+ * loaders, preserves cached loading only when defined, clears truthy loading
18
+ * on cached segments not in server diff.
19
+ * - 'stale-revalidation': From partial-update during stale revalidation or
20
+ * action-triggered refetch. Merges loaders, always preserves cached loading
21
+ * (same as action), clears truthy loading on cached segments not in server diff.
22
+ */
23
+ export type ReconcileActor = "navigation" | "action" | "stale-revalidation";
24
+
25
+ export interface ReconcileInput {
26
+ actor: ReconcileActor;
27
+ /** All segment IDs the server expects the client to have (matched array) */
28
+ matched: string[];
29
+ /** Segment IDs that changed (diff array) */
30
+ diff: string[];
31
+ /** Segments returned from server (raw array, keyed internally by ID) */
32
+ serverSegments: ResolvedSegment[];
33
+ /** Cached segments from current page (raw array, keyed internally by ID) */
34
+ cachedSegments: ResolvedSegment[];
35
+ /** When true, diff segments not in matched are inserted after their parent
36
+ * layout. Used during navigation when consolidation fetch returns loader
37
+ * segments that aren't in the matched array. */
38
+ insertMissingDiff?: boolean;
39
+ }
40
+
41
+ export interface ReconcileResult {
42
+ /** All merged segments in matched order (for caching and committing) */
43
+ segments: ResolvedSegment[];
44
+ /** Main segments excluding intercepts (for rendering) */
45
+ mainSegments: ResolvedSegment[];
46
+ /** Intercept segments only (passed via render options) */
47
+ interceptSegments: ResolvedSegment[];
48
+ }
49
+
50
+ /**
51
+ * Single source of truth for merging server segments with cached segments.
52
+ *
53
+ * Replaces the duplicated merge loops in server-action-bridge.ts and
54
+ * partial-update.ts. The actor parameter controls the subtle behavioral
55
+ * differences between action and navigation merging:
56
+ *
57
+ * Loading preservation:
58
+ * - action/stale-revalidation: Always preserves cached loading value when it
59
+ * differs from server (even when cached is undefined). This prevents tree
60
+ * structure changes that would remount components and destroy useActionState
61
+ * during action revalidation or action-triggered refetch.
62
+ * - navigation: Preserves cached loading only when the cached value is defined
63
+ * (not undefined). When cached is undefined, lets server value through
64
+ * because we're building a new tree.
65
+ *
66
+ * Loader merging:
67
+ * - action/stale-revalidation: Merges partial loader data when server returns
68
+ * fewer loaders than cached (revalidation only updated some loaders).
69
+ * - navigation: Does not merge (full navigation fetches complete data).
70
+ *
71
+ * Cached segment handling (segments in matched but not in server response):
72
+ * - action: Returns cached segment as-is (preserve tree structure).
73
+ * - navigation/stale-revalidation: Clears truthy loading to undefined
74
+ * (prevents showing stale skeletons), but preserves loading=false
75
+ * (suppressed boundary is structural).
76
+ */
77
+ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
78
+ const { actor, matched, diff, insertMissingDiff } = input;
79
+ const shouldMergeLoaders = actor !== "navigation";
80
+ const context = actor === "action" ? "action-bridge" : "partial-update";
81
+
82
+ // Build lookup maps from arrays
83
+ const serverSegments = new Map<string, ResolvedSegment>();
84
+ input.serverSegments.forEach((s) => serverSegments.set(s.id, s));
85
+ const cachedSegments = new Map<string, ResolvedSegment>();
86
+ input.cachedSegments.forEach((s) => cachedSegments.set(s.id, s));
87
+
88
+ const segments = matched
89
+ .map((segId: string) => {
90
+ const fromServer = serverSegments.get(segId);
91
+ const fromCache = cachedSegments.get(segId);
92
+
93
+ if (fromServer) {
94
+ // Merge partial loader data when server returns fewer loaders than cached
95
+ if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
96
+ return mergeSegmentLoaders(fromServer, fromCache);
97
+ }
98
+
99
+ // Preserve cached structural properties to maintain consistent React tree.
100
+ // Changing these between renders alters the element nesting
101
+ // (with/without RouteContentWrapper, MountContextProvider, etc.),
102
+ // causing React to remount components and destroy useActionState.
103
+ if (fromCache) {
104
+ let merged = fromServer;
105
+
106
+ // When server returns component: null for a layout segment, it means
107
+ // "this segment doesn't need re-rendering" - preserve the cached component
108
+ // to maintain the outlet chain and prevent React tree changes
109
+ if (
110
+ fromServer.component === null &&
111
+ fromServer.type === "layout" &&
112
+ fromCache.component != null
113
+ ) {
114
+ merged = { ...merged, component: fromCache.component };
115
+ }
116
+
117
+ // Loading preservation is actor-aware:
118
+ // - action/stale-revalidation: always preserve cached value to prevent
119
+ // tree remount (even when cached is undefined, to avoid adding a
120
+ // Suspense boundary that wasn't there before)
121
+ // - navigation: only when cached is defined (building a new tree)
122
+ if (actor !== "navigation") {
123
+ if (fromServer.loading !== fromCache.loading) {
124
+ merged = { ...merged, loading: fromCache.loading };
125
+ }
126
+ } else {
127
+ if (
128
+ fromCache.loading !== undefined &&
129
+ fromServer.loading !== fromCache.loading
130
+ ) {
131
+ merged = { ...merged, loading: fromCache.loading };
132
+ }
133
+ }
134
+
135
+ // mountPath: SSR segments may lack mountPath while revalidated segments
136
+ // include it. The conditional MountContextProvider wrapper changes tree depth.
137
+ if (fromServer.mountPath !== fromCache.mountPath) {
138
+ merged = { ...merged, mountPath: fromCache.mountPath };
139
+ }
140
+
141
+ // Dev-mode assertion: warn if the merged result still differs from cache
142
+ // in tree-structural properties. This catches bugs where the merge code
143
+ // above fails to preserve a value it should have.
144
+ assertSegmentStructure(fromCache, merged, context);
145
+
146
+ return merged;
147
+ }
148
+ return fromServer;
149
+ }
150
+
151
+ // Fall back to cached segment (server expects client to already have it)
152
+ if (!fromCache) {
153
+ if (actor === "action") {
154
+ console.error(`[Browser] MISSING SEGMENT: ${segId} not in cache!`);
155
+ } else {
156
+ console.warn(`[Browser] Missing segment: ${segId}`);
157
+ }
158
+ return fromCache;
159
+ }
160
+
161
+ // For non-action actors: cached segments the server decided not to re-render.
162
+ // - Preserve loading=false (suppressed boundary) to maintain tree structure
163
+ // - Clear truthy loading (active skeleton) to prevent suspense on cached content
164
+ if (actor !== "action") {
165
+ if (fromCache.loading !== undefined && fromCache.loading !== false) {
166
+ return { ...fromCache, loading: undefined };
167
+ }
168
+ }
169
+
170
+ return fromCache;
171
+ })
172
+ .filter(Boolean) as ResolvedSegment[];
173
+
174
+ // Insert diff segments not in matched (e.g., loader segments from consolidation fetch).
175
+ // Only needed during navigation - action bridge doesn't use this.
176
+ if (insertMissingDiff) {
177
+ const matchedIdSet = new Set(matched);
178
+ insertMissingDiffSegments(segments, diff, matchedIdSet, serverSegments);
179
+ }
180
+
181
+ const { main, intercept } = splitInterceptSegments(segments);
182
+
183
+ return {
184
+ segments,
185
+ mainSegments: main,
186
+ interceptSegments: intercept,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Reconcile error segments with cached segments.
192
+ *
193
+ * For error responses, the server returns the error boundary segment.
194
+ * This function overlays error segments onto the full cached tree,
195
+ * preserving sibling layouts that aren't in the error parent chain.
196
+ */
197
+ export function reconcileErrorSegments(
198
+ cachedSegments: ResolvedSegment[],
199
+ errorSegments: ResolvedSegment[],
200
+ ): ReconcileResult {
201
+ const errorMap = new Map<string, ResolvedSegment>();
202
+ errorSegments.forEach((s) => errorMap.set(s.id, s));
203
+
204
+ const segments = cachedSegments.map((cached) => {
205
+ const fromServer = errorMap.get(cached.id);
206
+ return fromServer || cached;
207
+ });
208
+
209
+ const { main, intercept } = splitInterceptSegments(segments);
210
+
211
+ return {
212
+ segments,
213
+ mainSegments: main,
214
+ interceptSegments: intercept,
215
+ };
216
+ }
@@ -0,0 +1,83 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+
3
+ /**
4
+ * Tree structure categories for the `loading` property.
5
+ *
6
+ * The `loading` value on a segment determines the React element tree
7
+ * that renderSegments() produces. Different categories produce different
8
+ * element nesting, which causes React to remount components when the
9
+ * category changes between renders (SSR -> navigation -> action).
10
+ *
11
+ * "none" -> OutletProvider (no boundary wrapping)
12
+ * "suppressed" -> LoaderBoundary > Suspense > LoaderResolver > OutletProvider
13
+ * (boundary present, but no RouteContentWrapper for children)
14
+ * "active" -> LoaderBoundary > Suspense > LoaderResolver > OutletProvider
15
+ * > RouteContentWrapper > Suspense > Suspender > component
16
+ */
17
+ type LoadingCategory = "none" | "suppressed" | "active";
18
+
19
+ function getLoadingCategory(loading: unknown): LoadingCategory {
20
+ if (loading === undefined || loading === null) return "none";
21
+ if (loading === false) return "suppressed";
22
+ return "active";
23
+ }
24
+
25
+ /**
26
+ * Assert that merging a server segment with a cached segment won't change
27
+ * the React tree structure. Logs a warning in development when a mismatch
28
+ * is detected.
29
+ *
30
+ * This catches the class of bugs where `loading()` with `{ ssr: false }`
31
+ * produces `loading=false` on SSR but `loading=<skeleton>` on actions,
32
+ * causing React to remount LoaderBoundary/RouteContentWrapper and destroy
33
+ * client state (useActionState, refs, etc.).
34
+ *
35
+ * @param cached - The currently cached segment
36
+ * @param incoming - The new segment from server
37
+ * @param context - Where this merge is happening (for the warning message)
38
+ */
39
+ export function assertSegmentStructure(
40
+ cached: ResolvedSegment,
41
+ incoming: ResolvedSegment,
42
+ context: string,
43
+ ): void {
44
+ if (process.env.NODE_ENV === "production") return;
45
+
46
+ const cachedCategory = getLoadingCategory(cached.loading);
47
+ const incomingCategory = getLoadingCategory(incoming.loading);
48
+
49
+ if (cachedCategory !== incomingCategory) {
50
+ console.warn(
51
+ `[RSC Router] Tree structure mismatch detected in ${context} ` +
52
+ `for segment "${cached.id}": loading category changed from ` +
53
+ `"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
54
+ `"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
55
+ `This will cause React to remount the component, destroying ` +
56
+ `useActionState and other client state. ` +
57
+ `The merge code should preserve the cached loading value.`,
58
+ );
59
+ }
60
+
61
+ // Check mountPath consistency. MountContextProvider is conditionally added
62
+ // in renderSegments() when mountPath is truthy, changing tree depth.
63
+ const cachedHasMount = !!cached.mountPath;
64
+ const incomingHasMount = !!incoming.mountPath;
65
+ if (cachedHasMount !== incomingHasMount) {
66
+ console.warn(
67
+ `[RSC Router] MountContextProvider mismatch detected in ${context} ` +
68
+ `for segment "${cached.id}": mountPath changed from ` +
69
+ `${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
70
+ `${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
71
+ `This will cause React to remount the component, destroying ` +
72
+ `useActionState and other client state. ` +
73
+ `The merge code should preserve the cached mountPath value.`,
74
+ );
75
+ }
76
+ }
77
+
78
+ function describeLoading(loading: unknown): string {
79
+ if (loading === undefined) return "undefined";
80
+ if (loading === null) return "null";
81
+ if (loading === false) return "false";
82
+ return "ReactNode";
83
+ }