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

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 +426 -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 +2500 -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 +29 -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-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -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 +177 -35
  205. package/src/index.ts +255 -71
  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 +337 -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 +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  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 +40 -72
  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 +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -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
@@ -2,263 +2,118 @@
2
2
  * CacheScope - Runtime cache scope for iterator-based caching
3
3
  *
4
4
  * Each cache() boundary in the route tree creates a new CacheScope.
5
- * The scope owns: config, serialization, and storage operations.
5
+ * The scope owns: config, key management, and storage operations.
6
+ *
7
+ * Serialization is delegated to segment-codec.ts.
8
+ * Handle data capture/restore is delegated to handle-snapshot.ts.
6
9
  */
7
10
 
8
- /// <reference types="@vitejs/plugin-rsc/types" />
9
-
10
11
  import type { PartialCacheOptions } from "../types.js";
11
12
  import type { ResolvedSegment } from "../types.js";
12
- import type {
13
- SegmentCacheStore,
14
- SegmentHandleData,
15
- CachedEntryData,
16
- SerializedSegmentData,
17
- } from "./types.js";
18
- import { getRequestContext } from "../server/request-context.js";
13
+ import type { SegmentCacheStore, CachedEntryData } from "./types.js";
14
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
19
15
  import {
20
- renderToReadableStream,
21
- createTemporaryReferenceSet,
22
- } from "@vitejs/plugin-rsc/rsc";
23
- import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
24
-
25
- // ============================================================================
26
- // Constants
27
- // ============================================================================
28
-
29
- /** Default TTL when no explicit value or store defaults are configured */
30
- const DEFAULT_TTL_SECONDS = 60;
31
-
32
- // ============================================================================
33
- // Serialization Utilities (internal)
34
- // ============================================================================
35
-
36
- /**
37
- * Generate cache key base from pathname and params.
38
- * Params are sorted alphabetically for consistent key generation.
39
- * @internal
40
- */
41
- function getCacheKeyBase(
42
- pathname: string,
43
- params?: Record<string, string>
44
- ): string {
45
- const paramStr = params
46
- ? Object.entries(params)
47
- .sort(([a], [b]) => a.localeCompare(b))
48
- .map(([k, v]) => `${k}=${v}`)
49
- .join("&")
50
- : "";
51
-
52
- return paramStr ? `${pathname}:${paramStr}` : pathname;
53
- }
54
-
55
- /**
56
- * Generate default cache key for a route request.
57
- * Single cache entry per route - uses pathname as the key.
58
- * Includes request type prefix since they produce different segment sets:
59
- * - doc: document requests (full page load)
60
- * - partial: navigation requests (client-side navigation)
61
- * - intercept: intercept navigation (modal/overlay routes)
62
- * @internal
63
- */
64
- function getDefaultRouteCacheKey(
65
- pathname: string,
66
- params?: Record<string, string>,
67
- isIntercept?: boolean
68
- ): string {
69
- const ctx = getRequestContext();
70
- const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
71
-
72
- // Intercept navigations get their own cache namespace
73
- const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
74
-
75
- return `${prefix}:${getCacheKeyBase(pathname, params)}`;
16
+ getRequestContext,
17
+ _getRequestContext,
18
+ } from "../server/request-context.js";
19
+ import { recordRequestTags } from "./cache-tag.js";
20
+ import { reportCacheError } from "./cache-error.js";
21
+ // segment-codec is the only module on cache-scope's import graph that eagerly
22
+ // pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
23
+ // resolve). It is imported LAZILY at the two call sites below (deserializeSegments
24
+ // in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
25
+ // e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
26
+ // cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
27
+ // is unchanged: both methods are async and already awaited the codec.
28
+ import {
29
+ captureHandles,
30
+ restoreHandles,
31
+ encodeHandles,
32
+ decodeHandles,
33
+ } from "./handle-snapshot.js";
34
+ import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
35
+ import {
36
+ DEFAULT_ROUTE_TTL,
37
+ isFiniteNonNegativeSeconds,
38
+ resolveCacheKey,
39
+ resolveCacheStore,
40
+ resolveTagsOption,
41
+ } from "./cache-policy.js";
42
+ import type { RequestContext } from "../server/request-context.js";
43
+
44
+ export function resolveCacheTags(
45
+ config: PartialCacheOptions | false,
46
+ ctx: RequestContext | undefined,
47
+ ): string[] | undefined {
48
+ if (config === false) return undefined;
49
+ return resolveTagsOption(config.tags, ctx, "CacheScope");
76
50
  }
