@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  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 +107 -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 +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -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 +3 -1
  114. package/src/client.tsx +106 -126
  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 +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -105,6 +105,7 @@ import type { ResolvedSegment } from "../../types.js";
105
105
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
106
  import { getRouterContext } from "../router-context.js";
107
107
  import type { GeneratorMiddleware } from "./cache-lookup.js";
108
+ import { debugLog } from "../logging.js";
108
109
 
109
110
  /**
110
111
  * Creates intercept resolution middleware
@@ -117,11 +118,14 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
117
118
  */
118
119
  export function withInterceptResolution<TEnv>(
119
120
  ctx: MatchContext<TEnv>,
120
- state: MatchPipelineState
121
+ state: MatchPipelineState,
121
122
  ): GeneratorMiddleware<ResolvedSegment> {
122
123
  return async function* (
123
- source: AsyncGenerator<ResolvedSegment>
124
+ source: AsyncGenerator<ResolvedSegment>,
124
125
  ): AsyncGenerator<ResolvedSegment> {
126
+ const pipelineStart = performance.now();
127
+ const ms = ctx.metricsStore;
128
+
125
129
  // First, yield all segments from the source (main segment resolution or cache)
126
130
  const segments: ResolvedSegment[] = [];
127
131
  for await (const segment of source) {
@@ -131,6 +135,13 @@ export function withInterceptResolution<TEnv>(
131
135
 
132
136
  // Skip intercept resolution for full match (document requests don't have intercepts)
133
137
  if (ctx.isFullMatch) {
138
+ if (ms) {
139
+ ms.metrics.push({
140
+ label: "pipeline:intercept",
141
+ duration: performance.now() - pipelineStart,
142
+ startTime: pipelineStart - ms.requestStart,
143
+ });
144
+ }
134
145
  return;
135
146
  }
136
147
 
@@ -149,6 +160,13 @@ export function withInterceptResolution<TEnv>(
149
160
  if (ctx.interceptResult && state.cacheHit && ctx.isIntercept) {
150
161
  await handleCacheHitIntercept(ctx, state, segments);
151
162
  }
163
+ if (ms) {
164
+ ms.metrics.push({
165
+ label: "pipeline:intercept",
166
+ duration: performance.now() - pipelineStart,
167
+ startTime: pipelineStart - ms.requestStart,
168
+ });
169
+ }
152
170
  return;
153
171
  }
154
172
 
@@ -156,9 +174,10 @@ export function withInterceptResolution<TEnv>(
156
174
  const { resolveInterceptEntry } = getRouterContext<TEnv>();
157
175
 
158
176
  const slotName = ctx.interceptResult!.intercept.slotName;
159
- console.log(
160
- `[Router.matchPartial] Found intercept for "${ctx.localRouteName}" -> slot "${slotName}"`
161
- );
177
+ debugLog("matchPartial.intercept", "intercept resolved", {
178
+ routeName: ctx.localRouteName,
179
+ slotName,
180
+ });
162
181
 
163
182
  // Resolve intercept entry (middleware, loaders, handler)
164
183
  const Store = ctx.Store;
@@ -178,8 +197,8 @@ export function withInterceptResolution<TEnv>(
178
197
  routeKey: ctx.routeKey,
179
198
  actionContext: ctx.actionContext,
180
199
  stale: ctx.stale,
181
- }
182
- )
200
+ },
201
+ ),
183
202
  );
184
203
 
185
204
  // Update state
@@ -193,6 +212,14 @@ export function withInterceptResolution<TEnv>(
193
212
  for (const segment of interceptSegments) {
194
213
  yield segment;
195
214
  }
215
+
216
+ if (ms) {
217
+ ms.metrics.push({
218
+ label: "pipeline:intercept",
219
+ duration: performance.now() - pipelineStart,
220
+ startTime: pipelineStart - ms.requestStart,
221
+ });
222
+ }
196
223
  };
197
224
  }
198
225
 
