@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.141

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +293 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2508 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +24 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-snapshot.ts +368 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +222 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1113 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +173 -35
  205. package/src/index.ts +241 -73
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +527 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +897 -0
  313. package/src/rsc/shell-serve.ts +124 -0
  314. package/src/rsc/ssr-setup.ts +144 -0
  315. package/src/rsc/transition-gate.ts +89 -0
  316. package/src/rsc/types.ts +95 -12
  317. package/src/runtime-env.ts +18 -0
  318. package/src/search-params.ts +99 -82
  319. package/src/segment-content-promise.ts +67 -0
  320. package/src/segment-loader-promise.ts +149 -0
  321. package/src/segment-system.tsx +349 -134
  322. package/src/serialize.ts +243 -0
  323. package/src/server/context.ts +459 -85
  324. package/src/server/cookie-parse.ts +32 -0
  325. package/src/server/cookie-store.ts +310 -0
  326. package/src/server/fetchable-loader-store.ts +11 -6
  327. package/src/server/handle-store.ts +123 -42
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +848 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +443 -135
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +76 -98
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +44 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +280 -0
  389. package/src/urls/pattern-types.ts +160 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -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";
@@ -111,16 +106,6 @@ import {
111
106
  withSegmentResolution,
112
107
  } from "./match-middleware/index.js";
113
108
 
114
- /**
115
- * Compose multiple async generator middleware into a single middleware
116
- *
117
- * Middleware are applied in reverse order (rightmost runs first, innermost).
118
- * For the pipeline:
119
- * compose(A, B, C)(source)
120
- *
121
- * The flow is: source -> C -> B -> A -> output
122
- * Where C is the innermost (runs first on input) and A is outermost (runs last).
123
- */
124
109
  export function compose<T>(
125
110
  ...middleware: GeneratorMiddleware<T>[]
126
111
  ): GeneratorMiddleware<T> {
@@ -131,83 +116,22 @@ export function compose<T>(
131
116
  return middleware[0];
132
117
  }
133
118
  return (source) => {
134
- // Apply middleware in reverse order (rightmost first)
135
119
  return middleware.reduceRight((prev, fn) => fn(prev), source);
136
120
  };
137
121
  }
138
122
 
139
- /**
140
- * Create an empty async generator (source for pipeline)
141
- */
142
- export async function* empty<T>(): AsyncGenerator<T> {
143
- // Yields nothing - used as the initial source for the pipeline
144
- }
123
+ export async function* empty<T>(): AsyncGenerator<T> {}
145
124
 
146
- /**
147
- * Create the match partial pipeline
148
- *
149
- * Pipeline order (innermost to outermost):
150
- * 1. cache-lookup - Check cache first, yield cached segments if hit
151
- * 2. segment-resolution - Resolve segments if cache miss
152
- * 3. intercept-resolution - Resolve intercept segments
153
- * 4. cache-store - Store segments in cache
154
- * 5. background-revalidation - Trigger SWR if cache was stale
155
- *
156
- * Data flow:
157
- * - empty() produces no segments
158
- * - cache-lookup either yields cached segments OR passes through to segment-resolution
159
- * - segment-resolution resolves fresh segments on cache miss
160
- * - intercept-resolution adds intercept segments
161
- * - cache-store observes and caches segments
162
- * - background-revalidation triggers SWR revalidation if needed
163
- */
164
125
  export function createMatchPartialPipeline<TEnv>(
165
126
  ctx: MatchContext<TEnv>,
166
- state: MatchPipelineState
127
+ state: MatchPipelineState,
167
128
  ): AsyncGenerator<ResolvedSegment> {
168
- // Build the middleware chain
169
129
  const pipeline = compose<ResolvedSegment>(
170
- // Outermost - observes segments and triggers background revalidation
171
130
  withBackgroundRevalidation(ctx, state),
172
- // Observes and stores segments in cache
173
- withCacheStore(ctx, state),
174
- // Adds intercept segments after main segments
175
- withInterceptResolution(ctx, state),
176
- // Resolves segments on cache miss
177
- withSegmentResolution(ctx, state),
178
- // Innermost - checks cache first
179
- withCacheLookup(ctx, state)
180
- );
181
-
182
- // Start with empty source - cache lookup or segment resolution will produce segments
183
- return pipeline(empty());
184
- }
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
131
  withCacheStore(ctx, state),
