@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
@@ -0,0 +1,386 @@
1
+ /**
2
+ * PPR Shell Cache Middleware
3
+ *
4
+ * Axis 2 of the two-axis render model (see docs/design/ppr-shell-resume.md).
5
+ * Opt-in middleware that caches the rendered HTML *shell* (React's `prelude`
6
+ * plus the `postponed` state from a static `prerender` abort) and, on a later
7
+ * request, serves those bytes immediately and resumes fizz for just the live
8
+ * holes. The browser sees one ordinary streamed document.
9
+ *
10
+ * The middleware owns only the cheap URL/method gating and the stream
11
+ * composition. The RENDER layer (rsc-rendering, integrated by a later stage) is
12
+ * the final authority on whether a resume actually happens: it reads
13
+ * requestCtx._shellResume, calls the resume strategy, and marks the response
14
+ * with the internal `x-rango-shell-resumed` header ONLY when it truly resumed.
15
+ * This middleware composes the cached prelude in front of the live response ONLY
16
+ * when that marker is present; everything else (redirects, 404s, error renders,
17
+ * nonce/allReady bypasses) flows through untouched — every non-resumed path
18
+ * fails open to axis 1.
19
+ *
20
+ * Flow (the middleware calls next() EXACTLY ONCE on every path — the executor's
21
+ * per-entry next() is a single-use latch, so a second call throws):
22
+ * 1. Bypass matrix (non-GET, _rsc_* params, RSC request, skipPaths, isEnabled,
23
+ * store lacks the shell family) → plain next(), axis 1.
24
+ * 2. getShell(key) HIT + reactVersion matches → arm _shellResume, await next()
25
+ * once. On a stale (SWR) hit, ALSO set the _shellCapture descriptor before
26
+ * that same next() so the render layer schedules a background recapture.
27
+ * - marker present → strip it, prepend prelude bytes, x-rango-shell: HIT.
28
+ * - marker absent → render layer did not resume; return untouched.
29
+ * 3. MISS (or reactVersion mismatch) → set the _shellCapture descriptor before
30
+ * the single next(), stream the live response to the user with
31
+ * x-rango-shell: MISS. The render layer reads the descriptor after building the
32
+ * response and schedules a BACKGROUND capture (via router.match under a derived
33
+ * context — NOT a second next()); see rsc-rendering.ts + shell-capture.ts.
34
+ * The descriptor is cleared in a finally so it never leaks into a reused ctx.
35
+ */
36
+
37
+ import React from "react";
38
+ import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
39
+ import {
40
+ getRequestContext,
41
+ type RequestContext,
42
+ } from "../server/request-context.js";
43
+ import { mayNeedSSR } from "../rsc/ssr-setup.js";
44
+ import type { SegmentCacheStore } from "./types.js";
45
+ import { sortedSearchString } from "./cache-key-utils.js";
46
+ import { reportCacheError } from "./cache-error.js";
47
+
48
+ /** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
49
+ const SHELL_STATUS_HEADER = "x-rango-shell";
50
+
51
+ /**
52
+ * Internal marker the render layer sets on the live response when — and only
53
+ * when — it actually resumed a cached shell. The middleware composes the prelude
54
+ * in front of the body iff this header is present, then strips it before the
55
+ * response leaves. It is the whole handshake between the middleware (which arms
56
+ * _shellResume optimistically) and the render layer (the final authority).
57
+ */
58
+ const SHELL_RESUMED_MARKER_HEADER = "x-rango-shell-resumed";
59
+
60
+ /**
61
+ * React version captured at prerender time is the invalidation gate: a stored
62
+ * shell whose reactVersion differs from the running React cannot be resumed (the
63
+ * postponed blob is build-coupled), so it is treated as a miss. Read once at
64
+ * module load — React.version is stable for the process lifetime.
65
+ */
66
+ const REACT_VERSION = React.version;
67
+
68
+ /** Decode a base64 prelude back into bytes for stream composition. */
69
+ function base64ToBytes(b64: string): Uint8Array {
70
+ const binary = atob(b64);
71
+ const bytes = new Uint8Array(binary.length);
72
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
73
+ return bytes;
74
+ }
75
+
76
+ /**
77
+ * Compose the served response: prelude bytes first, then the live (resumed) body.
78
+ * React relies on HTML-parser foster-parenting for content streamed after the
79
+ * prelude's closing `</body></html>`, so plain byte concatenation is the correct
80
+ * composition (POC item 6) — do not try to reopen or splice the document.
81
+ *
82
+ * Status and headers come from the LIVE next() response: Set-Cookie and friends
83
+ * are per-request and belong to the live pass, not the frozen shell. The internal
84
+ * marker is stripped and x-rango-shell: HIT is added.
85
+ */
86
+ function composeShellResponse(
87
+ response: Response,
88
+ preludeBase64: string,
89
+ ): Response {
90
+ const preludeBytes = base64ToBytes(preludeBase64);
91
+ const body = response.body;
92
+ const composed = new ReadableStream<Uint8Array>({
93
+ async start(controller) {
94
+ controller.enqueue(preludeBytes);
95
+ if (body) {
96
+ const reader = body.getReader();
97
+ try {
98
+ for (;;) {
99
+ const { done, value } = await reader.read();
100
+ if (done) break;
101
+ controller.enqueue(value);
102
+ }
103
+ } finally {
104
+ reader.releaseLock();
105
+ }
106
+ }
107
+ controller.close();
108
+ },
109
+ cancel(reason) {
110
+ // Propagate downstream cancellation to the live body so it does not leak.
111
+ return body?.cancel(reason);
112
+ },
113
+ });
114
+
115
+ const headers = new Headers(response.headers);
116
+ headers.delete(SHELL_RESUMED_MARKER_HEADER);
117
+ headers.set(SHELL_STATUS_HEADER, "HIT");
118
+ return new Response(composed, {
119
+ status: response.status,
120
+ statusText: response.statusText,
121
+ headers,
122
+ });
123
+ }
124
+
125
+ /** Clone a response with the x-rango-shell status header added. */
126
+ function withShellStatus(response: Response, status: "HIT" | "MISS"): Response {
127
+ const headers = new Headers(response.headers);
128
+ headers.set(SHELL_STATUS_HEADER, status);
129
+ return new Response(response.body, {
130
+ status: response.status,
131
+ statusText: response.statusText,
132
+ headers,
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Options for the PPR shell-cache middleware.
138
+ */
139
+ export interface ShellCacheOptions<TEnv = any> {
140
+ /**
141
+ * Cache store to use. Defaults to the request context's `_cacheStore`
142
+ * (the app-level store wired via the router's cache config).
143
+ */
144
+ store?: SegmentCacheStore<TEnv>;
145
+
146
+ /**
147
+ * Shell time-to-live in seconds. Defaults to 300.
148
+ */
149
+ ttlSeconds?: number;
150
+
151
+ /**
152
+ * Stale-while-revalidate window in seconds. On a stale hit the cached shell is
153
+ * still served and a background recapture is scheduled.
154
+ */
155
+ swrSeconds?: number;
156
+
157
+ /**
158
+ * Custom cache key generator. Receives the cleaned request URL. The middleware
159
+ * appends its own `:shell` namespace suffix, so the returned key never collides
160
+ * with a document-cache key.
161
+ *
162
+ * A custom generator owns the FULL key identity, including host scoping: the
163
+ * default key incorporates `url.host` so shells can never leak across tenants
164
+ * in multi-host deployments — include it in custom keys too unless the store
165
+ * is provably single-host.
166
+ */
167
+ keyGenerator?: (url: URL) => string;
168
+
169
+ /**
170
+ * Callback to decide whether shell caching is enabled for this request.
171
+ * Return false to fall through to a normal HTML render (axis 1).
172
+ */
173
+ isEnabled?: (ctx: MiddlewareContext<TEnv>) => boolean | Promise<boolean>;
174
+
175
+ /**
176
+ * Skip shell caching for specific path prefixes (e.g. admin, API routes).
177
+ */
178
+ skipPaths?: string[];
179
+
180
+ /**
181
+ * Enable debug logging for shell cache operations (HIT / MISS / CAPTURED).
182
+ * Defaults to false.
183
+ */
184
+ debug?: boolean;
185
+ }
186
+
187
+ /**
188
+ * Create the PPR shell-cache middleware.
189
+ *
190
+ * Add it to a router (or a route subtree) to cache the HTML shell and resume
191
+ * fizz for just the live holes on subsequent requests. Personalization must live
192
+ * in loaders/holes — the shell is shared per URL key (the shell-manifest
193
+ * pattern). Actions, progressive enhancement, formState, and per-request nonce
194
+ * always take axis 1.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const router = createRouter<AppEnv>()
199
+ * .use(createShellCacheMiddleware({ ttlSeconds: 600, swrSeconds: 60 }))
200
+ * .route("home", (ctx) => <HomePage />);
201
+ * ```
202
+ */
203
+ export function createShellCacheMiddleware<TEnv = any>(
204
+ options: ShellCacheOptions<TEnv> = {},
205
+ ): MiddlewareFn<TEnv> {
206
+ const {
207
+ ttlSeconds = 300,
208
+ swrSeconds,
209
+ keyGenerator,
210
+ isEnabled,
211
+ skipPaths = [],
212
+ debug = false,
213
+ } = options;
214
+
215
+ const log = debug ? (message: string) => console.log(message) : () => {};
216
+
217
+ return async function shellCacheMiddleware(
218
+ ctx: MiddlewareContext<TEnv>,
219
+ next: () => Promise<Response>,
220
+ ): Promise<Response> {
221
+ const url = ctx.url;
222
+ // ctx.url is stripped of _rsc_* params by the pipeline (stripInternalParams);
223
+ // read the raw request URL for internal-param detection, like document-cache.
224
+ const rawUrl = new URL(ctx.request.url);
225
+
226
+ // --- Bypass matrix (each bypass = plain next(), axis 1) ---
227
+
228
+ // Mutations are dynamic — never resume/capture a shell for them.
229
+ if (ctx.request.method !== "GET") return next();
230
+ // RSC action / loader / partial requests are not HTML document requests.
231
+ if (rawUrl.searchParams.has("_rsc_action")) return next();
232
+ if (rawUrl.searchParams.has("_rsc_loader")) return next();
233
+ if (rawUrl.searchParams.has("_rsc_partial")) return next();
234
+ // RSC (Flight) request — the Flight path is untouched by PPR.
235
+ if (!mayNeedSSR(ctx.request, rawUrl)) return next();
236
+ // Consumer opt-outs.
237
+ if (skipPaths.some((path) => url.pathname.startsWith(path))) return next();
238
+ if (isEnabled) {
239
+ const enabled = await isEnabled(ctx);
240
+ if (!enabled) return next();
241
+ }
242
+
243
+ const requestCtx = getRequestContext();
244
+ const store = options.store ?? requestCtx?._cacheStore;
245
+
246
+ // Store must implement the shell family — otherwise fail open to axis 1.
247
+ if (!store?.getShell || !store?.putShell) return next();
248
+
249
+ // Track whether next() has been called so the catch block knows whether it is
250
+ // safe to fall through to the handler (mirrors document-cache). cacheKey is
251
+ // assigned inside the try (a throwing keyGenerator must degrade, not 500).
252
+ let handlerCalled = false;
253
+ let cacheKey = "";
254
+
255
+ /**
256
+ * Build the "capture wanted" descriptor. Set on the request context BEFORE
257
+ * the single next() so the render layer can read it after building the
258
+ * response and schedule the background capture (via router.match under a
259
+ * derived context — NOT a second next()). `store` is the SAME store this
260
+ * middleware resolved for getShell, so a store-attached middleware writes
261
+ * captures where it reads them. `tags` is intentionally omitted: the capture
262
+ * collects the shell's own non-loader tags from its derived render.
263
+ */
264
+ const captureDescriptor = (): NonNullable<
265
+ RequestContext["_shellCapture"]
266
+ > => ({
267
+ key: cacheKey,
268
+ ttl: ttlSeconds,
269
+ swr: swrSeconds,
270
+ store,
271
+ });
272
+
273
+ try {
274
+ // Namespace the key with a `:shell` suffix (mirrors document-cache's
275
+ // `:html`/`:rsc` suffix) so it can never collide with a document-cache key;
276
+ // the store further isolates the shell family internally. Built inside the
277
+ // try so a throwing keyGenerator degrades to a full render, not a 500.
278
+ //
279
+ // The default key includes the request HOST: in a multi-tenant host-router
280
+ // deployment (one worker, one shared KV/runtime-cache store) a host-less
281
+ // key would serve tenant A's captured shell to tenant B's users. The CF
282
+ // document family fixed this exact class at the store tier (toDocKVHost);
283
+ // the shell family fixes it at the key tier so every store is safe.
284
+ let searchSuffix = "";
285
+ if (!keyGenerator) {
286
+ const sorted = sortedSearchString(url.searchParams);
287
+ if (sorted) searchSuffix = `?${sorted}`;
288
+ }
289
+ cacheKey = keyGenerator
290
+ ? `${keyGenerator(url)}:shell`
291
+ : `${url.host}${url.pathname}${searchSuffix}:shell`;
292
+
293
+ const cached = await store.getShell(cacheKey);
294
+ const validHit =
295
+ cached != null && cached.entry.reactVersion === REACT_VERSION;
296
+
297
+ if (cached && !validHit) {
298
+ // reactVersion mismatch: the postponed blob is build-coupled and cannot
299
+ // be resumed by the running React. There is no deleteShell primitive in
300
+ // v1, so we simply treat it as a MISS — the recapture below overwrites
301
+ // the same key, and the entry otherwise ages out via TTL.
302
+ log(
303
+ `[ShellCache] MISS ${url.pathname} (reactVersion ${cached.entry.reactVersion} != ${REACT_VERSION})`,
304
+ );
305
+ }
306
+
307
+ if (validHit) {
308
+ // Arm the resume optimistically. The render layer is the final authority
309
+ // (nonce/formState/allReady bypass); it engages resume and marks the
310
+ // response only when it actually resumed.
311
+ if (requestCtx) {
312
+ requestCtx._shellResume = { postponed: cached!.entry.postponed };
313
+ // SWR: on a stale hit, also request a background recapture by setting
314
+ // the descriptor before this single next(). Resume (foreground) and
315
+ // capture-request (background) legitimately coexist now — the render
316
+ // layer schedules the recapture off the descriptor after building the
317
+ // resumed response. A fresh hit leaves the descriptor unset.
318
+ if (cached!.shouldRevalidate) {
319
+ requestCtx._shellCapture = captureDescriptor();
320
+ }
321
+ }
322
+ handlerCalled = true;
323
+ let response: Response;
324
+ try {
325
+ response = await next();
326
+ } finally {
327
+ // Always disarm both single-request flags. A next() throw here (resume
328
+ // failure) propagates to the outer catch and rethrows; the
329
+ // version-keyed entry self-heals via axis 1 + recapture on the next
330
+ // request (v1 has no deleteShell to eagerly remove it).
331
+ if (requestCtx) {
332
+ requestCtx._shellResume = undefined;
333
+ requestCtx._shellCapture = undefined;
334
+ }
335
+ }
336
+
337
+ if (response.headers.has(SHELL_RESUMED_MARKER_HEADER)) {
338
+ log(`[ShellCache] HIT ${url.pathname}`);
339
+ return composeShellResponse(response, cached!.entry.prelude);
340
+ }
341
+
342
+ // Marker absent: the render layer did NOT resume (redirect, 404, error
343
+ // render, per-request nonce, or allReady buffering). Fail open to axis 1
344
+ // — return the live response untouched. Note: no manual onResponse-
345
+ // callback drain is needed here (or on any path in this middleware),
346
+ // because every path runs a full next() pipeline pass, which already
347
+ // drains those callbacks — unlike document-cache, which serves a fully
348
+ // cached response bypassing next() and must drain them itself.
349
+ log(`[ShellCache] PASS ${url.pathname} (resume not engaged)`);
350
+ return response;
351
+ }
352
+
353
+ // --- MISS (no entry, or reactVersion mismatch) ---
354
+ // Set the "capture wanted" descriptor before the single next(). The render
355
+ // layer reads it after building the response and, if the response is a
356
+ // servable 200 HTML document, schedules a BACKGROUND capture (router.match
357
+ // under a derived context — never a second next()). The descriptor's mere
358
+ // presence does not change the foreground render (loader masking keys off
359
+ // _shellCaptureRun, which only the background derived context sets).
360
+ if (requestCtx) requestCtx._shellCapture = captureDescriptor();
361
+ handlerCalled = true;
362
+ let response: Response;
363
+ try {
364
+ response = await next();
365
+ } finally {
366
+ // Clear the descriptor so it never leaks into a reused ctx. The render
367
+ // layer already read it (synchronously, inside next()) and captured its
368
+ // own reference for the background task, so clearing here is safe.
369
+ if (requestCtx) requestCtx._shellCapture = undefined;
370
+ }
371
+
372
+ log(`[ShellCache] MISS ${url.pathname}`);
373
+ return withShellStatus(response, "MISS");
374
+ } catch (error) {
375
+ reportCacheError(error, "cache-read", "[ShellCache] middleware");
376
+ if (handlerCalled) {
377
+ // Post-handler failure (resume/render throw, or a stream error): do not
378
+ // call next() again — that would re-run handler side effects.
379
+ throw error;
380
+ }
381
+ // Pre-handler failure (cache lookup / key generation): degrade to a full
382
+ // render.
383
+ return next();
384
+ }
385
+ };
386
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Cache Tag Invalidation API
3
+ *
4
+ * Two on-demand invalidation verbs, mirroring the distinction popularized by
5
+ * Next.js so consumers can pick the right consistency model:
6
+ *
7
+ * - updateTag(...tags): read-your-own-writes. Awaitable - resolves only after
8
+ * in-process invalidation across every configured store completes. Use in a
9
+ * Server Action and `await` it before the action re-renders, so the action's
10
+ * own response reflects the mutation.
11
+ *
12
+ * - revalidateTag(...tags): fire-and-forget via waitUntil - the response is not
13
+ * blocked. Use in Route Handlers / webhooks. NOTE: both verbs hard-purge; the
14
+ * only difference is awaitability. revalidateTag does NOT serve stale content -
15
+ * the next read after the invalidation lands is a hard miss that re-renders.
16
+ * (The name mirrors Next.js, where it is SWR; here it is background-purge.)
17
+ *
18
+ * Both fan out across the app-level store (ctx._cacheStore) and every explicit
19
+ * per-scope store from cache({ store }) registered for this handler
20
+ * (ctx._explicitTaggedStores), calling the store-level invalidateTags()
21
+ * primitive for each tag. A single configured store (the common case) owns its
22
+ * own tag index and distributed invalidation - there is no separate
23
+ * tag-invalidation store.
24
+ */
25
+
26
+ import { _getRequestContext } from "../server/request-context.js";
27
+ import { reportingAsync } from "./cache-error.js";
28
+ import { normalizeTags } from "./cache-tag.js";
29
+ import type { SegmentCacheStore } from "./types.js";
30
+
31
+ /**
32
+ * Collect every store that may hold entries tagged for this request's handler:
33
+ * the app-level store plus all explicit per-scope stores (deduplicated). Splits
34
+ * them into tag-capable (implement invalidateTags()) and not, so callers can
35
+ * warn about configured stores whose tagged entries will NOT be invalidated.
36
+ *
37
+ * `hasContext` reports whether an ALS request context existed at all. Without one
38
+ * (e.g. a queue consumer or cron job calling updateTag/revalidateTag) no stores
39
+ * are reachable, and the empty-capable case is a missing-context problem, not a
40
+ * store-config problem - callers branch on this to warn about the right cause.
41
+ */
42
+ function collectStores(): {
43
+ capable: SegmentCacheStore[];
44
+ incapable: number;
45
+ hasContext: boolean;
46
+ } {
47
+ const ctx = _getRequestContext();
48
+ const stores = new Set<SegmentCacheStore>();
49
+ if (ctx?._cacheStore) stores.add(ctx._cacheStore);
50
+ if (ctx?._explicitTaggedStores) {
51
+ for (const store of ctx._explicitTaggedStores) stores.add(store);
52
+ }
53
+ const capable: SegmentCacheStore[] = [];
54
+ let incapable = 0;
55
+ for (const store of stores) {
56
+ if (typeof store.invalidateTags === "function") capable.push(store);
57
+ else incapable++;
58
+ }
59
+ return { capable, incapable, hasContext: ctx != null };
60
+ }
61
+
62
+ /**
63
+ * Production-visible warning. A misconfigured store silently dropping
64
+ * invalidations is a data-correctness footgun, so this surfaces in every
65
+ * environment (not dev-only).
66
+ */
67
+ function warnNoTagStore(fn: string, tags: string[]): void {
68
+ console.warn(
69
+ `[${fn}] No tag-capable cache store is configured; tags ` +
70
+ `[${tags.join(", ")}] were not invalidated. The configured store must ` +
71
+ `implement invalidateTags() (the built-in MemorySegmentCacheStore and ` +
72
+ `CFCacheStore do).`,
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Production-visible warning for the no-request-context case. Distinct from
78
+ * warnNoTagStore: the stores are not unreachable because they are misconfigured,
79
+ * but because there is no ALS request context to reach them through (e.g. a queue
80
+ * consumer or scheduled job). Naming the real cause keeps consumers from chasing
81
+ * a store-config red herring.
82
+ */
83
+ function warnNoRequestContext(fn: string, tags: string[]): void {
84
+ console.warn(
85
+ `[${fn}] Called outside a request context (e.g. from a queue consumer or ` +
86
+ `scheduled job); no cache stores are reachable and tags ` +
87
+ `[${tags.join(", ")}] were not invalidated. Invoke it within a request ` +
88
+ `(Server Action or route handler).`,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Production-visible warning for mixed-store configs: at least one configured
94
+ * store does not support tag invalidation, so its tagged entries (if any) are
95
+ * left stale even though other stores were invalidated.
96
+ */
97
+ function warnPartialTagStore(fn: string, incapable: number): void {
98
+ console.warn(
99
+ `[${fn}] ${incapable} configured cache store(s) do not implement ` +
100
+ `invalidateTags(); their tagged entries were NOT invalidated. Use a ` +
101
+ `tag-capable store (e.g. MemorySegmentCacheStore / CFCacheStore) for any ` +
102
+ `cache({ store }) boundary whose entries you invalidate by tag.`,
103
+ );
104
+ }
105
+
106
+ async function invalidateAcross(
107
+ stores: SegmentCacheStore[],
108
+ tags: string[],
109
+ ): Promise<void> {
110
+ // One invalidateTags(tags) call per store: the store receives the whole tag
111
+ // batch so it can do a single CDN purge request rather than one per tag.
112
+ //
113
+ // allSettled, not all: a store's invalidateTags() can reject (e.g. CFCacheStore
114
+ // surfaces a failed durable KV marker write). With Promise.all, the first
115
+ // rejection would short-circuit and the other stores' outcomes would go
116
+ // unobserved. Attempt every store, then surface a combined error so an awaited
117
+ // updateTag() still rejects (read-your-own-writes honesty) without masking the
118
+ // stores that did succeed.
119
+ const results = await Promise.allSettled(
120
+ stores.map((store) => store.invalidateTags!(tags)),
121
+ );
122
+ const errors = results
123
+ .filter((r): r is PromiseRejectedResult => r.status === "rejected")
124
+ .map((r) => r.reason);
125
+ if (errors.length > 0) {
126
+ const err = new Error(
127
+ `[tag invalidation] ${errors.length}/${stores.length} store(s) failed to ` +
128
+ `invalidate tags [${tags.join(", ")}]; their entries may still serve ` +
129
+ `stale data. Retry the invalidation.`,
130
+ );
131
+ (err as Error & { cause?: unknown }).cause = errors[0];
132
+ throw err;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Immediately expire every cache entry tagged with any of `tags`, resolving
138
+ * once in-process invalidation across all configured stores completes.
139
+ *
140
+ * Read-your-own-writes: because the returned promise resolves before you return
141
+ * from a Server Action, awaiting it guarantees the action's own re-render (and
142
+ * any subsequent read) sees fresh data.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * async function updateProduct(formData: FormData) {
147
+ * "use server";
148
+ * await db.updateProduct(formData);
149
+ * await updateTag("products"); // next render is fresh
150
+ * }
151
+ * ```
152
+ */
153
+ export async function updateTag(...tags: string[]): Promise<void> {
154
+ const valid = normalizeTags(tags);
155
+ if (valid.length === 0) return;
156
+
157
+ const { capable, incapable, hasContext } = collectStores();
158
+ if (capable.length === 0) {
159
+ if (hasContext) warnNoTagStore("updateTag", valid);
160
+ else warnNoRequestContext("updateTag", valid);
161
+ return;
162
+ }
163
+ if (incapable > 0) warnPartialTagStore("updateTag", incapable);
164
+
165
+ await invalidateAcross(capable, valid);
166
+ }
167
+
168
+ /**
169
+ * Invalidate every cache entry tagged with any of `tags` in the background,
170
+ * without blocking the current response (fire-and-forget via waitUntil).
171
+ *
172
+ * This is NOT stale-while-revalidate: like updateTag() it hard-purges, so the
173
+ * next read after the invalidation lands is a miss that re-renders fresh. The
174
+ * only difference from updateTag() is awaitability - revalidateTag() defers the
175
+ * purge off the response path and is not awaited.
176
+ *
177
+ * Use in Route Handlers / webhooks. For read-your-own-writes inside a Server
178
+ * Action, use updateTag() instead so the action's own response is fresh.
179
+ *
180
+ * Fire-and-forget: because this returns void and runs in the background, a
181
+ * failed durable marker write (e.g. a transient KV outage) is NOT surfaced to
182
+ * the caller. It IS reported - logged loudly and routed through the router's
183
+ * `onError` callback (phase `cache`, `metadata.category === "cache-invalidate"`)
184
+ * via reportingAsync - so the failure is observable in telemetry even though it
185
+ * cannot be awaited. If you need the invalidation to be CONFIRMED (and to retry
186
+ * on failure), use `await updateTag()` instead, which rejects when a store's
187
+ * durable write fails.
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * // route handler invoked by an external webhook
192
+ * export async function POST() {
193
+ * "use server";
194
+ * revalidateTag("products");
195
+ * return new Response("ok");
196
+ * }
197
+ * ```
198
+ */
199
+ export function revalidateTag(...tags: string[]): void {
200
+ const valid = normalizeTags(tags);
201
+ if (valid.length === 0) return;
202
+
203
+ const { capable, incapable, hasContext } = collectStores();
204
+ if (capable.length === 0) {
205
+ if (hasContext) warnNoTagStore("revalidateTag", valid);
206
+ else warnNoRequestContext("revalidateTag", valid);
207
+ return;
208
+ }
209
+ if (incapable > 0) warnPartialTagStore("revalidateTag", incapable);
210
+
211
+ const ctx = _getRequestContext();
212
+ // reportingAsync never rejects: it catches a failed durable write and routes
213
+ // it through reportCacheError (loud log + onError). This is the only place a
214
+ // revalidateTag failure can be observed, since it is not awaitable. Pass ctx
215
+ // explicitly - the run executes in a detached waitUntil where the ALS context
216
+ // is gone, so onError fires only if we hand it the captured context.
217
+ const run = () =>
218
+ reportingAsync(
219
+ () => invalidateAcross(capable, valid),
220
+ "cache-invalidate",
221
+ "[revalidateTag] background invalidation",
222
+ ctx,
223
+ );
224
+ if (ctx?.waitUntil) {
225
+ ctx.waitUntil(run);
226
+ } else {
227
+ // No request context (e.g. called outside ALS): best-effort background run.
228
+ void run();
229
+ }
230
+ }