@@ -204,7 +231,7 @@ export function withInterceptResolution<TEnv>(
204
231
  async function handleCacheHitIntercept<TEnv>(
205
232
  ctx: MatchContext<TEnv>,
206
233
  state: MatchPipelineState,
207
- segments: ResolvedSegment[]
234
+ segments: ResolvedSegment[],
208
235
  ): Promise<void> {
209
236
  if (!ctx.interceptResult) return;
210
237
 
@@ -214,7 +241,7 @@ async function handleCacheHitIntercept<TEnv>(
214
241
 
215
242
  // Find intercept segments from cached segments (namespace starts with "intercept:")
216
243
  const interceptSegments = segments.filter((s) =>
217
- s.namespace?.startsWith("intercept:")
244
+ s.namespace?.startsWith("intercept:"),
218
245
  );
219
246
  state.interceptSegments = interceptSegments;
220
247
 
@@ -238,25 +265,36 @@ async function handleCacheHitIntercept<TEnv>(
238
265
  routeKey: ctx.routeKey,
239
266
  actionContext: ctx.actionContext,
240
267
  stale: ctx.stale,
241
- }
242
- )
268
+ },
269
+ ),
243
270
  );
244
271
 
245
272
  // Update intercept segment's loaderDataPromise with fresh data
246
273
  if (freshLoaderResult) {
247
274
  const interceptMainSegment = interceptSegments.find(
248
- (s) => s.type === "parallel" && s.slot
275
+ (s) => s.type === "parallel" && s.slot,
249
276
  );
250
277
  if (interceptMainSegment) {
251
- interceptMainSegment.loaderDataPromise = freshLoaderResult.loaderDataPromise;
278
+ interceptMainSegment.loaderDataPromise =
279
+ freshLoaderResult.loaderDataPromise;
252
280
  interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
253
- console.log(
254
- `[Router.matchPartial] Cache HIT + fresh loaders for intercept "${ctx.localRouteName}" -> slot "${slotName}"`
281
+ debugLog(
282
+ "matchPartial.intercept",
283
+ "cache hit with fresh intercept loaders",
284
+ {
285
+ routeName: ctx.localRouteName,
286
+ slotName,
287
+ },
255
288
  );
256
289
  }
257
290
  } else {
258
- console.log(
259
- `[Router.matchPartial] Cache HIT for intercept "${ctx.localRouteName}" -> slot "${slotName}" (no loader revalidation)`
291
+ debugLog(
292
+ "matchPartial.intercept",
293
+ "cache hit without intercept loader revalidation",
294
+ {
295
+ routeName: ctx.localRouteName,
296
+ slotName,
297
+ },
260
298
  );
261
299
  }
262
300
  }
@@ -99,11 +99,14 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
99
99
  */
100
100
  export function withSegmentResolution<TEnv>(
101
101
  ctx: MatchContext<TEnv>,
102
- state: MatchPipelineState
102
+ state: MatchPipelineState,
103
103
  ): GeneratorMiddleware<ResolvedSegment> {
104
104
  return async function* (
105
- source: AsyncGenerator<ResolvedSegment>
105
+ source: AsyncGenerator<ResolvedSegment>,
106
106
  ): AsyncGenerator<ResolvedSegment> {
107
+ const pipelineStart = performance.now();
108
+ const ms = ctx.metricsStore;
109
+
107
110
  // IMPORTANT: Always iterate source first to give cache-lookup a chance
108
111
  // to run and set state.cacheHit. Without this, cache-lookup never executes!
109
112
  for await (const segment of source) {
@@ -112,6 +115,13 @@ export function withSegmentResolution<TEnv>(
112
115
 
113
116
  // If cache hit, segments were already yielded by cache lookup
114
117
  if (state.cacheHit) {
118
+ if (ms) {
119
+ ms.metrics.push({
120
+ label: "pipeline:segment-resolve",
121
+ duration: performance.now() - pipelineStart,
122
+ startTime: pipelineStart - ms.requestStart,
123
+ });
124
+ }
115
125
  return;
116
126
  }
117
127
 
@@ -128,8 +138,8 @@ export function withSegmentResolution<TEnv>(
128
138
  ctx.routeKey,
129
139
  ctx.matched.params,
130
140
  ctx.handlerContext,
131
- ctx.loaderPromises
132
- )
141
+ ctx.loaderPromises,
142
+ ),
133
143
  );
134
144
 
135
145
  // Update state with resolved segments
@@ -157,8 +167,8 @@ export function withSegmentResolution<TEnv>(
157
167
  ctx.actionContext,
158
168
  ctx.interceptResult,
159
169
  ctx.localRouteName,
160
- ctx.pathname
161
- )
170
+ ctx.pathname,
171
+ ),
162
172
  );
163
173
 
164
174
  // Update state with resolved segments
@@ -170,5 +180,13 @@ export function withSegmentResolution<TEnv>(
170
180
  yield segment;
171
181
  }
172
182
  }