208
132
  withInterceptResolution(ctx, state),
209
133
  withSegmentResolution(ctx, state),
210
- withCacheLookup(ctx, state)
134
+ withCacheLookup(ctx, state),
211
135
  );
212
136
 
213
137
  return pipeline(empty());
@@ -67,10 +67,11 @@
67
67
  * Keep if:
68
68
  * - component !== null (needs rendering)
69
69
  * - type === "loader" (carries data even with null component)
70
+ * - client doesn't have the segment (structurally required parent node)
70
71
  *
71
72
  * Skip if:
72
- * - component === null AND type !== "loader"
73
- * - (Client already has this segment's UI)
73
+ * - component === null AND type !== "loader" AND client has it cached
74
+ * - (Revalidation skip — client already has this segment's UI)
74
75
  *
75
76
  *
76
77
  * INTERCEPT HANDLING
@@ -108,14 +109,11 @@
108
109
  */
109
110
  import type { MatchResult, ResolvedSegment } from "../types.js";
110
111
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
- import { generateServerTiming, logMetrics } from "./metrics.js";
112
112
  import { debugLog } from "./logging.js";
113
+ import { appendMetric } from "./metrics.js";
113
114
 
114
- /**
115
- * Collect all segments from an async generator
116
- */
117
115
  export async function collectSegments(
118
- generator: AsyncGenerator<ResolvedSegment>
116
+ generator: AsyncGenerator<ResolvedSegment>,
119
117
  ): Promise<ResolvedSegment[]> {
120
118
  const segments: ResolvedSegment[] = [];
121
119
  for await (const segment of generator) {
@@ -125,88 +123,161 @@ export async function collectSegments(
125
123
  }
126
124
 
127
125
  /**
128
- * Build the final MatchResult from collected segments and context
126
+ * Deduplicate inherited loader segments by loaderId.
127
+ *
128
+ * When a route has loaders and a child layout has parallel slots, the same
129
+ * loader is resolved twice: once for the route and once inherited into the
130
+ * layout (tagged with `_inherited`). The inherited copy is only needed when
131
+ * the route uses `loading()` — in that case, the loader data is inside a
132
+ * LoaderBoundary/Suspense that parallel slots can't reach through. Without
133
+ * loading(), useLoader() traverses parent contexts and finds the data.
129
134
  */
135
+ function deduplicateLoaderSegments(
136
+ segments: ResolvedSegment[],
137
+ logPrefix: string,
138
+ ): { segments: ResolvedSegment[]; removedIds: Set<string> } {
139
+ // Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
140
+ // namespace, and namespaces of segments that declare loading().
141
+ const originalLoaders = new Set<string>();
142
+ const loaderIdsByNamespace = new Map<string, string[]>();
143
+ const namespacesWithLoading = new Set<string>();
144
+ for (const s of segments) {
145
+ if (s.type === "loader" && s.loaderId) {
146
+ if (!s._inherited) originalLoaders.add(s.loaderId);
147
+ const ids = loaderIdsByNamespace.get(s.namespace);
148
+ if (ids) ids.push(s.loaderId);
149
+ else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
150
+ } else if (
151
+ s.type !== "loader" &&
152
+ s.loading !== undefined &&
153
+ s.loading !== false
154
+ ) {
155
+ namespacesWithLoading.add(s.namespace);
156
+ }
157
+ }
158
+
159
+ const loadersWithLoading = new Set<string>();
160
+ for (const ns of namespacesWithLoading) {
161
+ for (const id of loaderIdsByNamespace.get(ns) ?? []) {
162
+ loadersWithLoading.add(id);
163
+ }
164
+ }
165
+
166
+ const result: ResolvedSegment[] = [];
167
+ const removedIds = new Set<string>();
168
+
169
+ for (const s of segments) {
170
+ if (
171
+ s.type === "loader" &&
172
+ s.loaderId &&
173
+ s._inherited &&
174
+ originalLoaders.has(s.loaderId) &&
175
+ !loadersWithLoading.has(s.loaderId)
176
+ ) {
177
+ removedIds.add(s.id);
178
+ continue;
179
+ }
180
+ result.push(s);
181
+ }
182
+
183
+ if (removedIds.size > 0) {
184
+ debugLog(
185
+ logPrefix,
186
+ `deduped ${removedIds.size} inherited loader segment(s)`,
187
+ );
188
+ }
189
+
190
+ return { segments: result, removedIds };
191
+ }
192
+
130
193
  export function buildMatchResult<TEnv>(
131
194
  allSegments: ResolvedSegment[],
132
195
  ctx: MatchContext<TEnv>,
133
- state: MatchPipelineState
196
+ state: MatchPipelineState,
134
197
  ): MatchResult {
135
- const logPrefix = ctx.isFullMatch ? "[Router.match]" : "[Router.matchPartial]";
198
+ const logPrefix = ctx.isFullMatch
199
+ ? "[Router.match]"
200
+ : "[Router.matchPartial]";
136
201
 
137
202
  let allIds: string[];
138
203
  let segmentsToRender: ResolvedSegment[];
139
204
 
140
205
  if (ctx.isFullMatch) {
141
- // Full match (document request) - all segments are rendered
142
- allIds = allSegments.map((s) => s.id);
143
- segmentsToRender = allSegments;
206
+ const seen = new Set<string>();
207
+ segmentsToRender = [];
208
+ for (const s of allSegments) {
209
+ if (!seen.has(s.id)) {
210
+ seen.add(s.id);
211
+ segmentsToRender.push(s);
212
+ }
213
+ }
214
+ allIds = segmentsToRender.map((s) => s.id);
144
215
  } else {
145
- // Partial match (navigation) - filter and handle intercepts
146
- // When intercepting, tell browser to keep its current segments + add modal
147
- // This prevents the browser from discarding the current page content
148
- // If client sent empty segments (HMR recovery), use segment IDs from allSegments
149
216
  allIds = ctx.interceptResult
150
217
  ? ctx.clientSegmentIds.length > 0
151
218
  ? [...ctx.clientSegmentIds, ...state.interceptSegments.map((s) => s.id)]
152
- : allSegments.map((s) => s.id) // Use actual segments, not matchedIds
219
+ : allSegments.map((s) => s.id)
153
220
  : [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
154
221
 
155
- // Filter out segments with null components (client already has them)
156
- // BUT always include loader segments - they carry data even with null component
222
+ allIds = [...new Set(allIds)];
223
+
224
+ const clientIdSet = new Set(ctx.clientSegmentIds);
157
225
  segmentsToRender = allSegments.filter(
158
- (s) => s.component !== null || s.type === "loader"
226
+ (s) =>
227
+ s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
159
228
  );
160
229
  }
161
230
 
162
- debugLog(logPrefix, "all segments", {
163
- segments: allSegments.map((s) => ({
164
- id: s.id,
165
- type: s.type,
166
- hasComponent: s.component !== null,
167
- })),
168
- });
169
- debugLog(logPrefix, "segments to render", {
170
- segmentIds: segmentsToRender.map((s) => s.id),
171
- });
231
+ const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
232
+ segmentsToRender,
233
+ logPrefix,
234
+ );
172
235
 
173
- // Output metrics if enabled
174
- let serverTiming: string | undefined;
175
- if (ctx.metricsStore) {
176
- logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
177
- serverTiming = generateServerTiming(ctx.metricsStore);
178
- }
236
+ const matchedIds =
237
+ removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
238
+
239
+ const resolvedIds = ctx.isFullMatch
240
+ ? allSegments.map((s) => s.id)
241
+ : allSegments.filter((s) => s._handlerRan).map((s) => s.id);
242
+
243
+ const cleanedSegments = dedupedSegments.map((s) => {
244
+ if (s._handlerRan === undefined) return s;
245
+ const { _handlerRan: _drop, ...rest } = s;
246
+ return rest as ResolvedSegment;
247
+ });
179
248
 
180
249
  return {
181
- segments: segmentsToRender,
182
- matched: allIds,
183
- diff: segmentsToRender.map((s) => s.id),
250
+ segments: cleanedSegments,
251
+ matched: matchedIds,
252
+ diff: cleanedSegments.map((s) => s.id),
253
+ resolvedIds,
184
254
  params: ctx.matched.params,
185
255
  routeName: ctx.routeKey,
186
- serverTiming,
187
256
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
188
257
  routeMiddleware:
189
258
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
190
259
  };
191
260
  }
192
261
 
193
- /**
194
- * Collect segments from pipeline and build MatchResult
195
- *
196
- * This is the main entry point for building the final result after
197
- * the pipeline has processed all segments.
198
- */
199
262
  export async function collectMatchResult<TEnv>(
200
263
  pipeline: AsyncGenerator<ResolvedSegment>,
201
264
  ctx: MatchContext<TEnv>,
202
- state: MatchPipelineState
265
+ state: MatchPipelineState,
203
266
  ): Promise<MatchResult> {
204
267
  const allSegments = await collectSegments(pipeline);
205
268
 
206
- // Update state with collected segments if not already set
269
+ const buildStart = performance.now();
270
+
207
271
  if (state.segments.length === 0) {
208
272
  state.segments = allSegments;
209
273
  }
210
274
 
211
- return buildMatchResult(allSegments, ctx, state);
275
+ const result = buildMatchResult(allSegments, ctx, state);
276
+ appendMetric(
277
+ ctx.metricsStore,
278
+ "collect-result",
279
+ buildStart,
280
+ performance.now() - buildStart,
281
+ );
282
+ return result;
212
283
  }
@@ -1,61 +1,252 @@
1
- /**
2
- * Router Metrics Utilities
3
- *
4
- * Performance metrics collection and reporting for RSC Router.
5
- */
1
+ import type { MetricsStore, PerformanceMetric } from "../server/context";
6
2
 
7
- import type { MetricsStore } from "../server/context";
3
+ const BASE_INDENT = 2;
4
+ const DEPTH_INDENT = 2;
5
+ const TIMELINE_WIDTH = 40;
6
+
7
+ function formatMs(value: number): string {
8
+ return `${value.toFixed(2)}ms`;
9
+ }
10
+
11
+ function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
12
+ return [...metrics].sort((a, b) => {
13
+ if (a.label === "handler:total") return 1;
14
+ if (b.label === "handler:total") return -1;
15
+ return a.startTime - b.startTime;
16
+ });
17
+ }
18
+
19
+ interface Span {
20
+ startTime: number;
21
+ duration: number;
22
+ }
23
+
24
+ function renderTimeline(spans: Span[], total: number): string {
25
+ if (TIMELINE_WIDTH <= 0) {
26
+ return "||";
27
+ }
28
+
29
+ const cells = Array(TIMELINE_WIDTH).fill(".");
30
+
31
+ if (!(total > 0)) {
32
+ cells[0] = "#";
33
+ return `|${cells.join("")}|`;
34
+ }
35
+
36
+ for (const span of spans) {
37
+ const start = Math.max(0, span.startTime);
38
+ const end = Math.max(start, span.startTime + span.duration);
39
+ const startColumn = Math.min(
40
+ TIMELINE_WIDTH - 1,
41
+ Math.floor((start / total) * TIMELINE_WIDTH),
42
+ );
43
+ const endColumn = Math.max(
44
+ startColumn + 1,
45
+ Math.min(
46
+ TIMELINE_WIDTH,
47
+ Math.ceil((Math.min(total, end) / total) * TIMELINE_WIDTH),
48
+ ),
49
+ );
50
+
51
+ cells.fill("#", startColumn, endColumn);
52
+ }
53
+
54
+ return `|${cells.join("")}|`;
55
+ }
56
+
57
+ function createTimelineAxis(total: number): string {
58
+ const totalLabel = formatMs(total);
59
+ return `0ms${" ".repeat(
60
+ Math.max(1, TIMELINE_WIDTH - "0ms".length - totalLabel.length),
61
+ )}${totalLabel}`;
62
+ }
8
63
 
9
- /**
10
- * Create a metrics store for the request if debugPerformance is enabled
11
- */
12
64
  export function createMetricsStore(
13
- debugPerformance: boolean
65
+ debugPerformance: boolean,
66
+ requestStart?: number,
14
67
  ): MetricsStore | undefined {
15
68
  if (!debugPerformance) return undefined;
16
69
  return {
17
70
  enabled: true,
18
- requestStart: performance.now(),
71
+ requestStart: requestStart ?? performance.now(),
19
72
  metrics: [],
20
73
  };
21
74
  }
22
75
 
23
- /**
24
- * Log metrics to console in a formatted way
25
- */
76
+ export function appendMetric(
77
+ metricsStore: MetricsStore | undefined,
78
+ label: string,
79
+ start: number,
80
+ duration: number,
81
+ depth?: number,
82
+ ): void {
83
+ if (!metricsStore) return;
84
+ metricsStore.metrics.push({
85
+ label,
86
+ duration,
87
+ startTime: start - metricsStore.requestStart,
88
+ depth,
89
+ });
90
+ }
91
+
92
+ export function buildMetricsTiming(
93
+ method: string,
94
+ pathname: string,
95
+ metricsStore: MetricsStore | undefined,
96
+ ): string | undefined {
97
+ if (!metricsStore) return undefined;
98
+ logMetrics(method, pathname, metricsStore);
99
+ return generateServerTiming(metricsStore) || undefined;
100
+ }
101
+
102
+ interface DisplayRow {
103
+ label: string;
104
+ startTime: number;
105
+ duration: number;
106
+ depth: number | undefined;
107
+ spans: Span[];
108
+ }
109
+
110
+ function buildDisplayRows(sorted: PerformanceMetric[]): DisplayRow[] {
111
+ const preMap = new Map<string, PerformanceMetric>();
112
+ const postMap = new Map<string, PerformanceMetric>();
113
+ const consumed = new Set<PerformanceMetric>();
114
+
115
+ for (const m of sorted) {
116
+ if (m.label.endsWith(":pre")) {
117
+ preMap.set(m.label.slice(0, -4), m);
118
+ } else if (m.label.endsWith(":post")) {
119
+ postMap.set(m.label.slice(0, -5), m);
120
+ }
121
+ }
122
+
123
+ const rows: DisplayRow[] = [];
124
+
125
+ for (const m of sorted) {
126
+ if (consumed.has(m)) continue;
127
+
128
+ if (m.label.endsWith(":pre")) {
129
+ const base = m.label.slice(0, -4);
130
+ const post = postMap.get(base);
131
+ if (post) {
132
+ // Merge into a single row with two disjoint spans
133
+ consumed.add(m);
134
+ consumed.add(post);
135
+ rows.push({
136
+ label: base,
137
+ startTime: m.startTime,
138
+ duration: m.duration + post.duration,
139
+ depth: m.depth,
140
+ spans: [
141
+ { startTime: m.startTime, duration: m.duration },
142
+ { startTime: post.startTime, duration: post.duration },
143
+ ],
144
+ });
145
+ continue;
146
+ }
147
+ // Lone :pre — display with base label
148
+ consumed.add(m);
149
+ rows.push({
150
+ label: base,
151
+ startTime: m.startTime,
152
+ duration: m.duration,
153
+ depth: m.depth,
154
+ spans: [{ startTime: m.startTime, duration: m.duration }],
155
+ });
156
+ continue;
157
+ }
158
+
159
+ if (m.label.endsWith(":post")) {
160
+ const base = m.label.slice(0, -5);
161
+ if (preMap.has(base)) {
162
+ // Already consumed as part of the pair above
163
+ continue;
164
+ }
165
+ // Lone :post — display with base label
166
+ consumed.add(m);
167
+ rows.push({
168
+ label: base,
169
+ startTime: m.startTime,
170
+ duration: m.duration,
171
+ depth: m.depth,
172
+ spans: [{ startTime: m.startTime, duration: m.duration }],
173
+ });
174
+ continue;
175
+ }
176
+
177
+ // Regular metric
178
+ rows.push({
179
+ label: m.label,
180
+ startTime: m.startTime,
181
+ duration: m.duration,
182
+ depth: m.depth,
183
+ spans: [{ startTime: m.startTime, duration: m.duration }],
184
+ });
185
+ }
186
+
187
+ return rows;
188
+ }
189
+
26
190
  export function logMetrics(
27
191
  method: string,
28
192
  pathname: string,
29
- metricsStore: MetricsStore
193
+ metricsStore: MetricsStore,
30
194
  ): void {
31
195
  const total = performance.now() - metricsStore.requestStart;
32
196
 
33
- // Find max label length for alignment
34
- const maxLabelLen = Math.max(
35
- ...metricsStore.metrics.map((m) => m.label.length),
36
- 20
197
+ const sorted = sortMetrics(metricsStore.metrics);
198
+ const displayRows = buildDisplayRows(sorted);
199
+
200
+ const labels = displayRows.map(
201
+ (r) =>
202
+ `${" ".repeat(BASE_INDENT + (r.depth ?? 0) * DEPTH_INDENT)}${r.label}`,
203
+ );
204
+ const startValues = displayRows.map((r) => formatMs(r.startTime));
205
+ const durationValues = displayRows.map((r) => formatMs(r.duration));
206
+ const startWidth = Math.max(
207
+ "start".length,
208
+ ...startValues.map((v) => v.length),
209
+ );
210
+ const durationWidth = Math.max(
211
+ "dur".length,
212
+ ...durationValues.map((v) => v.length),
213
+ );
214
+ const spanWidth = Math.max(
215
+ "span".length,
216
+ ...labels.map((label) => label.length),
217
+ 22,
218
+ );
219
+ const timelinePadding = " ".repeat(
220
+ startWidth + 2 + durationWidth + 2 + spanWidth + 2,
221
+ );
222
+
223
+ console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(2)}ms)`);
224
+ console.log(
225
+ `${"start".padStart(startWidth)} ${"dur".padStart(durationWidth)} ${"span".padEnd(spanWidth)} timeline`,
37
226
  );
227
+ console.log(`${timelinePadding}${createTimelineAxis(total)}`);
38
228
 
39
- console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(1)}ms)`);
229
+ for (let index = 0; index < displayRows.length; index++) {
230
+ const row = displayRows[index];
231
+ const label = labels[index].padEnd(spanWidth);
232
+ const start = formatMs(row.startTime).padStart(startWidth);
233
+ const duration = formatMs(row.duration).padStart(durationWidth);
40
234
 
41
- for (const m of metricsStore.metrics) {
42
- const paddedLabel = m.label.padEnd(maxLabelLen);
43
- console.log(` ${paddedLabel} ${m.duration.toFixed(1)}ms`);
235
+ console.log(
236
+ `${start} ${duration} ${label} ${renderTimeline(row.spans, total)}`,
237
+ );
44
238
  }
45
239
  }
46
240
 
47
- /**
48
- * Generate Server-Timing header value from metrics
49
- * Format: metric-name;dur=X.XX
50
- */
51
241
  export function generateServerTiming(metricsStore: MetricsStore): string {
52
242
  return metricsStore.metrics
53
243
  .map((m) => {
54
244
  // Convert label to valid Server-Timing name (alphanumeric and hyphens)
55
- const name = m.label
245
+ const base = m.label
56
246
  .replace(/:/g, "-")
57
247
  .replace(/[^a-zA-Z0-9-]/g, "")
58
248
  .toLowerCase();
249
+ const name = m.depth ? `d${m.depth}-${base}` : base;
59
250
  return `${name};dur=${m.duration.toFixed(2)}`;
60
251
  })
61
252
  .join(", ");