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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -3,7 +3,10 @@ import ts from "typescript";
3
3
  import { extractParamsFromPattern } from "./param-extraction.js";
4
4
  import { extractRoutesFromSource } from "./ast-route-extraction.js";
5
5
  import { generatePerModuleTypesSource } from "./codegen.js";
6
- import { buildCombinedRouteMapWithSearch } from "./include-resolution.js";
6
+ import {
7
+ buildCombinedRouteMapWithSearch,
8
+ createScanMemo,
9
+ } from "./include-resolution.js";
7
10
  import type { ScanFilter } from "./scan-filter.js";
8
11
  import { findTsFiles } from "./scan-filter.js";
9
12
 
@@ -78,10 +81,20 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
78
81
  if (varNames.length > 0) {
79
82
  // Follow includes recursively via the combined route map builder.
80
83
  // The visited set in buildCombinedRouteMapWithSearch prevents infinite loops.
84
+ // Share one per-file scan memo across all urls() variables so a shared
85
+ // include target is read+parsed once for this file, not once per variable.
81
86
  routes = [];
87
+ const memo = createScanMemo();
82
88
  for (const varName of varNames) {
83
89
  const { routes: routeMap, searchSchemas } =
84
- buildCombinedRouteMapWithSearch(filePath, varName);
90
+ buildCombinedRouteMapWithSearch(
91
+ filePath,
92
+ varName,
93
+ undefined,
94
+ undefined,
95
+ undefined,
96
+ memo,
97
+ );
85
98
  for (const [name, pattern] of Object.entries(routeMap)) {
86
99
  const params = extractParamsFromPattern(pattern);
87
100
  routes.push({
@@ -54,8 +54,33 @@ function isRoutableSourceFile(name: string): boolean {
54
54
  );
55
55
  }
56
56
 
57
- function findRouterFilesRecursive(
57
+ function isExcludedScanDir(name: string): boolean {
58
+ return (
59
+ name === "node_modules" ||
60
+ name === "dist" ||
61
+ name === "coverage" ||
62
+ name === "__tests__" ||
63
+ name === "__mocks__" ||
64
+ name.startsWith(".")
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Recursively collect source files whose code contains `pattern` (a comment- or
70
+ * string-only mention is ignored via firstCodeMatchIndex). Shared by createRouter
71
+ * and createHostRouter discovery, which differ only in the call pattern and
72
+ * `stopAtMatchDir`: createRouter treats a directory containing a match as a router
73
+ * root and stops descending it; createHostRouter descends the whole tree (the host
74
+ * entry sits above the sub-app router roots).
75
+ *
76
+ * `pattern` is the non-global tester (no lastIndex state); `patternG` is its global
77
+ * twin for the code-region scan.
78
+ */
79
+ function findCallSiteFilesRecursive(
58
80
  dir: string,
81
+ pattern: RegExp,
82
+ patternG: RegExp,
83
+ stopAtMatchDir: boolean,
59
84
  filter: ScanFilter | undefined,
60
85
  results: string[],
61
86
  ): void {
@@ -70,21 +95,12 @@ function findRouterFilesRecursive(
70
95
  }
71
96
 
72
97
  const childDirs: string[] = [];
73
- const routerFilesInDir: string[] = [];
98
+ const matchesInDir: string[] = [];
74
99
 
75
100
  for (const entry of entries) {
76
101
  const fullPath = join(dir, entry.name);
77
102
  if (entry.isDirectory()) {
78
- if (
79
- entry.name === "node_modules" ||
80
- entry.name === "dist" ||
81
- entry.name === "coverage" ||
82
- entry.name === "__tests__" ||
83
- entry.name === "__mocks__" ||
84
- entry.name.startsWith(".")
85
- )
86
- continue;
87
- childDirs.push(fullPath);
103
+ if (!isExcludedScanDir(entry.name)) childDirs.push(fullPath);
88
104
  continue;
89
105
  }
90
106
 
@@ -100,26 +116,29 @@ function findRouterFilesRecursive(
100
116
  // so a mention inside a comment or string is not mistaken for a real
101
117
  // router file (which previously triggered a spurious "Multiple routers
102
118
  // found" error).
103
- if (
104
- ROUTER_CALL_PATTERN.test(source) &&
105
- firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0
106
- ) {
107
- routerFilesInDir.push(fullPath);
119
+ if (pattern.test(source) && firstCodeMatchIndex(source, patternG) >= 0) {
120
+ matchesInDir.push(fullPath);
108
121
  }
109
122
  } catch {
110
123
  continue;
111
124
  }
112
125
  }
113
126
 
114
- // A directory that contains a router file is treated as a router root.
115
- // Once found, deeper directories are skipped to avoid redundant scans.
116
- if (routerFilesInDir.length > 0) {
117
- results.push(...routerFilesInDir);
118
- return;
119
- }
120
-
121
- for (const childDir of childDirs) {
122
- findRouterFilesRecursive(childDir, filter, results);
127
+ results.push(...matchesInDir);
128
+
129
+ // createRouter (stopAtMatchDir): a directory that contains a match is a router
130
+ // root, so deeper directories are skipped. createHostRouter: always descend.
131
+ if (!stopAtMatchDir || matchesInDir.length === 0) {
132
+ for (const childDir of childDirs) {
133
+ findCallSiteFilesRecursive(
134
+ childDir,
135
+ pattern,
136
+ patternG,
137
+ stopAtMatchDir,
138
+ filter,
139
+ results,
140
+ );
141
+ }
123
142
  }
124
143
  }
125
144
 
@@ -322,12 +341,6 @@ export function extractBasenameFromRouter(code: string): string | undefined {
322
341
  return result;
323
342
  }
324
343
 
325
- /** @deprecated Use extractUrlsFromRouter instead */
326
- export function extractUrlsVariableFromRouter(code: string): string | null {
327
- const result = extractUrlsFromRouter(code);
328
- return result?.kind === "variable" ? result.name : null;
329
- }
330
-
331
344
  /** Apply a basename prefix to all route patterns in a result set. */
332
345
  function applyBasenameToRoutes(
333
346
  result: {
@@ -558,7 +571,38 @@ export function detectUnresolvableIncludesForUrlsFile(
558
571
  */
559
572
  export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
560
573
  const result: string[] = [];
561
- findRouterFilesRecursive(root, filter, result);
574
+ findCallSiteFilesRecursive(
575
+ root,
576
+ ROUTER_CALL_PATTERN,
577
+ ROUTER_CALL_PATTERN_G,
578
+ true,
579
+ filter,
580
+ result,
581
+ );
582
+ return result;
583
+ }
584
+
585
+ const HOST_ROUTER_CALL_PATTERN = /\bcreateHostRouter\s*[<(]/;
586
+ const HOST_ROUTER_CALL_PATTERN_G = /\bcreateHostRouter\s*[<(]/g;
587
+
588
+ /**
589
+ * Scan for files containing createHostRouter() and return their paths. Unlike
590
+ * findRouterFiles, this does NOT stop at the first router-root directory -- a host
591
+ * entry typically sits above the sub-app router roots, so the whole tree is scanned.
592
+ */
593
+ export function findHostRouterFiles(
594
+ root: string,
595
+ filter?: ScanFilter,
596
+ ): string[] {
597
+ const result: string[] = [];
598
+ findCallSiteFilesRecursive(
599
+ root,
600
+ HOST_ROUTER_CALL_PATTERN,
601
+ HOST_ROUTER_CALL_PATTERN_G,
602
+ false,
603
+ filter,
604
+ result,
605
+ );
562
606
  return result;
563
607
  }
564
608
 
@@ -617,9 +661,6 @@ export function writeCombinedRouteTypes(
617
661
  ? readFileSync(outPath, "utf-8")
618
662
  : null;
619
663
 
620
- // When the static parser can't extract routes (e.g. callback-style urls()),
621
- // write an empty placeholder so the build-time transform's injected import
622
- // resolves. Runtime discovery will overwrite this with the real routes.
623
664
  if (Object.keys(result.routes).length === 0) {
624
665
  if (!existing) {
625
666
  const emptySource = generateRouteTypesSource({});
@@ -635,11 +676,6 @@ export function writeCombinedRouteTypes(
635
676
  hasSearchSchemas ? result.searchSchemas : undefined,
636
677
  );
637
678
  if (existing !== source) {
638
- // On initial dev startup, don't overwrite a file from runtime discovery
639
- // (which has all dynamic routes) with a smaller set from the static
640
- // parser. The static parser can't see routes generated by Array.from()
641
- // or other dynamic code. During HMR (file watcher), always write so
642
- // newly added routes appear immediately.
643
679
  if (opts?.preserveIfLarger && existing) {
644
680
  const existingCount = countPublicRouteEntries(existing);
645
681
  const newCount = Object.keys(result.routes).filter(
@@ -13,12 +13,19 @@
13
13
  // Memory: O(1) for the boolean check; O(#matches) for the index list. No
14
14
  // stripped copy and no per-char array are ever materialized.
15
15
  //
16
- // Pragmatic scanner, not a full tokenizer: regex literals are not special-cased
17
- // (a target token inside one is implausible) and template interpolations are
18
- // treated as opaque string content. One intentional consequence: a token whose
19
- // match would only complete by treating an interleaved comment as whitespace
20
- // (e.g. `createRouter /* x */ (`) is not detected — real calls never interleave
21
- // a comment between the callee and its arguments.
16
+ // Pragmatic scanner, not a full tokenizer: regex literals ARE coarsely skipped
17
+ // (see below) and template interpolations are treated as opaque string content.
18
+ // One intentional consequence: a token whose match would only complete by
19
+ // treating an interleaved comment as whitespace (e.g. `createRouter /* x */ (`)
20
+ // is not detected — real calls never interleave a comment between the callee
21
+ // and its arguments.
22
+ //
23
+ // Regex literals are skipped because a literal containing a quote or comment
24
+ // char (e.g. `const re = /it's a "x"/g;`) would otherwise open a phantom string
25
+ // at the inner quote and swallow the following REAL code — dropping a router
26
+ // file from discovery. We only treat a `/` as a regex start when it is in
27
+ // "regex position" (the previous significant code char is not value-producing),
28
+ // so genuine division (`a / b`) is left untouched.
22
29
 
23
30
  // JS line terminators end a `//` comment: LF, CR, LS (U+2028), PS (U+2029).
24
31
  function isLineTerminator(ch: string): boolean {
@@ -27,6 +34,56 @@ function isLineTerminator(ch: string): boolean {
27
34
  return c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
28
35
  }
29
36
 
37
+ // Identifier-position keywords after which a `/` begins a regex literal, not
38
+ // division: the keyword cannot be the left operand of a division, so `return
39
+ // /re/`, `typeof /re/`, `case /re/`, etc. are regexes. After any OTHER identifier
40
+ // or number (a value), `/` is division.
41
+ const REGEX_PRECEDING_KEYWORDS = new Set([
42
+ "return",
43
+ "typeof",
44
+ "instanceof",
45
+ "in",
46
+ "of",
47
+ "new",
48
+ "delete",
49
+ "void",
50
+ "do",
51
+ "else",
52
+ "yield",
53
+ "await",
54
+ "case",
55
+ "throw",
56
+ ]);
57
+
58
+ // A `/` at `slashPos` is a regex-literal start (not division) when the previous
59
+ // significant code char cannot end an expression. A closing `)`/`]`/`}` and an
60
+ // identifier/digit/`$`/`_` are value-producing (division); everything else
61
+ // (operators, `(`, `,`, `=`, `:`, `{`, `;`, `<`, `>`, ...) and the start-of-file
62
+ // put `/` in regex position. The one subtlety: an identifier that is actually a
63
+ // regex-preceding KEYWORD (`return /re/`) ends in a word char, so the
64
+ // previous-char-only test misread it as division and then let the regex body's
65
+ // inner quotes open a phantom string — dropping a later real `createRouter()`.
66
+ // So when the previous char is a word char we walk back over any whitespace and
67
+ // the identifier and treat `/` as a regex iff that identifier is such a keyword.
68
+ // `}` stays value-producing to avoid swallowing an object/block followed by
69
+ // division; the cost is only that a regex right after a block isn't skipped.
70
+ function isRegexPositionAt(
71
+ code: string,
72
+ slashPos: number,
73
+ prevChar: string | undefined,
74
+ ): boolean {
75
+ if (prevChar === undefined) return true; // start of file
76
+ if (prevChar === ")" || prevChar === "]" || prevChar === "}") return false;
77
+ if (!/[\w$]/.test(prevChar)) return true; // operator / `(` / `,` / `=` / ...
78
+ // Previous char ends an identifier or number: regex only after a keyword that
79
+ // expects an expression. Walk back over whitespace + the identifier run.
80
+ let k = slashPos - 1;
81
+ while (k >= 0 && /\s/.test(code[k])) k--;
82
+ const wordEnd = k + 1;
83
+ while (k >= 0 && /[\w$]/.test(code[k])) k--;
84
+ return REGEX_PRECEDING_KEYWORDS.has(code.slice(k + 1, wordEnd));
85
+ }
86
+
30
87
  /**
31
88
  * Build a classifier that answers "is offset `q` in code (not a comment or
32
89
  * string)?" for STRICTLY INCREASING `q`. The internal cursor only moves forward,
@@ -37,6 +94,9 @@ function makeCodeClassifier(code: string): (q: number) => boolean {
37
94
  let i = 0; // forward cursor: everything before `i` is already classified
38
95
  let skipStart = -1; // last detected comment/string region (cache)
39
96
  let skipEnd = -1;
97
+ // Last significant code char, used to disambiguate `/` (regex vs division).
98
+ // Comments are transparent (don't update it); strings/regex are value-producing.
99
+ let lastSig: string | undefined;
40
100
 
41
101
  return (q: number): boolean => {
42
102
  if (q >= skipStart && q < skipEnd) return false; // q in the cached region
@@ -44,14 +104,17 @@ function makeCodeClassifier(code: string): (q: number) => boolean {
44
104
  const c = code[i];
45
105
  const d = i + 1 < n ? code[i + 1] : "";
46
106
  let end = -1;
107
+ let transparent = false; // comment: skipped but does not set lastSig
47
108
  if (c === "/" && d === "/") {
48
109
  let j = i + 2;
49
110
  while (j < n && !isLineTerminator(code[j])) j++;
50
111
  end = j;
112
+ transparent = true;
51
113
  } else if (c === "/" && d === "*") {
52
114
  let j = i + 2;
53
115
  while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
54
116
  end = Math.min(n, j + 2);
117
+ transparent = true;
55
118
  } else if (c === '"' || c === "'" || c === "`") {
56
119
  let j = i + 1;
57
120
  while (j < n) {
@@ -66,16 +129,51 @@ function makeCodeClassifier(code: string): (q: number) => boolean {
66
129
  j++;
67
130
  }
68
131
  end = j;
132
+ } else if (
133
+ c === "/" &&
134
+ d !== "/" &&
135
+ d !== "*" &&
136
+ isRegexPositionAt(code, i, lastSig)
137
+ ) {
138
+ // Coarse regex-literal skip. A regex literal cannot span a raw newline;
139
+ // `/` inside a `[...]` character class is literal (not a terminator).
140
+ // Bail (treat the `/` as a normal char) if no closing `/` on the line
141
+ // so a stray division-looking `/` never swallows the rest of the line.
142
+ let j = i + 1;
143
+ let inClass = false;
144
+ let closed = false;
145
+ while (j < n && !isLineTerminator(code[j])) {
146
+ const r = code[j];
147
+ if (r === "\\") {
148
+ j += 2;
149
+ continue;
150
+ }
151
+ if (r === "[") inClass = true;
152
+ else if (r === "]") inClass = false;
153
+ else if (r === "/" && !inClass) {
154
+ j++;
155
+ closed = true;
156
+ break;
157
+ }
158
+ j++;
159
+ }
160
+ if (closed) {
161
+ while (j < n && /[a-z]/.test(code[j])) j++; // flags
162
+ end = j;
163
+ }
69
164
  }
70
165
  if (end >= 0) {
71
- // Comment/string region [i, end). `q >= i` here (loop condition).
166
+ // Comment/string/regex region [i, end). `q >= i` here (loop condition).
72
167
  if (q < end) {
73
168
  skipStart = i;
74
169
  skipEnd = end;
75
170
  return false;
76
171
  }
77
172
  i = end;
173
+ // Strings and regex literals are value-producing; comments are not.
174
+ if (!transparent) lastSig = "x";
78
175
  } else {
176
+ if (!/\s/.test(c)) lastSig = c;
79
177
  i++;
80
178
  }
81
179
  }
@@ -139,7 +139,10 @@ export async function discoverAndWriteRouteTypes(
139
139
  for (const [id, router] of registry) {
140
140
  if (!router.urlpatterns) continue;
141
141
 
142
- const manifest = generateManifest(router.urlpatterns, routerMountIndex);
142
+ const manifest = await generateManifest(
143
+ router.urlpatterns,
144
+ routerMountIndex,
145
+ );
143
146
  routerMountIndex++;
144
147
 
145
148
  // Filter out auto-generated route names that the runtime creates for
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Cache error reporting.
3
+ *
4
+ * Caches are best-effort: a read failure degrades to a miss (render fresh) and a
5
+ * write failure degrades to a no-op - they MUST NOT throw up and fail the
6
+ * request. But the failure must still be LOUD: it is logged to the console (so
7
+ * it is visible even in a background waitUntil task or when no hook is wired)
8
+ * AND routed through the router's onError callback (via the request context's
9
+ * deduped _reportBackgroundError) so consumers can observe cache degradation in
10
+ * their own telemetry.
11
+ *
12
+ * The one deliberate exception is the invalidation WRITE verb (updateTag ->
13
+ * store.invalidateTags): a failed durable marker write is rejected so an awaited
14
+ * updateTag() surfaces it (read-your-own-writes honesty). That is not a data
15
+ * read/write and does not go through this helper's swallow-and-degrade path.
16
+ */
17
+
18
+ import { _getRequestContext } from "../server/request-context.js";
19
+
20
+ /**
21
+ * Minimal shape of a request context for error reporting. Passed explicitly by
22
+ * background tasks (waitUntil) where the ALS context is already gone, so the
23
+ * error can still reach the router's onError. Structural to avoid importing the
24
+ * full RequestContext type (request-context.ts imports CacheErrorCategory from
25
+ * here - a mutual type-only reference).
26
+ */
27
+ export interface CacheErrorReporter {
28
+ _reportBackgroundError?: (
29
+ error: unknown,
30
+ category: CacheErrorCategory,
31
+ ) => void;
32
+ }
33
+
34
+ export type CacheErrorCategory =
35
+ /** A read failed (transient infra: KV/Cache API error). Degrade to a miss. */
36
+ | "cache-read"
37
+ /** A write failed. Degrade to a no-op (entry simply not cached). */
38
+ | "cache-write"
39
+ /** A delete/eviction failed. Best-effort. */
40
+ | "cache-delete"
41
+ /**
42
+ * A STORED entry could not be parsed/deserialized (partial KV read, truncated
43
+ * Cache API body, malformed envelope/RSC payload). The entry is faulty and is
44
+ * evicted so subsequent reads do not keep failing on it. Distinct from
45
+ * cache-read so consumers can tell corruption from a transient outage.
46
+ */
47
+ | "cache-corrupt"
48
+ /** A tag-invalidation side effect failed (e.g. the eager CDN purge hook). */
49
+ | "cache-invalidate"
50
+ /**
51
+ * A background stale-while-revalidate refresh failed (the `"use cache"`
52
+ * read-through path). The stale value was already served; the refresh that
53
+ * would have replaced it errored.
54
+ */
55
+ | "stale-revalidation";
56
+
57
+ /**
58
+ * Report a non-fatal cache error loudly without failing the request: always logs
59
+ * (label + error) and, when a request context is available, routes the error
60
+ * through the router's onError callback. Never throws.
61
+ *
62
+ * `ctx` is for callers running in a detached background task (waitUntil), where
63
+ * the ALS request context is already gone (so `_getRequestContext()` is null):
64
+ * they capture the context up front and pass it here so onError still fires.
65
+ * Foreground callers omit it and fall back to the ALS context.
66
+ */
67
+ export function reportCacheError(
68
+ error: unknown,
69
+ category: CacheErrorCategory,
70
+ label: string,
71
+ ctx?: CacheErrorReporter,
72
+ ): void {
73
+ console.error(`${label}:`, error);
74
+ try {
75
+ const target = ctx ?? _getRequestContext();
76
+ target?._reportBackgroundError?.(error, category);
77
+ } catch {
78
+ // Reporting must never itself break the cache path.
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Run a best-effort async cache task (typically scheduled via waitUntil), catching
84
+ * any rejection and routing it through reportCacheError so background cache work
85
+ * (non-blocking L1 writes, KV persistence, L1 promotion) reports failures via
86
+ * onError instead of throwing or silently swallowing. Never rejects.
87
+ *
88
+ * Pass `ctx` when the task runs detached (the ALS context is gone) and the
89
+ * failure should still reach onError; omit it to fall back to the ALS context.
90
+ *
91
+ * @example this.waitUntil(() => reportingAsync(() => cache.put(req, res), "cache-write", "[CFCacheStore] L1 write"))
92
+ */
93
+ export async function reportingAsync(
94
+ task: () => Promise<unknown>,
95
+ category: CacheErrorCategory,
96
+ label: string,
97
+ ctx?: CacheErrorReporter,
98
+ ): Promise<void> {
99
+ try {
100
+ await task();
101
+ } catch (error) {
102
+ reportCacheError(error, category, label, ctx);
103
+ }
104
+ }
@@ -6,24 +6,45 @@
6
6
  * document-cache, and loader-cache.
7
7
  */
8
8
 
9
+ import { encodeKV } from "../encode-kv.js";
10
+
11
+ /**
12
+ * Reserved URL query params that the router owns and must never key the cache
13
+ * on. `_rsc*` is the router's internal navigation/action/loader prefix (matched
14
+ * by prefix). `__no_cache` is the single `__`-prefixed param the router reads
15
+ * (handler.ts / testing dispatch.ts use it to bypass the store); it and the
16
+ * other router-internal `__`-prefixed request params are matched by an EXACT
17
+ * allowlist, not a blanket `__` prefix. A blanket `__` filter would silently
18
+ * collapse consumer params like `__variant=a` vs `__variant=b` onto one cache
19
+ * slot; an allowlist keeps the router's own params out of the key while leaving
20
+ * consumer `__` params intact.
21
+ */
22
+ const RESERVED_SEARCH_PARAMS = new Set([
23
+ "__no_cache",
24
+ "__rsc",
25
+ "__html",
26
+ "__debug_manifest",
27
+ "__prerender_collect",
28
+ ]);
29
+
30
+ function isReservedSearchParam(key: string): boolean {
31
+ return key.startsWith("_rsc") || RESERVED_SEARCH_PARAMS.has(key);
32
+ }
33
+
9
34
  /**
10
35
  * Build a sorted, deterministic query string from URLSearchParams,
11
- * excluding internal _rsc* and __* params.
36
+ * excluding the router's reserved params (see isReservedSearchParam).
12
37
  *
13
38
  * Returns empty string when no user-facing params exist.
14
39
  */
15
40
  export function sortedSearchString(searchParams: URLSearchParams): string {
16
41
  const pairs: [string, string][] = [];
17
42
  for (const [k, v] of searchParams) {
18
- if (!k.startsWith("_rsc") && !k.startsWith("__")) {
43
+ if (!isReservedSearchParam(k)) {
19
44
  pairs.push([k, v]);
20
45
  }
21
46
  }
22
- if (pairs.length === 0) return "";
23
- pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
24
- return pairs
25
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
26
- .join("&");
47
+ return encodeKV(pairs, { sort: true });
27
48
  }
28
49
 
29
50
  /**
@@ -35,10 +56,34 @@ export function sortedRouteParams(
35
56
  params: Record<string, string> | undefined,
36
57
  ): string {
37
58
  if (!params) return "";
38
- const entries = Object.entries(params);
39
- if (entries.length === 0) return "";
40
- return entries
41
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
42
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
43
- .join("&");
59
+ return encodeKV(Object.entries(params), { sort: true });
60
+ }
61
+
62
+ /**
63
+ * Host-namespaced cache key base: `${host}${pathname}[:params][?search]`.
64
+ *
65
+ * The ONE composition of the host-namespacing rule, shared by the segment tier
66
+ * (cache-scope.ts) and the document tier (document-cache.ts) so the rule cannot
67
+ * drift between them. Host prefixing matters because VercelCacheStore /
68
+ * MemorySegmentCacheStore key by the raw string (only CFCacheStore adds host
69
+ * internally) -- on a single function serving multiple domains an
70
+ * un-namespaced key bleeds tenant A's cached response to tenant B.
71
+ *
72
+ * Output is BYTE-STABLE by contract: changing the composition silently
73
+ * invalidates every persisted cache entry on upgrade. Callers append their own
74
+ * tier-specific suffixes (`:rsc`/`:html`, segment hash) after this base.
75
+ */
76
+ export function cacheKeyBase(
77
+ host: string,
78
+ pathname: string,
79
+ searchParams?: URLSearchParams,
80
+ params?: Record<string, string>,
81
+ ): string {
82
+ const paramStr = sortedRouteParams(params);
83
+ const searchStr = searchParams ? sortedSearchString(searchParams) : "";
84
+
85
+ let key = `${host}${pathname}`;
86
+ if (paramStr) key += `:${paramStr}`;
87
+ if (searchStr) key += `?${searchStr}`;
88
+ return key;
44
89
  }