183
+
184
+ if (ms) {
185
+ ms.metrics.push({
186
+ label: "pipeline:segment-resolve",
187
+ duration: performance.now() - pipelineStart,
188
+ startTime: pipelineStart - ms.requestStart,
189
+ });
190
+ }
173
191
  };
174
192
  }
@@ -86,19 +86,14 @@
86
86
  * -> output: cached segments + fresh loader data
87
87
  *
88
88
  *
89
- * TWO PIPELINE VARIANTS
90
- * =====================
91
- *
92
- * 1. createMatchPipeline (Full Match)
93
- * - Used for document requests (initial page load)
94
- * - No revalidation logic (no previous state to compare)
95
- * - Simpler segment resolution
96
- *
97
- * 2. createMatchPartialPipeline (Partial Match)
98
- * - Used for client-side navigation
99
- * - Includes revalidation for SWR
100
- * - Compares with previous params/URL
101
- * - Supports intercepts (soft navigation modals)
89
+ * PIPELINE VARIANT
90
+ * ================
91
+ *
92
+ * createMatchPartialPipeline handles both full (document) and partial
93
+ * (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
94
+ * - cache-lookup/store work for both
95
+ * - background-revalidation is a no-op for full matches (no stale state)
96
+ * - intercept-resolution is a no-op for full matches (no previous navigation)
102
97
  */
103
98
  import type { ResolvedSegment } from "../types.js";
104
99
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
@@ -163,7 +158,7 @@ export async function* empty<T>(): AsyncGenerator<T> {
163
158
  */
164
159
  export function createMatchPartialPipeline<TEnv>(
165
160
  ctx: MatchContext<TEnv>,
166
- state: MatchPipelineState
161
+ state: MatchPipelineState,
167
162
  ): AsyncGenerator<ResolvedSegment> {
168
163
  // Build the middleware chain
169
164
  const pipeline = compose<ResolvedSegment>(
@@ -176,39 +171,9 @@ export function createMatchPartialPipeline<TEnv>(
176
171
  // Resolves segments on cache miss
177
172
  withSegmentResolution(ctx, state),
178
173
  // Innermost - checks cache first
179
- withCacheLookup(ctx, state)
174
+ withCacheLookup(ctx, state),
180
175
  );
181
176
 
182
177
  // Start with empty source - cache lookup or segment resolution will produce segments
183
178
  return pipeline(empty());
184
179
  }
185
-
186
- /**
187
- * Create the full match pipeline (simpler, no revalidation)
188
- *
189
- * Used for document requests (initial page load) where we don't need
190
- * revalidation logic since there's no previous state to compare against.
191
- */
192
- export function createMatchPipeline<TEnv>(
193
- ctx: MatchContext<TEnv>,
194
- state: MatchPipelineState
195
- ): AsyncGenerator<ResolvedSegment> {
196
- // For full match, we only need:
197
- // 1. Cache lookup
198
- // 2. Segment resolution (without revalidation)
199
- // 3. Intercept resolution
200
- // 4. Cache store
201
-
202
- // Note: Full match uses different resolution logic (resolveAllSegments instead of
203
- // resolveAllSegmentsWithRevalidation). This will be handled by the segment resolution
204
- // middleware checking ctx.isFullMatch or similar flag.
205
-
206
- const pipeline = compose<ResolvedSegment>(
207
- withCacheStore(ctx, state),
208
- withInterceptResolution(ctx, state),
209
- withSegmentResolution(ctx, state),
210
- withCacheLookup(ctx, state)
211
- );
212
-
213
- return pipeline(empty());
214
- }
@@ -108,13 +108,13 @@
108
108
  */
109
109
  import type { MatchResult, ResolvedSegment } from "../types.js";
110
110
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
- import { generateServerTiming, logMetrics } from "./metrics.js";
111
+ import { debugLog } from "./logging.js";
112
112
 
113
113
  /**
114
114
  * Collect all segments from an async generator
115
115
  */
116
116
  export async function collectSegments(
117
- generator: AsyncGenerator<ResolvedSegment>
117
+ generator: AsyncGenerator<ResolvedSegment>,
118
118
  ): Promise<ResolvedSegment[]> {
119
119
  const segments: ResolvedSegment[] = [];
120
120
  for await (const segment of generator) {
@@ -129,17 +129,30 @@ export async function collectSegments(
129
129
  export function buildMatchResult<TEnv>(
130
130
  allSegments: ResolvedSegment[],
131
131
  ctx: MatchContext<TEnv>,
132
- state: MatchPipelineState
132
+ state: MatchPipelineState,
133
133
  ): MatchResult {
134
- const logPrefix = ctx.isFullMatch ? "[Router.match]" : "[Router.matchPartial]";
134
+ const logPrefix = ctx.isFullMatch
135
+ ? "[Router.match]"
136
+ : "[Router.matchPartial]";
135
137
 
136
138
  let allIds: string[];
137
139
  let segmentsToRender: ResolvedSegment[];
138
140
 
139
141
  if (ctx.isFullMatch) {
140
142
  // Full match (document request) - all segments are rendered
141
- allIds = allSegments.map((s) => s.id);
142
- segmentsToRender = allSegments;
143
+ // Deduplicate by segment ID (defense-in-depth). The primary dedup is in
144
+ // resolveAllSegments, but this guards against any path that bypasses it.
145
+ // include() scopes can produce entries that resolve the same shared layout,
146
+ // and duplicate IDs change the client's React tree depth causing remounts.
147
+ const seen = new Set<string>();
148
+ segmentsToRender = [];
149
+ for (const s of allSegments) {
150
+ if (!seen.has(s.id)) {
151
+ seen.add(s.id);
152
+ segmentsToRender.push(s);
153
+ }
154
+ }
155
+ allIds = segmentsToRender.map((s) => s.id);
143
156
  } else {
144
157
  // Partial match (navigation) - filter and handle intercepts
145
158
  // When intercepting, tell browser to keep its current segments + add modal
@@ -151,32 +164,26 @@ export function buildMatchResult<TEnv>(
151
164
  : allSegments.map((s) => s.id) // Use actual segments, not matchedIds
152
165
  : [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
153
166
 
167
+ // Deduplicate allIds (defense-in-depth for partial match path)
168
+ allIds = [...new Set(allIds)];
169
+
154
170
  // Filter out segments with null components (client already has them)
155
171
  // BUT always include loader segments - they carry data even with null component
156
172
  segmentsToRender = allSegments.filter(
157
- (s) => s.component !== null || s.type === "loader"
158
- );
159
- }
160
-
161
- if (process.env.NODE_ENV === "development") {
162
- console.log(
163
- `${logPrefix} All segments:`,
164
- allSegments
165
- .map((s) => `${s.id}(${s.type}, component=${s.component !== null})`)
166
- .join(", ")
167
- );
168
- console.log(
169
- `${logPrefix} Segments to render:`,
170
- segmentsToRender.map((s) => s.id).join(", ")
173
+ (s) => s.component !== null || s.type === "loader",
171
174
  );
172
175
  }
173
176
 
174
- // Output metrics if enabled
175
- let serverTiming: string | undefined;
176
- if (ctx.metricsStore) {
177
- logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
178
- serverTiming = generateServerTiming(ctx.metricsStore);
179
- }
177
+ debugLog(logPrefix, "all segments", {
178
+ segments: allSegments.map((s) => ({
179
+ id: s.id,
180
+ type: s.type,
181
+ hasComponent: s.component !== null,
182
+ })),
183
+ });
184
+ debugLog(logPrefix, "segments to render", {
185
+ segmentIds: segmentsToRender.map((s) => s.id),
186
+ });
180
187
 
181
188
  return {
182
189
  segments: segmentsToRender,
@@ -184,7 +191,6 @@ export function buildMatchResult<TEnv>(
184
191
  diff: segmentsToRender.map((s) => s.id),
185
192
  params: ctx.matched.params,
186
193
  routeName: ctx.routeKey,
187
- serverTiming,
188
194
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
189
195
  routeMiddleware:
190
196
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
@@ -200,7 +206,7 @@ export function buildMatchResult<TEnv>(
200
206
  export async function collectMatchResult<TEnv>(
201
207
  pipeline: AsyncGenerator<ResolvedSegment>,
202
208
  ctx: MatchContext<TEnv>,
203
- state: MatchPipelineState
209
+ state: MatchPipelineState,
204
210
  ): Promise<MatchResult> {
205
211
  const allSegments = await collectSegments(pipeline);
206
212