@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  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 +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,167 @@
1
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
2
+
3
+ /**
4
+ * Eager Flight-payload injector for the PPR resume path.
5
+ *
6
+ * rsc-html-stream's injectRSCPayload starts forwarding Flight chunks only from
7
+ * inside its first transform() callback — i.e. AFTER the first HTML chunk flows.
8
+ * That policy exists for the normal document path (a <script> must not precede
9
+ * the doctype). On a PPR shell HIT it parks the ENTIRE hydration payload: the
10
+ * resumed fizz render emits its first chunk only when the first hole's data
11
+ * resolves (live loaders — measured ~1.5s on SFCC-backed pages), while the
12
+ * Flight root row is ready within ~30ms of the tail render starting. The client
13
+ * cannot call hydrateRoot until that root row arrives, so the lazy start held
14
+ * hydration hostage to the slowest loader for no structural reason: the stored
15
+ * prelude (a complete document through </body></html>) is already on the wire
16
+ * before the tail, so every tail byte is foster-parented and a Flight <script>
17
+ * is valid as the FIRST tail byte.
18
+ *
19
+ * This injector starts pumping Flight chunks immediately in start(). Ordering
20
+ * safety is kept by serializing ALL writes through one promise chain: fizz
21
+ * chunks buffered within a tick flush as one atomic task (same batching idea as
22
+ * the stock injector — never inject between two partial HTML chunks), and each
23
+ * Flight script is its own task, so scripts land only between batches. The
24
+ * trailer is stripped from passing HTML and re-appended once, after both
25
+ * streams complete — identical to the stock contract.
26
+ *
27
+ * RESUME/DATA-VARIANT ONLY. The normal document path must keep the stock
28
+ * injector: there the first bytes are the document head, and an eager script
29
+ * would precede the doctype.
30
+ */
31
+
32
+ const encoder = new TextEncoder();
33
+ const TRAILER = "</body></html>";
34
+
35
+ // Escape closing script tags and HTML comments in JS content (ported from
36
+ // rsc-html-stream/server; escapes the "s" instead of the slash so a regexp
37
+ // literal like `0</script/` stays valid JS).
38
+ function escapeScript(script: string): string {
39
+ return script.replace(/<!--/g, "<\\!--").replace(/<\/(script)/gi, "</\\$1");
40
+ }
41
+
42
+ function writeScript(
43
+ controller: TransformStreamDefaultController<Uint8Array>,
44
+ jsExpr: string,
45
+ nonce: string | undefined,
46
+ ): void {
47
+ controller.enqueue(
48
+ encoder.encode(
49
+ `<script${nonce ? ` nonce="${nonce}"` : ""}>${escapeScript(
50
+ `(self.__FLIGHT_DATA||=[]).push(${jsExpr})`,
51
+ )}</script>`,
52
+ ),
53
+ );
54
+ }
55
+
56
+ export function injectRSCPayloadEager(
57
+ rscStream: ReadableStream<Uint8Array>,
58
+ options?: { nonce?: string },
59
+ ): TransformStream<Uint8Array, Uint8Array> {
60
+ const nonce = options?.nonce;
61
+ const htmlDecoder = new TextDecoder();
62
+ const t0 = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
63
+ let loggedFirstFlight = false;
64
+ let loggedFirstHtml = false;
65
+
66
+ // All output goes through this chain: one task per Flight script, one task
67
+ // per buffered-HTML batch. A script can therefore never split a batch.
68
+ let queue: Promise<void> = Promise.resolve();
69
+ const enqueueTask = (fn: () => void): Promise<void> => {
70
+ queue = queue.then(fn);
71
+ return queue;
72
+ };
73
+
74
+ let buffered: Uint8Array[] = [];
75
+ let timeout: ReturnType<typeof setTimeout> | null = null;
76
+ let rscDone: Promise<void> = Promise.resolve();
77
+
78
+ function flushBufferedHTML(
79
+ controller: TransformStreamDefaultController<Uint8Array>,
80
+ ): void {
81
+ if (INTERNAL_RANGO_DEBUG && !loggedFirstHtml && buffered.length > 0) {
82
+ loggedFirstHtml = true;
83
+ console.log(
84
+ `[Server][ppr] eager-inject: first resumed HTML batch +${Math.round(performance.now() - t0)}ms`,
85
+ );
86
+ }
87
+ for (const chunk of buffered) {
88
+ let buf = htmlDecoder.decode(chunk, { stream: true });
89
+ if (buf.endsWith(TRAILER)) buf = buf.slice(0, -TRAILER.length);
90
+ controller.enqueue(encoder.encode(buf));
91
+ }
92
+ const remaining = htmlDecoder.decode();
93
+ if (remaining.length) {
94
+ const out = remaining.endsWith(TRAILER)
95
+ ? remaining.slice(0, -TRAILER.length)
96
+ : remaining;
97
+ controller.enqueue(encoder.encode(out));
98
+ }
99
+ buffered.length = 0;
100
+ timeout = null;
101
+ }
102
+
103
+ async function pumpRSC(
104
+ controller: TransformStreamDefaultController<Uint8Array>,
105
+ ): Promise<void> {
106
+ const rscDecoder = new TextDecoder("utf-8", { fatal: true });
107
+ const reader = rscStream.getReader();
108
+ for (;;) {
109
+ const { done, value } = await reader.read();
110
+ if (done) break;
111
+ // String when the chunk is valid unicode, base64 round-trip otherwise —
112
+ // same fallback the stock injector uses.
113
+ let jsExpr: string;
114
+ try {
115
+ jsExpr = JSON.stringify(rscDecoder.decode(value, { stream: true }));
116
+ } catch {
117
+ const base64 = JSON.stringify(
118
+ btoa(String.fromCodePoint(...(value as Uint8Array))),
119
+ );
120
+ jsExpr = `Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`;
121
+ }
122
+ await enqueueTask(() => {
123
+ if (INTERNAL_RANGO_DEBUG && !loggedFirstFlight) {
124
+ loggedFirstFlight = true;
125
+ console.log(
126
+ `[Server][ppr] eager-inject: first flight script +${Math.round(performance.now() - t0)}ms`,
127
+ );
128
+ }
129
+ writeScript(controller, jsExpr, nonce);
130
+ });
131
+ }
132
+ const remaining = rscDecoder.decode();
133
+ if (remaining.length) {
134
+ await enqueueTask(() =>
135
+ writeScript(controller, JSON.stringify(remaining), nonce),
136
+ );
137
+ }
138
+ }
139
+
140
+ return new TransformStream<Uint8Array, Uint8Array>({
141
+ start(controller) {
142
+ // The eager part: pump Flight immediately, before any HTML arrives.
143
+ rscDone = pumpRSC(controller).catch((err) => {
144
+ try {
145
+ controller.error(err);
146
+ } catch {
147
+ // Stream already errored/closed; nothing to signal.
148
+ }
149
+ });
150
+ },
151
+ transform(chunk, controller) {
152
+ buffered.push(chunk);
153
+ if (timeout) return;
154
+ // Batch same-tick fizz chunks so a Flight script cannot land between two
155
+ // partial HTML chunks of one logical write (stock injector's invariant).
156
+ timeout = setTimeout(() => {
157
+ void enqueueTask(() => flushBufferedHTML(controller));
158
+ }, 0);
159
+ },
160
+ async flush(controller) {
161
+ await rscDone;
162
+ if (timeout) clearTimeout(timeout);
163
+ await enqueueTask(() => flushBufferedHTML(controller));
164
+ controller.enqueue(encoder.encode(TRAILER));
165
+ },
166
+ });
167
+ }
@@ -0,0 +1,228 @@
1
+ import React from "react";
2
+ import { renderSegments } from "../segment-system.js";
3
+ import {
4
+ filterSegmentOrder,
5
+ filterRouteSegmentIds,
6
+ } from "../browser/react/filter-segment-order.js";
7
+ import { ThemeProvider } from "../theme/ThemeProvider.js";
8
+ import { NonceContext } from "../browser/react/nonce-context.js";
9
+ import { NavigationStoreContext } from "../browser/react/context.js";
10
+ import type { NavigationStoreContextValue } from "../browser/react/context.js";
11
+ import type { HandleData } from "../browser/types.js";
12
+ import type { ResolvedSegment } from "../types.js";
13
+ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
14
+ import type {
15
+ EventController,
16
+ DerivedNavigationState,
17
+ } from "../browser/event-controller.js";
18
+
19
+ /**
20
+ * createFromReadableStream from @rangojs/router/internal/deps/ssr.
21
+ * Deserializes the Flight branch used to build the SSR VDOM.
22
+ */
23
+ export type CreateFromReadableStream = <T>(
24
+ stream: ReadableStream<Uint8Array>,
25
+ ) => Promise<T>;
26
+
27
+ /**
28
+ * RSC payload type (minimal interface for SSR)
29
+ */
30
+ export interface RscPayload {
31
+ metadata?: {
32
+ segments?: ResolvedSegment[];
33
+ rootLayout?: React.ComponentType<{ children: React.ReactNode }>;
34
+ handles?: AsyncGenerator<HandleData, void, unknown>;
35
+ matched?: string[];
36
+ pathname?: string;
37
+ params?: Record<string, string>;
38
+ basename?: string;
39
+ themeConfig?: ResolvedThemeConfig | null;
40
+ initialTheme?: Theme;
41
+ version?: string;
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Consume an async generator and return a Promise that resolves with the final value.
47
+ * Used for SSR where we need to await all handle data before rendering.
48
+ */
49
+ async function consumeAsyncGenerator(
50
+ generator: AsyncGenerator<HandleData, void, unknown>,
51
+ ): Promise<HandleData> {
52
+ let lastData: HandleData = {};
53
+ for await (const data of generator) {
54
+ lastData = data;
55
+ }
56
+ return lastData;
57
+ }
58
+
59
+ /**
60
+ * Create a minimal event controller for SSR.
61
+ * This provides the correct pathname so useNavigation returns the right value during SSR.
62
+ */
63
+ function createSsrEventController(opts: {
64
+ pathname: string;
65
+ params?: Record<string, string>;
66
+ handleData?: HandleData;
67
+ matched?: string[];
68
+ }): EventController {
69
+ const location = new URL(opts.pathname, "http://localhost");
70
+ let params = opts.params ?? {};
71
+ const rawMatched = opts.matched ?? [];
72
+ const handleState = {
73
+ data: opts.handleData ?? {},
74
+ segmentOrder: filterSegmentOrder(rawMatched),
75
+ routeSegmentIds: filterRouteSegmentIds(rawMatched),
76
+ };
77
+ const state: DerivedNavigationState = {
78
+ state: "idle",
79
+ isStreaming: false,
80
+ isNavigating: false,
81
+ location,
82
+ pendingUrl: null,
83
+ inflightActions: [],
84
+ };
85
+
86
+ return {
87
+ getState: () => state,
88
+ getLocation: () => location,
89
+ subscribe: () => () => {},
90
+ getActionState: () => ({
91
+ state: "idle",
92
+ actionId: null,
93
+ payload: null,
94
+ error: null,
95
+ result: null,
96
+ }),
97
+ subscribeToAction: () => () => {},
98
+ subscribeToHandles: () => () => {},
99
+ setHandleData: () => {},
100
+ getHandleState: () => handleState,
101
+ setRouteSegmentIds: () => {},
102
+ setParams: (nextParams) => {
103
+ params = nextParams;
104
+ },
105
+ getParams: () => params,
106
+ setLocation: () => {},
107
+ startNavigation: () => {
108
+ throw new Error("Navigation not supported during SSR");
109
+ },
110
+ abortNavigation: () => {},
111
+ startAction: () => {
112
+ throw new Error("Actions not supported during SSR");
113
+ },
114
+ abortAllActions: () => {},
115
+ getCurrentNavigation: () => null,
116
+ getInflightActions: () => new Map(),
117
+ hadAnyConcurrentActions: () => false,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Options for {@link createSsrRootComponent}.
123
+ */
124
+ export interface SsrRootOptions {
125
+ /** createFromReadableStream for the SSR branch of the Flight stream. */
126
+ createFromReadableStream: CreateFromReadableStream;
127
+ /** The Flight stream branch to deserialize into the SSR VDOM. */
128
+ rscStream: ReadableStream<Uint8Array>;
129
+ /** Nonce for CSP; propagated to NonceContext. */
130
+ nonce?: string;
131
+ }
132
+
133
+ /**
134
+ * Build the closure component that deserializes the Flight payload, consumes
135
+ * handles to completion, builds the segment tree, and wraps it in the
136
+ * NavigationStore / Nonce / Theme providers.
137
+ *
138
+ * The full-fizz path (renderHTML), the shell prerender pass (capture), and the
139
+ * shell resume pass all render this identical tree. `resume` requires the tree
140
+ * above the postponed holes to match the prerendered tree; rendering the same
141
+ * builder over the same replayed segments is what makes that hold. A fresh
142
+ * component instance per pass is fine — replay matches structure, not function
143
+ * identity.
144
+ *
145
+ * The memo slots (payload/handles/context/root) are closure-scoped to the
146
+ * returned instance, so each render pass memoizes independently. renderSegments
147
+ * is async: React.use() on a fresh promise would suspend and replay SsrRoot,
148
+ * re-running the whole segment-tree build unless the promise is memoized.
149
+ */
150
+ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
151
+ const { createFromReadableStream, rscStream, nonce } = opts;
152
+
153
+ let payload: Promise<RscPayload> | undefined;
154
+ let handlesPromise: Promise<HandleData> | undefined;
155
+ let ssrContextValue: NavigationStoreContextValue | undefined;
156
+ let rootPromise: Promise<React.ReactNode> | undefined;
157
+
158
+ return function SsrRoot() {
159
+ payload ??= createFromReadableStream<RscPayload>(rscStream);
160
+ const resolved = React.use(payload);
161
+
162
+ const themeConfig = resolved.metadata?.themeConfig ?? null;
163
+ const pathname = resolved.metadata?.pathname ?? "/";
164
+
165
+ // Await handles before creating SSR event controller so hooks can
166
+ // read request-local handle data via NavigationStoreContext.
167
+ // The handles property is an async generator that yields on each push
168
+ // Memoize the promise since async generators can only be iterated once
169
+ let handleData: HandleData = {};
170
+ if (resolved.metadata?.handles) {
171
+ handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
172
+ handleData = React.use(handlesPromise);
173
+ }
174
+
175
+ // Create SSR context with request-local pathname/params/handles.
176
+ ssrContextValue ??= {
177
+ store: null as any,
178
+ eventController: createSsrEventController({
179
+ pathname,
180
+ params: resolved.metadata?.params,
181
+ handleData,
182
+ matched: resolved.metadata?.matched,
183
+ }),
184
+ navigate: async () => {},
185
+ refresh: async () => {},
186
+ version: resolved.metadata?.version,
187
+ basename: resolved.metadata?.basename,
188
+ };
189
+
190
+ // Build content tree from segments.
191
+ // Order must match NavigationProvider: NavigationStoreContext > NonceContext > ThemeProvider > content
192
+ // Memoize like payload/handles above: renderSegments is async, so
193
+ // React.use() on a fresh promise suspends and replays SsrRoot, which
194
+ // would re-run the entire segment-tree build on every initial render.
195
+ rootPromise ??= Promise.resolve(
196
+ renderSegments(resolved.metadata?.segments ?? [], {
197
+ rootLayout: resolved.metadata?.rootLayout,
198
+ }),
199
+ );
200
+ let content: React.ReactNode = React.use(rootPromise);
201
+
202
+ // Wrap content with ThemeProvider if theme is enabled
203
+ if (themeConfig) {
204
+ content = (
205
+ <ThemeProvider
206
+ config={themeConfig}
207
+ initialTheme={resolved.metadata?.initialTheme}
208
+ >
209
+ {content}
210
+ </ThemeProvider>
211
+ );
212
+ }
213
+
214
+ // Wrap with NonceContext so client components (e.g. MetaTags) can
215
+ // apply CSP nonces to inline scripts during SSR. Always present to
216
+ // match the browser-side NavigationProvider tree shape for hydration.
217
+ content = (
218
+ <NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
219
+ );
220
+
221
+ // Wrap with NavigationStoreContext for useNavigation hook
222
+ return (
223
+ <NavigationStoreContext.Provider value={ssrContextValue!}>
224
+ {content}
225
+ </NavigationStoreContext.Provider>
226
+ );
227
+ };
228
+ }
@@ -35,8 +35,7 @@ import type { Handler } from "./types.js";
35
35
  import type { StaticBuildContext } from "./prerender.js";
36
36
  import type { UseItems, HandlerUseItem } from "./route-types.js";
37
37
  import { isCachedFunction } from "./cache/taint.js";
38
-
39
- // -- Types ------------------------------------------------------------------
38
+ import { isUnderTestRunner } from "./runtime-env.js";
40
39
 
41
40
  export interface StaticHandlerOptions {
42
41
  /**
@@ -61,7 +60,9 @@ export interface StaticHandlerDefinition<
61
60
  use?: () => UseItems<HandlerUseItem>;
62
61
  }
63
62
 
64
- // -- Function ---------------------------------------------------------------
63
+ // Process-stable fallback id counter (mirrors createHandle/createLoader/Prerender).
64
+ // Only assigned in bare unit tests where the Vite plugin did not inject an id.
65
+ let runtimeStaticIdCounter = 0;
65
66
 
66
67
  export function Static<TParams extends Record<string, any> = {}>(
67
68
  handler: (ctx: StaticBuildContext) => ReactNode | Promise<ReactNode>,
@@ -69,8 +70,6 @@ export function Static<TParams extends Record<string, any> = {}>(
69
70
  __injectedId?: string,
70
71
  ): StaticHandlerDefinition<TParams>;
71
72
 
72
- // -- Implementation ---------------------------------------------------------
73
-
74
73
  export function Static<TParams extends Record<string, any>>(
75
74
  handler: Function,
76
75
  optionsOrId?: StaticHandlerOptions | string,
@@ -94,12 +93,15 @@ export function Static<TParams extends Record<string, any>>(
94
93
  id = maybeId ?? "";
95
94
  }
96
95
 
97
- if (!id) {
96
+ if (!id && !isUnderTestRunner()) {
98
97
  throw new Error(
99
- "[rango] Static: missing $$id. " +
100
- "Ensure the exposeInternalIds Vite plugin is configured.",
98
+ "[rango] Static: missing $$id. Use `export const X = Static(...)` and " +
99
+ "ensure the exposeInternalIds Vite plugin is configured.",
101
100
  );
102
101
  }
102
+ if (!id) {
103
+ id = `__rango_runtime_static_${runtimeStaticIdCounter++}`;
104
+ }
103
105
 
104
106
  return {
105
107
  __brand: "staticHandler" as const,
@@ -109,11 +111,6 @@ export function Static<TParams extends Record<string, any>>(
109
111
  };
110
112
  }
111
113
 
112
- // -- Type guard -------------------------------------------------------------
113
-
114
- /**
115
- * Type guard to check if a value is a StaticHandlerDefinition.
116
- */
117
114
  export function isStaticHandler(
118
115
  value: unknown,
119
116
  ): value is StaticHandlerDefinition {
@@ -12,7 +12,14 @@
12
12
  * 2. Telemetry path — `createCacheSink` returns a `{ sink, events }` pair the
13
13
  * consumer wires via `createRouter({ telemetry: sink })`. This has ZERO
14
14
  * production surface: no header, just structured `cache.decision` events
15
- * (which carry the same coarse `segments` cache signal).
15
+ * (which carry the same coarse `segments` cache signal). Assert with
16
+ * `assertCacheDecision(events, routeKey, expected)` (the one-call counterpart
17
+ * of `assertCacheStatus`) or filter raw via `filterCacheDecisions`.
18
+ *
19
+ * Both paths report the SAME coarse route-level signal — pick by TRANSPORT, not
20
+ * by meaning: the header is the only signal a black-box Playwright `Response`
21
+ * carries (needs the debug gate ON); the sink is the only zero-production-surface
22
+ * option and the only one exposing per-segment `shouldRevalidate`.
16
23
  *
17
24
  * v1 cache status is COARSE (route-level): the router reports a single entry
18
25
  * keyed by the route key (the route NAME), not per individual segment.
@@ -37,18 +44,6 @@ export type ExpectedCacheStatus = CacheSegmentStatus;
37
44
  /** A target carrying response headers (a Response or a `{ headers }` object). */
38
45
  export type CacheStatusTarget = Response | { headers: Headers };
39
46
 
40
- /**
41
- * Parse an `X-Rango-Cache` header value into a `{ routeKey: status }` map.
42
- *
43
- * Header format: `<routeKey>=<status>, <routeKey2>=<status2>`. The key is the
44
- * route NAME (ctx.routeKey, e.g. `product.detail`), NOT the URL pattern —
45
- * see assertCacheStatus. Whitespace around entries and the `=` is tolerated.
46
- * Entries without a status are ignored.
47
- *
48
- * @example
49
- * parseCacheHeader("product.detail=hit, shop.layout=stale")
50
- * // => { "product.detail": "hit", "shop.layout": "stale" }
51
- */
52
47
  export function parseCacheHeader(
53
48
  headerValue: string | null | undefined,
54
49
  ): Record<string, string> {
@@ -71,25 +66,6 @@ function getHeaders(target: CacheStatusTarget): Headers {
71
66
  return target.headers;
72
67
  }
73
68
 
74
- /**
75
- * Assert that the `X-Rango-Cache` header reports `expected` status for the
76
- * given route. Throws a descriptive error when the header is missing (gate
77
- * off), the route is absent, or the status differs.
78
- *
79
- * `routeKey` is the route NAME (e.g. `product.detail`), the same id the header
80
- * carries — NOT the URL pattern (`/products/:id`). The signal is built from
81
- * ctx.routeKey (telemetry.ts), so a pattern-shaped key never matches.
82
- *
83
- * The header is produced by the RSC render pipeline, so get the Response from
84
- * the router's real fetch path (`router.fetch(...)`), with the debug cache
85
- * signal gate enabled (`debugCacheSignal: true` or `RANGO_TEST_SIGNALS=1`).
86
- * NOTE: `dispatch()` is the non-RSC primitive and never emits this header.
87
- *
88
- * @example
89
- * // debugCacheSignal must be enabled on the router under test.
90
- * const res = await router.fetch(new Request("https://app/products/42"));
91
- * assertCacheStatus(res, "product.detail", "hit");
92
- */
93
69
  export function assertCacheStatus(
94
70
  target: CacheStatusTarget,
95
71
  segment: string,
@@ -131,19 +107,6 @@ export interface CacheSink {
131
107
  events: TelemetryEvent[];
132
108
  }
133
109
 
134
- /**
135
- * Create a capturing telemetry sink for asserting on `cache.decision` events.
136
- *
137
- * This is the ZERO-production-surface path: no response header is emitted, the
138
- * consumer just inspects the captured events.
139
- *
140
- * @example
141
- * const { sink, events } = createCacheSink();
142
- * const router = createRouter({ telemetry: sink, ... });
143
- * // ...send a request through the router's RSC fetch path...
144
- * const decisions = filterCacheDecisions(events);
145
- * expect(decisions[0].segments?.[0].cacheStatus).toBe("hit");
146
- */
147
110
  export function createCacheSink(): CacheSink {
148
111
  const events: TelemetryEvent[] = [];
149
112
  const sink: TelemetrySink = {
@@ -154,9 +117,6 @@ export function createCacheSink(): CacheSink {
154
117
  return { sink, events };
155
118
  }
156
119
 
157
- /**
158
- * Filter captured telemetry events down to `cache.decision` events.
159
- */
160
120
  export function filterCacheDecisions(
161
121
  events: readonly TelemetryEvent[],
162
122
  ): CacheDecisionEvent[] {
@@ -164,3 +124,39 @@ export function filterCacheDecisions(
164
124
  (e): e is CacheDecisionEvent => e.type === "cache.decision",
165
125
  );
166
126
  }
127
+
128
+ /**
129
+ * Telemetry-path counterpart of {@link assertCacheStatus}: assert a captured
130
+ * `cache.decision` event reported `expected` for the segment keyed by `routeKey`
131
+ * (the route NAME, the same coarse key the header path uses). Throws an
132
+ * actionable error when no matching segment was captured, or on a mismatch.
133
+ *
134
+ * Pairs with {@link createCacheSink}: wire `createRouter({ telemetry: sink })`,
135
+ * drive an RSC request, then assert against the recorded `events`. This is the
136
+ * zero-production-surface path (no header to enable). NOTE: `events` accumulates
137
+ * across requests, so the FIRST matching segment wins — slice or recreate the
138
+ * sink between requests for the same `routeKey`.
139
+ */
140
+ export function assertCacheDecision(
141
+ events: readonly TelemetryEvent[],
142
+ routeKey: string,
143
+ expected: ExpectedCacheStatus,
144
+ ): void {
145
+ const segments = filterCacheDecisions(events).flatMap(
146
+ (d) => d.segments ?? [],
147
+ );
148
+ const seg = segments.find((s) => s.id === routeKey);
149
+ if (seg === undefined) {
150
+ const known = segments.map((s) => s.id);
151
+ throw new Error(
152
+ `assertCacheDecision: no cache.decision segment for routeKey "${routeKey}". ` +
153
+ `Seen: ${known.length > 0 ? known.join(", ") : "(none)"}. Wire ` +
154
+ `createRouter({ telemetry: createCacheSink().sink }) and drive an RSC request.`,
155
+ );
156
+ }
157
+ if (seg.cacheStatus !== expected) {
158
+ throw new Error(
159
+ `assertCacheDecision: routeKey "${routeKey}" expected "${expected}" but got "${seg.cacheStatus}".`,
160
+ );
161
+ }
162
+ }
@@ -12,32 +12,13 @@
12
12
  * It relies on createHandle registering the collect even in a bare test (it
13
13
  * assigns a runtime fallback id when the Vite plugin did not inject one). If a
14
14
  * handle's module was never imported (so createHandle never ran), the collect is
15
- * unregistered and this falls back to a flat array with a warning.
15
+ * unregistered and this falls back to the default identity collect — with a
16
+ * warning, since a CUSTOM collect that failed to register silently returns the
17
+ * wrong shape.
16
18
  */
17
19
 
18
20
  import { getCollectFn, type Handle } from "../handle.js";
19
21
 
20
- /**
21
- * Run a handle's collect function on per-segment pushed values.
22
- *
23
- * @param handle - The handle whose collect to run.
24
- * @param segments - Per-segment pushed values: each entry is the array of values
25
- * one route segment pushed for this handle, in parent -> child order. Empty
26
- * per-segment arrays are dropped before the collect runs, matching production
27
- * collectHandleData (a segment that pushed nothing is not passed through).
28
- * @returns The accumulated value the handle's collect produces.
29
- *
30
- * @example
31
- * ```ts
32
- * // Default flatten
33
- * collectHandle(Breadcrumbs, [[{ label: "Home", href: "/" }], [{ label: "P", href: "/p" }]]);
34
- * // -> [{ label: "Home", href: "/" }, { label: "P", href: "/p" }]
35
- *
36
- * // Custom "last wins"
37
- * const PageTitle = createHandle<string, string>((s) => s.flat().at(-1) ?? "");
38
- * collectHandle(PageTitle, [["Home"], ["Product"]]); // -> "Product"
39
- * ```
40
- */
41
22
  export function collectHandle<TData, TAccumulated>(
42
23
  handle: Handle<TData, TAccumulated>,
43
24
  segments: ReadonlyArray<ReadonlyArray<TData>>,
@@ -46,18 +27,20 @@ export function collectHandle<TData, TAccumulated>(
46
27
  | ((segments: TData[][]) => TAccumulated)
47
28
  | undefined;
48
29
 
30
+ // Drop empty arrays matching production behavior (segment count/indices).
31
+ const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
32
+
33
+ // No registered collect (the handle's module was not imported): fall back to the
34
+ // default identity collect — the per-segment arrays as-is, mirroring production
35
+ // collectHandleData. Warn, because a handle with a CUSTOM collect would silently
36
+ // get the wrong shape (the runtime can't tell it from an intended default).
49
37
  if (!collectFn) {
50
38
  console.warn(
51
- `[rango] collectHandle: handle "${handle.$$id}" has no registered collect ` +
52
- `function. Import the handle's module so createHandle() runs. Falling ` +
53
- `back to a flat array.`,
39
+ `[rango] collectHandle: handle "${handle.$$id}" has no registered collect ` +
40
+ `falling back to the identity (per-segment data as-is). Import the handle's ` +
41
+ `module so createHandle() runs if you expected a custom collect.`,
54
42
  );
55
- return segments.flat() as unknown as TAccumulated;
43
+ return nonEmpty as unknown as TAccumulated;
56
44
  }
57
-
58
- // Match production collectHandleData (handle.ts): segments that pushed
59
- // nothing (empty arrays) are dropped before the collect runs, so a collect
60
- // that inspects segment count or indices sees the same input as at runtime.
61
- const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
62
45
  return collectFn(nonEmpty);
63
46
  }