77
51
 
78
- /**
79
- * Convert a ReadableStream to a string.
80
- * @internal
81
- */
82
- async function streamToString(
83
- stream: ReadableStream<Uint8Array>
84
- ): Promise<string> {
85
- const reader = stream.getReader();
86
- const decoder = new TextDecoder();
87
- let result = "";
88
-
89
- while (true) {
90
- const { done, value } = await reader.read();
91
- if (done) break;
92
- result += decoder.decode(value, { stream: true });
52
+ function debugCacheLog(message: string): void {
53
+ if (INTERNAL_RANGO_DEBUG) {
54
+ console.log(message);
93
55
  }
94
-
95
- result += decoder.decode(); // flush
96
- return result;
97
56
  }
98
57
 
99
58
  /**
100
- * Convert a string to a ReadableStream.
101
- * @internal
59
+ * A finite, non-negative seconds value? A NaN/Infinity ttl/swr (from a bad
60
+ * cache() option or store defaults) flows into computeExpiration ->
61
+ * staleAt/expiresAt = NaN, where every `now > NaN` is false so the entry never
62
+ * evicts and is served fresh forever; a negative value makes every read a miss.
63
+ * Mirror profile-registry.ts's Number.isFinite + >= 0 check, but the callers
64
+ * degrade to a default (warning in dev) rather than throw — this runs on the
65
+ * foreground render.
102
66
  */
103
- function stringToStream(str: string): ReadableStream<Uint8Array> {
104
- const encoder = new TextEncoder();
105
- const uint8 = encoder.encode(str);
106
-
107
- return new ReadableStream({
108
- start(controller) {
109
- controller.enqueue(uint8);
110
- controller.close();
111
- },
112
- });
67
+ function isValidCacheSeconds(value: number, label: string): boolean {
68
+ if (isFiniteNonNegativeSeconds(value)) return true;
69
+ if (process.env.NODE_ENV !== "production") {
70
+ console.warn(
71
+ `[CacheScope] Invalid ${label} ${value}; falling back to default`,
72
+ );
73
+ }
74
+ return false;
113
75
  }
114
76
 
115
- /**
116
- * RSC-serialize a value using React Server Components stream.
117
- * Used for serializing loaderData, layout, loading components etc.
118
- * @internal
119
- */
120
- async function rscSerialize(value: unknown): Promise<string | undefined> {
121
- if (value === undefined || value === null) return undefined;
122
-
123
- const temporaryReferences = createTemporaryReferenceSet();
124
- const stream = renderToReadableStream(value, { temporaryReferences });
125
- return streamToString(stream);
77
+ /** Coerce a resolved ttl to a finite, non-negative number (default on invalid). */
78
+ function validatedTtl(value: number): number {
79
+ return isValidCacheSeconds(value, "ttl") ? value : DEFAULT_ROUTE_TTL;
126
80
  }
127
81
 
128
- /**
129
- * RSC-deserialize a value from a stored string.
130
- * @internal
131
- */
132
- async function rscDeserialize<T>(
133
- encoded: string | undefined
134
- ): Promise<T | undefined> {
135
- if (!encoded) return undefined;
136
-
137
- const temporaryReferences = createTemporaryReferenceSet();
138
- const stream = stringToStream(encoded);
139
- return createFromReadableStream<T>(stream, { temporaryReferences });
82
+ /** Coerce a resolved swr to a finite, non-negative number, or undefined (no SWR window). */
83
+ function validatedSwr(value: number | undefined): number | undefined {
84
+ if (value === undefined) return undefined;
85
+ return isValidCacheSeconds(value, "swr") ? value : undefined;
140
86
  }
141
87
 
142
- /**
143
- * Serialize segments for storage.
144
- * Each segment's component, layout, loading, and loaderData are RSC-serialized.
145
- * Metadata is preserved as-is.
146
- */
147
- export async function serializeSegments(
148
- segments: ResolvedSegment[]
149
- ): Promise<SerializedSegmentData[]> {
150
- const serialized: SerializedSegmentData[] = [];
151
-
152
- for (const segment of segments) {
153
- const temporaryReferences = createTemporaryReferenceSet();
154
-
155
- // Await component if it's a Promise (intercepts with loading keep component as Promise)
156
- const componentResolved =
157
- segment.component instanceof Promise
158
- ? await segment.component
159
- : segment.component;
160
-
161
- // Serialize the component to RSC stream
162
- const stream = renderToReadableStream(componentResolved, {
163
- temporaryReferences,
164
- });
165
-
166
- // Convert stream to string
167
- const encoded = await streamToString(stream);
168
-
169
- // RSC-serialize layout if present (ReactNode)
170
- const encodedLayout = segment.layout
171
- ? await rscSerialize(segment.layout)
172
- : undefined;
173
-
174
- // RSC-serialize loading if present (ReactNode) - preserves tree structure
175
- // Use "null" string to distinguish explicit null from undefined
176
- const encodedLoading =
177
- segment.loading !== undefined
178
- ? segment.loading === null
179
- ? "null"
180
- : await rscSerialize(segment.loading)
181
- : undefined;
182
-
183
- // Await and RSC-serialize loaderData if present
184
- const loaderDataResolved =
185
- segment.loaderData instanceof Promise
186
- ? await segment.loaderData
187
- : segment.loaderData;
188
- const encodedLoaderData = await rscSerialize(loaderDataResolved);
189
-
190
- // Await and RSC-serialize loaderDataPromise if present
191
- const loaderDataPromiseResolved =
192
- segment.loaderDataPromise instanceof Promise
193
- ? await segment.loaderDataPromise
194
- : segment.loaderDataPromise;
195
- const encodedLoaderDataPromise = await rscSerialize(
196
- loaderDataPromiseResolved
197
- );
198
-
199
- serialized.push({
200
- encoded,
201
- encodedLayout,
202
- encodedLoading,
203
- encodedLoaderData,
204
- encodedLoaderDataPromise,
205
- metadata: {
206
- id: segment.id,
207
- type: segment.type,
208
- namespace: segment.namespace,
209
- index: segment.index,
210
- params: segment.params,
211
- slot: segment.slot,
212
- belongsToRoute: segment.belongsToRoute,
213
- layoutName: segment.layoutName,
214
- parallelName: segment.parallelName,
215
- loaderId: segment.loaderId,
216
- loaderIds: segment.loaderIds,
217
- },
218
- });
219
- }
88
+ function getCacheKeyBase(
89
+ host: string,
90
+ pathname: string,
91
+ params?: Record<string, string>,
92
+ searchParams?: URLSearchParams,
93
+ ): string {
94
+ const paramStr = sortedRouteParams(params);
95
+ const searchStr = searchParams ? sortedSearchString(searchParams) : "";
220
96
 
221
- return serialized;
97
+ let key = `${host}${pathname}`;
98
+ if (paramStr) key += `:${paramStr}`;
99
+ if (searchStr) key += `?${searchStr}`;
100
+ return key;
222
101
  }
223
102
 
224
- /**
225
- * Deserialize segments from storage.
226
- * Reconstructs ResolvedSegment objects from RSC-serialized data.
227
- */
228
- export async function deserializeSegments(
229
- data: SerializedSegmentData[]
230
- ): Promise<ResolvedSegment[]> {
231
- const segments: ResolvedSegment[] = [];
232
-
233
- for (const item of data) {
234
- const temporaryReferences = createTemporaryReferenceSet();
235
-
236
- // Revive the component from cached string
237
- const stream = stringToStream(item.encoded);
238
- const component = await createFromReadableStream(stream, {
239
- temporaryReferences,
240
- });
103
+ function getDefaultRouteCacheKey(
104
+ pathname: string,
105
+ params?: Record<string, string>,
106
+ isIntercept?: boolean,
107
+ ): string {
108
+ const ctx = getRequestContext();
109
+ const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
110
+ const searchParams = ctx?.url.searchParams;
111
+ const host = ctx?.url.host ?? "localhost";
241
112
 
242
- // RSC-deserialize layout, loaderData, loaderDataPromise in parallel
243
- const [layout, loaderData, loaderDataPromise, loadingData] =
244
- await Promise.all([
245
- rscDeserialize(item.encodedLayout),
246
- rscDeserialize(item.encodedLoaderData),
247
- rscDeserialize(item.encodedLoaderDataPromise),
248
- rscDeserialize(item.encodedLoading),
249
- ]);
250
-
251
- segments.push({
252
- ...item.metadata,
253
- component: await component,
254
- layout,
255
- loading: loadingData,
256
- loaderData,
257
- loaderDataPromise,
258
- } as ResolvedSegment);
259
- }
113
+ // Intercept navigations get their own cache namespace
114
+ const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
260
115
 
261
- return segments;
116
+ return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
262
117
  }
263
118
 
264
119
  // ============================================================================
@@ -269,7 +124,8 @@ export async function deserializeSegments(
269
124
  * CacheScope represents a cache boundary in the route tree.
270
125
  *
271
126
  * When withCache encounters an entry with cache config, it creates
272
- * a new CacheScope. The scope owns serialization, storage, and TTL.
127
+ * a new CacheScope. The scope owns key management, TTL resolution,
128
+ * and storage operations. Serialization is handled by segment-codec.ts.
273
129
  *
274
130
  * Store resolution priority:
275
131
  * 1. Explicit store in cache() options
@@ -289,7 +145,7 @@ export class CacheScope {
289
145
 
290
146
  constructor(
291
147
  config: PartialCacheOptions | false,
292
- parent: CacheScope | null = null
148
+ parent: CacheScope | null = null,
293
149
  ) {
294
150
  this.config = config;
295
151
  this.parent = parent;
@@ -305,40 +161,49 @@ export class CacheScope {
305
161
  }
306
162
 
307
163
  /**
308
- * Get effective TTL from config or store defaults
164
+ * Get effective TTL from config or store defaults.
165
+ *
166
+ * Unlike profile-registry.ts (which fails fast at config time), the render
167
+ * path must DEGRADE: a non-finite/negative ttl (NaN/Infinity from a bad
168
+ * defaults config) would make computeExpiration produce NaN deadlines so the
169
+ * entry never evicts, or a guaranteed miss for a negative value. Fall back to
170
+ * DEFAULT_ROUTE_TTL instead of throwing in the foreground render.
309
171
  */
310
172
  get ttl(): number {
311
173
  if (this.config === false) return 0;
312
174
 
313
175
  // Explicit TTL in cache() options
314
176
  if (this.config.ttl !== undefined) {
315
- return this.config.ttl;
177
+ return validatedTtl(this.config.ttl);
316
178
  }
317
179
 
318
180
  // Fall back to store defaults (explicit store first, then app-level)
319
181
  const store = this.getStore();
320
182
  if (store?.defaults?.ttl !== undefined) {
321
- return store.defaults.ttl;
183
+ return validatedTtl(store.defaults.ttl);
322
184
  }
323
185
 
324
186
  // Hardcoded fallback
325
- return DEFAULT_TTL_SECONDS;
187
+ return DEFAULT_ROUTE_TTL;
326
188
  }
327
189
 
328
190
  /**
329
- * Get SWR window from config or store defaults
191
+ * Get SWR window from config or store defaults.
192
+ *
193
+ * A non-finite/negative swr is degraded to undefined (no SWR window) rather
194
+ * than fed into expiry math; see the ttl getter for the rationale.
330
195
  */
331
196
  get swr(): number | undefined {
332
197
  if (this.config === false) return undefined;
333
198
 
334
199
  // Explicit SWR in cache() options
335
200
  if (this.config.swr !== undefined) {
336
- return this.config.swr;
201
+ return validatedSwr(this.config.swr);
337
202
  }
338
203
 
339
204
  // Fall back to store defaults
340
205
  const store = this.getStore();
341
- return store?.defaults?.swr;
206
+ return validatedSwr(store?.defaults?.swr);
342
207
  }
343
208
 
344
209
  /**
@@ -346,65 +211,48 @@ export class CacheScope {
346
211
  * 1. Explicit store from cache() options
347
212
  * 2. App-level store from request context
348
213
  */
349
- private getStore(): SegmentCacheStore | null {
350
- // Explicit store from cache() options takes precedence
351
- if (this.explicitStore) {
352
- return this.explicitStore;
353
- }
354
- // Fall back to app-level store from request context
355
- const ctx = getRequestContext();
356
- return ctx?._cacheStore ?? null;
214
+ getStore(): SegmentCacheStore | null {
215
+ return resolveCacheStore(this.explicitStore);
357
216
  }
358
217
 
359
218
  /**
360
- * Resolve the cache key using custom key functions or default generation.
361
- *
362
- * Resolution priority:
363
- * 1. Route-level `key` function (full override)
364
- * 2. Store-level `keyGenerator` (modifies default key)
365
- * 3. Default key generation (prefix:pathname:params)
366
- *
219
+ * Resolve the cache key using the shared 3-tier priority.
367
220
  * @internal
368
221
  */
369
222
  private async resolveKey(
370
223
  pathname: string,
371
224
  params: Record<string, string>,
372
- isIntercept?: boolean
225
+ isIntercept?: boolean,
373
226
  ): Promise<string> {
374
- const requestCtx = getRequestContext();
375
- if (!requestCtx) {
376
- // Fallback to default key if no request context
377
- return getDefaultRouteCacheKey(pathname, params, isIntercept);
378
- }
379
-
380
- // Priority 1: Route-level key function (full override)
381
- if (this.config !== false && this.config.key) {
382
- try {
383
- const customKey = await this.config.key(requestCtx);
384
- return customKey;
385
- } catch (error) {
386
- console.error(`[CacheScope] Custom key function failed, using default:`, error);
387
- return getDefaultRouteCacheKey(pathname, params, isIntercept);
388
- }
389
- }
390
-
391
- // Generate default key
392
227
  const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
228
+ const keyFn = this.config !== false ? this.config.key : undefined;
229
+ return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
230
+ }
393
231
 
394
- // Priority 2: Store-level keyGenerator (modifies default key)
395
- const store = this.getStore();
396
- if (store?.keyGenerator) {
397
- try {
398
- const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
399
- return modifiedKey;
400
- } catch (error) {
401
- console.error(`[CacheScope] Store keyGenerator failed, using default:`, error);
402
- return defaultKey;
232
+ /**
233
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
234
+ * operation) when the predicate returns false or throws; returns true when
235
+ * there is no condition or no request context to evaluate it against.
236
+ */
237
+ private conditionAllows(op: "read" | "write"): boolean {
238
+ if (this.config === false || !this.config.condition) return true;
239
+ const requestCtx = getRequestContext();
240
+ if (!requestCtx) return true;
241
+ try {
242
+ if (!this.config.condition(requestCtx)) {
243
+ debugCacheLog(
244
+ `[CacheScope] condition returned false, skipping cache ${op}`,
245
+ );
246
+ return false;
403
247
  }
248
+ return true;
249
+ } catch (error) {
250
+ console.error(
251
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
252
+ error,
253
+ );
254
+ return false;
404
255
  }
405
-
406
- // Priority 3: Default key
407
- return defaultKey;
408
256
  }
409
257
 
410
258
  /**
@@ -418,56 +266,118 @@ export class CacheScope {
418
266
  async lookupRoute(
419
267
  pathname: string,
420
268
  params: Record<string, string>,
421
- isIntercept?: boolean
269
+ isIntercept?: boolean,
422
270
  ): Promise<{
423
271
  segments: ResolvedSegment[];
424
272
  shouldRevalidate: boolean;
425
273
  } | null> {
426
274
  if (!this.enabled) return null;
275
+ if (!this.conditionAllows("read")) return null;
427
276
 
428
277
  const store = this.getStore();
429
278
  if (!store) return null;
430
279
 
431
- // Resolve cache key (may use custom key functions)
432
- const key = await this.resolveKey(pathname, params, isIntercept);
433
-
280
+ // Resolve cache key INSIDE the try so a throwing consumer key() (or a
281
+ // store.keyGenerator) degrades to a cache miss (return null -> render
282
+ // uncached) instead of crashing the foreground render. resolveCacheKey
283
+ // itself keeps its hard-fail/no-fallback-to-default contract (a throw must
284
+ // not silently collide onto the default slot); the graceful degradation
285
+ // happens here, where a miss is a safe outcome.
286
+ let key: string | undefined;
434
287
  try {
288
+ key = await this.resolveKey(pathname, params, isIntercept);
289
+
435
290
  const result = await store.get(key);
436
291
 
437
292
  if (!result) {
438
- console.log(`[CacheScope] MISS: ${key}`);
293
+ debugCacheLog(`[CacheScope] MISS: ${key}`);
439
294
  return null;
440
295
  }
441
296
 
442
297
  const { data: cached, shouldRevalidate } = result;
443
298
 
444
- // Deserialize segments
445
- const segments = await deserializeSegments(cached.segments);
299
+ // Deserialize segments. A failure means the cached segments are corrupt/
300
+ // partial: evict the entry (self-heal - the re-render re-caches under the
301
+ // same key) and report it as corruption, distinct from a transient infra
302
+ // error (handled by the outer catch).
303
+ let segments: ResolvedSegment[];
304
+ try {
305
+ const { deserializeSegments } = await import("./segment-codec.js");
306
+ segments = await deserializeSegments(cached.segments);
307
+ } catch (error) {
308
+ reportCacheError(
309
+ error,
310
+ "cache-corrupt",
311
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
312
+ );
313
+ await store
314
+ .delete(key)
315
+ .catch((e) =>
316
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
317
+ );
318
+ return null;
319
+ }
446
320
 
447
- // Replay handle data
448
- const handleStore = getRequestContext()?._handleStore;
449
- if (handleStore) {
450
- for (const [segId, segHandles] of Object.entries(cached.handles)) {
451
- if (Object.keys(segHandles).length > 0) {
452
- handleStore.replaySegmentData(segId, segHandles);
453
- }
321
+ // A hit serves content that was tagged at write time, so the document
322
+ // tag union must include this entry's tags for updateTag()/revalidateTag()
323
+ // to invalidate any full-page entry built on top of it. The write path
324
+ // records via cacheRoute (resolveCacheTags); the hit path records here.
325
+ recordRequestTags(cached.tags);
326
+
327
+ // Replay handle data. An empty string means the route pushed no handles —
328
+ // skip the decode entirely (the common case). Otherwise decode the
329
+ // Flight-encoded blob; a decode failure skips handle restore but keeps the
330
+ // valid cached segments.
331
+ const handleStore = _getRequestContext()?._handleStore;
332
+ if (handleStore && cached.handles) {
333
+ const handlesRecord = await decodeHandles(cached.handles);
334
+ if (handlesRecord) {
335
+ restoreHandles(handlesRecord, handleStore);
454
336
  }
455
337
  }
456
338
 
457
- const segmentTypes = segments.map((s) =>
458
- s.type === "parallel" ? s.slot : s.type
459
- );
460
- console.log(
461
- `[CacheScope] ${shouldRevalidate ? "STALE" : "HIT"}: ${key} (${segmentTypes.join(", ")})`
462
- );
339
+ if (INTERNAL_RANGO_DEBUG) {
340
+ const segmentTypes = segments.map((s) =>
341
+ s.type === "parallel" ? s.slot : s.type,
342
+ );
343
+ debugCacheLog(
344
+ `[CacheScope] ${shouldRevalidate ? "STALE" : "HIT"}: ${key} (${segmentTypes.join(", ")})`,
345
+ );
346
+ }
463
347
 
464
348
  return { segments, shouldRevalidate };
465
349
  } catch (error) {
466
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
350
+ // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
351
+ // (resolveKey). Either way degrade to a cache miss so the render proceeds.
352
+ reportCacheError(
353
+ error,
354
+ "cache-read",
355
+ `[CacheScope] lookup ${key ?? "(key resolution failed)"}`,
356
+ );
467
357
  return null;
468
358
  }
469
359
  }
470
360
 
361
+ /**
362
+ * Record this scope's segment-DSL cache({ tags }) into the request tag union
363
+ * synchronously, under the same gate cacheRoute() uses for a write.
364
+ *
365
+ * cacheRoute() already records these tags, but it is invoked inside
366
+ * requestCtx.waitUntil() by the cache-store middleware (and the proactive path
367
+ * re-resolves the whole tree before calling it), so its recording is deferred
368
+ * and RACES the document cache's post-body-drain snapshot of _requestTags. On a
369
+ * first-write (segment-cache miss) the document tag union could miss these
370
+ * tags, and updateTag()/revalidateTag() would then fail to invalidate the
371
+ * cached document until a later write reseeded it. Calling this synchronously
372
+ * in the request pipeline (before the snapshot) closes that window. Idempotent
373
+ * (the tag union is a Set), so the duplicate record in cacheRoute is harmless.
374
+ */
375
+ recordTags(requestCtx: RequestContext | undefined): void {
376
+ if (!this.enabled) return;
377
+ if (!this.conditionAllows("write")) return;
378
+ recordRequestTags(resolveCacheTags(this.config, requestCtx), requestCtx);
379
+ }
380
+
471
381
  /**
472
382
  * Cache all segments for a route (non-blocking via waitUntil)
473
383
  * Single cache entry per route request.
@@ -482,9 +392,10 @@ export class CacheScope {
482
392
  pathname: string,
483
393
  params: Record<string, string>,
484
394
  segments: ResolvedSegment[],
485
- isIntercept?: boolean
395
+ isIntercept?: boolean,
486
396
  ): Promise<void> {
487
397
  if (!this.enabled || segments.length === 0) return;
398
+ if (!this.conditionAllows("write")) return;
488
399
 
489
400
  const store = this.getStore();
490
401
  if (!store) return;
@@ -505,47 +416,102 @@ export class CacheScope {
505
416
  // Resolve cache key early (while request context is available)
506
417
  const key = await this.resolveKey(pathname, params, isIntercept);
507
418
 
419
+ // Resolve tags early (while request context is available, before waitUntil)
420
+ const tags = resolveCacheTags(this.config, requestCtx);
421
+ recordRequestTags(tags, requestCtx);
422
+
508
423
  // Check if this is a partial request (navigation) vs document request
509
- const isPartial = requestCtx.url.searchParams.has("_rsc_partial");
424
+ const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
425
+
426
+ if (INTERNAL_RANGO_DEBUG) {
427
+ debugCacheLog(
428
+ `[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
429
+ );
430
+ }
510
431
 
511
432
  requestCtx.waitUntil(async () => {
433
+ if (INTERNAL_RANGO_DEBUG) {
434
+ debugCacheLog(
435
+ `[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
436
+ );
437
+ }
438
+
512
439
  await handleStore.settled;
513
440
 
514
- // For document requests: only cache if ALL segments have components (complete render)
515
- // For partial requests: null components are expected (client already has them)
441
+ if (INTERNAL_RANGO_DEBUG) {
442
+ debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
443
+ }
444
+
445
+ // For document requests: only cache if layout segments have components
446
+ // (complete render). Parallel and route segments may legitimately have
447
+ // null components — UI-less @meta parallels return null, and void route
448
+ // handlers produce null when the UI lives in parallel slots/layouts.
449
+ // Partial requests always allow null components (client already has them).
516
450
  if (!isPartial) {
517
- const hasAllComponents = nonLoaderSegments.every(
518
- (s) => s.component !== null
451
+ const hasIncompleteLayouts = nonLoaderSegments.some(
452
+ (s) => s.component === null && s.type === "layout",
519
453
  );
520
- if (!hasAllComponents) return;
454
+ if (hasIncompleteLayouts) {
455
+ const nullSegments = nonLoaderSegments
456
+ .filter((s) => s.component === null && s.type === "layout")
457
+ .map((s) => s.id);
458
+ const error = new Error(
459
+ `[CacheScope] Cache write skipped: layout segments have null components ` +
460
+ `(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
461
+ `layout handlers must return JSX for document requests to be cacheable.`,
462
+ );
463
+ error.name = "CacheScopeInvariantError";
464
+ console.error(error.message);
465
+ return;
466
+ }
521
467
  }
522
468
 
523
469
  // Collect handle data for non-loader segments only
524
- const handles: Record<string, SegmentHandleData> = {};
525
- for (const seg of nonLoaderSegments) {
526
- handles[seg.id] = handleStore.getDataForSegment(seg.id);
527
- }
470
+ const handles = captureHandles(nonLoaderSegments, handleStore);
528
471
 
529
472
  try {
530
- // Serialize non-loader segments only
531
- const serializedSegments = await serializeSegments(nonLoaderSegments);
473
+ if (INTERNAL_RANGO_DEBUG) {
474
+ debugCacheLog(
475
+ `[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
476
+ );
477
+ }
478
+
479
+ // Serialize segments and Flight-encode handles in parallel. Handles go
480
+ // through the codec (not raw into the entry) so Promise/ReactNode handle
481
+ // values survive a JSON-serializing store — see encodeHandles.
482
+ const { serializeSegments } = await import("./segment-codec.js");
483
+ const [serializedSegments, encodedHandles] = await Promise.all([
484
+ serializeSegments(nonLoaderSegments),
485
+ encodeHandles(handles),
486
+ ]);
532
487
 
533
488
  const data: CachedEntryData = {
534
489
  segments: serializedSegments,
535
- handles,
490
+ handles: encodedHandles,
536
491
  expiresAt: Date.now() + ttl * 1000,
492
+ tags,
537
493
  };
538
494
 
495
+ if (INTERNAL_RANGO_DEBUG) {
496
+ debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
497
+ }
498
+
539
499
  await store.set(key, data, ttl, swr);
540
500
 
541
- const segmentTypes = nonLoaderSegments.map((s) =>
542
- s.type === "parallel" ? s.slot : s.type
543
- );
544
- console.log(
545
- `[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`
546
- );
501
+ if (INTERNAL_RANGO_DEBUG) {
502
+ const segmentTypes = nonLoaderSegments.map((s) =>
503
+ s.type === "parallel" ? s.slot : s.type,
504
+ );
505
+ debugCacheLog(
506
+ `[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`,
507
+ );
508
+ }
547
509
  } catch (error) {
548
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
510
+ reportCacheError(
511
+ error,
512
+ "cache-write",
513
+ `[CacheScope] Failed to cache ${key}`,
514
+ );
549
515
  }
550
516
  });
551
517
  }
@@ -556,7 +522,7 @@ export class CacheScope {
556
522
  */
557
523
  export function createCacheScope(
558
524
  config: { options: PartialCacheOptions | false } | undefined,
559
- parent: CacheScope | null = null
525
+ parent: CacheScope | null = null,
560
526
  ): CacheScope | null {
561
527
  if (!config) return parent; // No config, inherit parent
562
528
  return new CacheScope(config.options, parent);