@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -49,7 +49,6 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
49
49
  );
50
50
  genFileVars.push(varName);
51
51
  } else {
52
- // Routers without sourceFile: inline their manifest data directly
53
52
  routersWithoutGenFile.push({
54
53
  id: entry.id,
55
54
  manifest: entry.routeManifest,
@@ -58,7 +57,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
58
57
  }
59
58
 
60
59
  const lines = [
61
- `import { setCachedManifest, setPrecomputedEntries, setRouteTrie, setRouterManifest, registerRouterManifestLoader, clearAllRouterData } from "@rangojs/router/server";`,
60
+ `import { setCachedManifest, setRouterManifest, registerRouterManifestLoader, clearAllRouterData } from "@rangojs/router/server";`,
62
61
  ...genFileImports,
63
62
  // Clear stale per-router cached data (manifest, trie, precomputed entries)
64
63
  // before re-populating. In Cloudflare dev mode, program reloads re-evaluate
@@ -68,14 +67,12 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
68
67
  `clearAllRouterData();`,
69
68
  ];
70
69
 
71
- // Flatten NamedRoutes entries: search schema objects -> plain string paths
72
70
  if (genFileVars.length > 0) {
73
71
  lines.push(
74
72
  `function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,
75
73
  );
76
74
  }
77
75
 
78
- // Build the merged manifest from gen file imports + inlined data
79
76
  if (genFileVars.length === 1 && routersWithoutGenFile.length === 0) {
80
77
  lines.push(`setCachedManifest(__flat(${genFileVars[0]}));`);
81
78
  } else {
@@ -86,7 +83,6 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
86
83
  lines.push(`setCachedManifest({ ${parts.join(", ")} });`);
87
84
  }
88
85
 
89
- // Set per-router manifests
90
86
  let genVarIdx = 0;
91
87
  for (const entry of state.perRouterManifests) {
92
88
  if (entry.sourceFile) {
@@ -101,31 +97,19 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
101
97
  }
102
98
  }
103
99
 
104
- // In dev mode, skip trie and precomputed entries injection. These are
105
- // computed once during initial discovery and become stale after route
106
- // changes. A stale trie would incorrectly match removed routes. The
107
- // handler falls back to Phase 2 regex matching against the live
108
- // router.urlpatterns, which is always correct after a program reload.
109
- // In build mode, the trie is always fresh (built from the final route
110
- // tree) so it's safe to inject.
111
- if (state.isBuildMode) {
112
- if (
113
- state.mergedPrecomputedEntries &&
114
- state.mergedPrecomputedEntries.length > 0
115
- ) {
116
- lines.push(
117
- `setPrecomputedEntries(${jsonParseExpression(state.mergedPrecomputedEntries)});`,
118
- );
119
- }
120
- if (state.mergedRouteTrie) {
121
- lines.push(
122
- `setRouteTrie(${jsonParseExpression(state.mergedRouteTrie)});`,
123
- );
124
- }
125
- }
100
+ // Per-router trie and precomputedEntries are NOT inlined eagerly.
101
+ // They live in the per-router lazy chunks (generatePerRouterModule) and
102
+ // are loaded via ensureRouterManifest(routerId), which is awaited before
103
+ // every request in router.fetch() and before findMatch is reached.
104
+ // Inlining the merged versions here would duplicate the per-router data
105
+ // (the merged trie/precomputedEntries equal the per-router data for
106
+ // single-router apps; for multi-router, the merged trie is dead code
107
+ // because find-match.ts only consumes per-router tries).
108
+ //
109
+ // In dev mode, the handler also falls back to Phase 2 regex matching
110
+ // against live router.urlpatterns, which is always correct after a
111
+ // program reload.
126
112
 
127
- // Register lazy loaders for per-router manifest modules.
128
- // Each import() uses a static string literal so Rollup creates separate chunks.
129
113
  for (const routerId of state.perRouterManifestDataMap.keys()) {
130
114
  lines.push(
131
115
  `registerRouterManifestLoader(${JSON.stringify(routerId)}, () => import(${JSON.stringify(VIRTUAL_ROUTES_MANIFEST_ID + "/" + routerId)}));`,
@@ -139,9 +123,6 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
139
123
  return lines.join("\n");
140
124
  }
141
125
 
142
- // No manifest: either discovery hasn't completed or no runner (Cloudflare dev).
143
- // Still inject __PRERENDER_DEV_URL so the prerender store can fetch on-demand.
144
- // Re-resolve origin now since the server is listening by module load time.
145
126
  if (!state.isBuildMode) {
146
127
  const origin =
147
128
  state.devServerOrigin ||
@@ -170,7 +151,6 @@ export function generatePerRouterModule(
170
151
  const lines: string[] = [];
171
152
 
172
153
  if (routerEntry?.sourceFile) {
173
- // Import manifest from the gen file so HMR auto-propagates
174
154
  const routerDir = dirname(routerEntry.sourceFile);
175
155
  const routerBasename = basename(routerEntry.sourceFile).replace(
176
156
  /\.(tsx?|jsx?)$/,
@@ -199,5 +179,5 @@ export function generatePerRouterModule(
199
179
  `export const precomputedEntries = ${jsonParseExpression(entries)};`,
200
180
  );
201
181
  }
202
- return lines.join("\n") || "// empty router manifest";
182
+ return lines.join("\n") || "";
203
183
  }
package/src/vite/index.ts CHANGED
@@ -13,6 +13,8 @@ export type {
13
13
  RangoNodeOptions,
14
14
  RangoCloudflareOptions,
15
15
  RangoOptions,
16
+ ClientChunks,
17
+ ClientChunkMeta,
16
18
  BuildEnvOption,
17
19
  BuildEnvFactory,
18
20
  BuildEnvFactoryContext,
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Bake the resolved INTERNAL_RANGO_DEBUG value into the router's `internal-debug`
3
+ * module so the flag reaches the CLIENT debug logs by just setting the env var.
4
+ *
5
+ * internal-debug.ts normally reads the flag via `typeof __RANGO_DEBUG__`, a Vite
6
+ * define. That delivery is unreliable on the client: in dev Vite ships the define
7
+ * only as an injected global whose presence varies across consumer setups, so the
8
+ * module can fall through to `process.env` (undefined in the browser) and the FE
9
+ * debug flag silently stays false while the server logs work. A Vite `transform`
10
+ * runs on the module regardless of how (or whether) the define is delivered, in
11
+ * both dev and build and for every environment, so the discovery plugin uses this
12
+ * to replace the module with the resolved literal.
13
+ *
14
+ * Returns null for any module that is not the router's internal-debug module.
15
+ */
16
+ export function injectClientDebugFlag(
17
+ id: string,
18
+ ): { code: string; map: null } | null {
19
+ // Cheap early-out: this hook runs on every module in every environment.
20
+ if (!id.includes("internal-debug")) return null;
21
+ const norm = id.replace(/\\/g, "/");
22
+ // Scope to the router's own internal-debug module: the published package
23
+ // (`/@rangojs/router/`, incl. pnpm's nested layout) or the monorepo workspace
24
+ // (`/packages/rangojs-router/`). The package-anchored path avoids matching a
25
+ // consumer file that merely sits under a directory named `rangojs-router`.
26
+ const isInternalDebug =
27
+ /\/internal-debug\.[cm]?[jt]sx?(\?|$)/.test(norm) &&
28
+ (norm.includes("/@rangojs/router/") ||
29
+ norm.includes("/packages/rangojs-router/"));
30
+ if (!isInternalDebug) return null;
31
+ // Emit the whole module: internal-debug.ts has a single export, kept in sync.
32
+ return {
33
+ code: `export const INTERNAL_RANGO_DEBUG = ${!!process.env.INTERNAL_RANGO_DEBUG};\n`,
34
+ map: null,
35
+ };
36
+ }
@@ -47,6 +47,64 @@ export type BuildEnvOption =
47
47
  | Record<string, unknown>
48
48
  | BuildEnvFactory;
49
49
 
50
+ // -- Client chunking --------------------------------------------------------
51
+
52
+ /**
53
+ * Metadata for one client ("use client") module, passed to a {@link ClientChunks}
54
+ * function. Mirrors the shape `@vitejs/plugin-rsc` passes to its own
55
+ * `clientChunks` option.
56
+ */
57
+ export interface ClientChunkMeta {
58
+ /** Absolute module id of the "use client" file. */
59
+ id: string;
60
+ /** Normalized (posix) module id — convenient for path-based matching. */
61
+ normalizedId: string;
62
+ /**
63
+ * The RSC/server chunk that statically imports this client reference. This is
64
+ * the key used for the default grouping when no override is supplied: a single
65
+ * router that statically imports every route yields ONE `serverChunk`, hence
66
+ * one client chunk for all routes.
67
+ */
68
+ serverChunk: string;
69
+ }
70
+
71
+ /**
72
+ * Controls how client ("use client") components are grouped into browser
73
+ * chunks, i.e. per-route / per-feature code splitting of the client bundle.
74
+ *
75
+ * Without splitting, a single router ships ONE client chunk containing every
76
+ * route's client components (and their CSS) — navigating to one route downloads
77
+ * every other route's client code. (Host sub-apps loaded via a dynamic `import()`
78
+ * are the exception: each forms its own chunk.) This option controls how that
79
+ * monolith is split.
80
+ *
81
+ * Behavior branches:
82
+ * - `true` / omitted (**default**, pre-1.0): Rango's built-in **directory
83
+ * strategy**. It splits app `"use client"` modules by **route id** — the segment
84
+ * after a route-root directory (`routes`, `app`, `pages`, `features`, `handlers`,
85
+ * …) — so `routes/dashboard/**` becomes `app-dashboard` at any nesting depth.
86
+ * Where it finds NO route structure (a flat `src/components/`, or host sub-apps
87
+ * already split by a dynamic `import()`), it inherits the default grouping
88
+ * unchanged — so the shared `src/components` chunk stays shared and host apps do
89
+ * not leak across each other. Shared runtime (React, the router, `node_modules`)
90
+ * is never split.
91
+ * - `false`: opt out — inherit `@vitejs/plugin-rsc`'s default grouping everywhere
92
+ * (one chunk per router / per host sub-app).
93
+ * - function: full override. Return a chunk group name, or `undefined` to fall
94
+ * back to the default grouping for that one module. Forwarded directly to
95
+ * `@vitejs/plugin-rsc`'s `clientChunks`.
96
+ *
97
+ * Every module maps to exactly one group, so there is no byte duplication: a
98
+ * component used by two routes lives in one group and is fetched whenever it
99
+ * renders. Put genuinely shared client components OUTSIDE route directories so
100
+ * they land in the shared group rather than one route's chunk.
101
+ *
102
+ * @default true
103
+ */
104
+ export type ClientChunks =
105
+ | boolean
106
+ | ((meta: ClientChunkMeta) => string | undefined);
107
+
50
108
  // -- Plugin options ---------------------------------------------------------
51
109
 
52
110
  /**
@@ -60,16 +118,52 @@ interface RangoBaseOptions {
60
118
  banner?: boolean;
61
119
 
62
120
  /**
63
- * Environment bindings available to Prerender and Static handlers at build
64
- * time via `ctx.env`. Applies to both production build and dev on-demand
65
- * prerender (`/__rsc_prerender`).
121
+ * Group client ("use client") components into browser chunks for per-route /
122
+ * per-feature code splitting. On by default (pre-1.0); pass `false` to opt out.
123
+ * See {@link ClientChunks}.
66
124
  *
67
- * This is the build-time env supplied by the Vite plugin, not the live
68
- * request env. It is shared across all prerender invocations for the build.
125
+ * @default true
126
+ */
127
+ clientChunks?: ClientChunks;
128
+
129
+ /**
130
+ * Filter which files route discovery scans, by glob. Paths are matched
131
+ * root-relative (e.g. `src/routes/**`). `include` restricts discovery to
132
+ * matching files; `exclude` removes matches (the defaults cover tests, dist,
133
+ * coverage, etc.). Mirrors the CLI's `--include`/`--exclude`.
69
134
  *
70
- * @default false
135
+ * @example
136
+ * rango({ discovery: { include: ["src/routes/**"] } })
71
137
  */
72
- buildEnv?: BuildEnvOption;
138
+ discovery?: {
139
+ include?: string[];
140
+ exclude?: string[];
141
+ };
142
+
143
+ /**
144
+ * What to do when a `Prerender` route's or `Static` handler's render throws at
145
+ * build time. Otherwise the route error boundary catches it and the rendered
146
+ * error page is baked as the artifact, then served as an HTTP 200 — a silent,
147
+ * user-visible breakage (issue #587). Independent of this setting, a render may
148
+ * `throw new Skip()` (from `@rangojs/router`) to skip a single URL/handler.
149
+ *
150
+ * - `"fail"` (**default**): fail the build, naming the URL/handler and the
151
+ * original render error.
152
+ * - `"warn"`: log a warning and skip baking the artifact — it is never served as a
153
+ * baked 200 error page. `"warn"` is a build-unblock, NOT a runtime contract for
154
+ * the skipped entry: the route falls through to normal resolution, which may
155
+ * render live (its handler is still bundled — e.g. when nothing else baked) or
156
+ * 404 (once prerender handler eviction has run for other baked entries), so the
157
+ * outcome depends on the rest of the build, and a skipped `Static()` handler's
158
+ * evicted code can surface as an error. For DEFINED runtime behavior use
159
+ * `Passthrough()` (a live fallback) or `throw new Skip()` (an intentional skip);
160
+ * otherwise prefer the default `"fail"`.
161
+ *
162
+ * @default "fail"
163
+ */
164
+ prerender?: {
165
+ onError?: "fail" | "warn";
166
+ };
73
167
  }
74
168
 
75
169
  /**
@@ -80,6 +174,17 @@ export interface RangoNodeOptions extends RangoBaseOptions {
80
174
  * Deployment preset. Defaults to 'node' when not specified.
81
175
  */
82
176
  preset?: "node";
177
+
178
+ /**
179
+ * Environment bindings available to Prerender and Static handlers at build
180
+ * time via `ctx.env`. Shared across all prerender invocations for the build.
181
+ *
182
+ * `"auto"` is Cloudflare-only (it resolves the wrangler platform proxy), so it
183
+ * is not accepted on the Node preset — pass an object or a factory instead.
184
+ *
185
+ * @default false
186
+ */
187
+ buildEnv?: Exclude<BuildEnvOption, "auto">;
83
188
  }
84
189
 
85
190
  /**
@@ -89,12 +194,25 @@ export interface RangoCloudflareOptions extends RangoBaseOptions {
89
194
  /**
90
195
  * Deployment preset for Cloudflare Workers.
91
196
  * When using cloudflare preset:
92
- * - @vitejs/plugin-rsc is NOT added (cloudflare plugin adds it)
197
+ * - @vitejs/plugin-rsc IS still added by rango(), but with `serverHandler: false`
198
+ * (the cloudflare plugin owns the RSC worker/server entry); only `client` and
199
+ * `ssr` virtual entries are configured, no rsc entry
93
200
  * - Your worker entry (e.g., worker.rsc.tsx) imports the router directly
94
201
  * - Browser and SSR use virtual entries
95
202
  * - Build-time manifest generation is auto-detected from the resolved RSC environment config
96
203
  */
97
204
  preset: "cloudflare";
205
+
206
+ /**
207
+ * Environment bindings available to Prerender and Static handlers at build
208
+ * time via `ctx.env`. Shared across all prerender invocations for the build.
209
+ *
210
+ * `"auto"` resolves the Cloudflare platform proxy via wrangler
211
+ * `getPlatformProxy()`.
212
+ *
213
+ * @default false
214
+ */
215
+ buildEnv?: BuildEnvOption;
98
216
  }
99
217
 
100
218
  /**
@@ -1,86 +1,83 @@
1
1
  import type { Plugin } from "vite";
2
+ import { createRangoDebugger, NS } from "../debug.js";
3
+
4
+ const debug = createRangoDebugger(NS.transform);
2
5
 
3
6
  /**
4
7
  * Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
5
8
  * The react-server-dom vendor files are shipped as CJS which doesn't work in browsers.
6
9
  */
7
10
  export function createCjsToEsmPlugin(): Plugin {
11
+ // Picked from Vite's resolved mode, not process.env.NODE_ENV, so the dev vs
12
+ // production vendor variant tracks the build mode the user actually ran.
13
+ let isProduction = false;
14
+
8
15
  return {
9
16
  name: "@rangojs/router:cjs-to-esm",
10
17
  enforce: "pre",
18
+ configResolved(config) {
19
+ isProduction = config.isProduction;
20
+ },
11
21
  transform(code, id) {
12
- const cleanId = id.split("?")[0];
22
+ const cleanId = id.split("?")[0].replaceAll("\\", "/");
13
23
 
14
- // Transform the client.browser.js entry point to re-export from CJS
15
- if (
16
- cleanId.includes("vendor/react-server-dom/client.browser.js") ||
17
- cleanId.includes("vendor\\react-server-dom\\client.browser.js")
18
- ) {
19
- const isProd = process.env.NODE_ENV === "production";
24
+ if (cleanId.includes("vendor/react-server-dom/client.browser.js")) {
25
+ const isProd = isProduction;
20
26
  const cjsFile = isProd
21
27
  ? "./cjs/react-server-dom-webpack-client.browser.production.js"
22
28
  : "./cjs/react-server-dom-webpack-client.browser.development.js";
23
29
 
30
+ debug?.("cjs-to-esm entry redirect %s", id);
24
31
  return {
25
32
  code: `export * from "${cjsFile}";`,
26
33
  map: null,
27
34
  };
28
35
  }
29
36
 
30
- // Transform the actual CJS files to ESM
31
37
  if (
32
- (cleanId.includes("vendor/react-server-dom/cjs/") ||
33
- cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
38
+ cleanId.includes("vendor/react-server-dom/cjs/") &&
34
39
  cleanId.includes("client.browser")
35
40
  ) {
36
41
  let transformed = code;
37
42
 
38
- // Extract the license comment to preserve it
39
43
  const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
40
44
  const license = licenseMatch ? licenseMatch[0] : "";
41
45
  if (license) {
42
46
  transformed = transformed.slice(license.length);
43
47
  }
44
48
 
45
- // Remove "use strict" (both dev and prod have this)
46
49
  transformed = transformed.replace(/^\s*["']use strict["'];\s*/, "");
47
50
 
48
- // Remove the conditional IIFE wrapper (development only)
49
51
  transformed = transformed.replace(
50
52
  /^\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
51
53
  "",
52
54
  );
53
55
 
54
- // Remove the closing of the conditional IIFE at the end (development only)
55
56
  transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
56
57
 
57
- // Replace require('react') and require('react-dom') with imports (development)
58
58
  transformed = transformed.replace(
59
59
  /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
60
60
  'import React from "react";\nimport ReactDOM from "react-dom";\nvar ',
61
61
  );
62
62
 
63
- // Replace require('react-dom') only (production - doesn't import React)
64
63
  transformed = transformed.replace(
65
64
  /var\s+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
66
65
  'import ReactDOM from "react-dom";\nvar ',
67
66
  );
68
67
 
69
- // Transform exports.xyz = function() to export function xyz()
70
68
  transformed = transformed.replace(
71
69
  /exports\.(\w+)\s*=\s*function\s*\(/g,
72
70
  "export function $1(",
73
71
  );
74
72
 
75
- // Transform exports.xyz = value to export const xyz = value
76
73
  transformed = transformed.replace(
77
74
  /exports\.(\w+)\s*=/g,
78
75
  "export const $1 =",
79
76
  );
80
77
 
81
- // Reconstruct with license at the top
82
78
  transformed = license + "\n" + transformed;
83
79
 
80
+ debug?.("cjs-to-esm body rewrite %s", id);
84
81
  return {
85
82
  code: transformed,
86
83
  map: null,
@@ -1,4 +1,7 @@
1
1
  import type { Plugin, ResolvedConfig } from "vite";
2
+ import { createRangoDebugger, NS } from "../debug.js";
3
+
4
+ const debug = createRangoDebugger(NS.transform);
2
5
 
3
6
  const CLIENT_IN_SERVER_PROXY_PREFIX =
4
7
  "virtual:vite-rsc/client-in-server-package-proxy/";
@@ -62,6 +65,7 @@ export function extractPackageName(absolutePath: string): string | null {
62
65
  */
63
66
  export function clientRefDedup(): Plugin {
64
67
  let clientExclude: string[] = [];
68
+ const dedupedPackages = new Set<string>();
65
69
 
66
70
  return {
67
71
  name: "@rangojs/router:client-ref-dedup",
@@ -69,33 +73,35 @@ export function clientRefDedup(): Plugin {
69
73
  apply: "serve",
70
74
 
71
75
  configResolved(config: ResolvedConfig) {
72
- // Respect user's optimizeDeps.exclude — if a package is explicitly
73
- // excluded from pre-bundling, we shouldn't redirect it there.
74
76
  const clientEnv = config.environments?.["client"];
75
77
  clientExclude =
76
78
  clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
77
79
  },
78
80
 
81
+ buildEnd() {
82
+ if (debug && dedupedPackages.size > 0) {
83
+ debug(
84
+ "client-ref-dedup: redirected %d package(s) (%s)",
85
+ dedupedPackages.size,
86
+ [...dedupedPackages].join(","),
87
+ );
88
+ }
89
+ },
90
+
79
91
  resolveId(source, importer, options) {
80
- // Only intercept in the client environment
81
92
  if (this.environment?.name !== "client") return;
82
93
 
83
- // Only handle imports from client-in-server-package-proxy virtual modules
84
94
  if (!importer?.includes(CLIENT_IN_SERVER_PROXY_PREFIX)) return;
85
95
 
86
- // Only handle absolute node_modules paths
87
96
  if (!source.includes("/node_modules/")) return;
88
97
 
89
- // Must have an importer
90
- if (!importer) return;
91
-
92
98
  const packageName = extractPackageName(source);
93
99
  if (!packageName) return;
94
100
 
95
- // Don't redirect packages that are excluded from optimization
96
101
  if (clientExclude.includes(packageName)) return;
97
102
 
98
- // Return a virtual module that re-exports via bare specifier
103
+ if (debug) dedupedPackages.add(packageName);
104
+
99
105
  return `\0rango:dedup/${packageName}`;
100
106
  },
101
107
 
@@ -104,7 +110,6 @@ export function clientRefDedup(): Plugin {
104
110
 
105
111
  const packageName = id.slice("\0rango:dedup/".length);
106
112
 
107
- // Re-export via bare specifier so Vite routes through pre-bundling
108
113
  return [
109
114
  `export * from ${JSON.stringify(packageName)};`,
110
115
  `import * as __all__ from ${JSON.stringify(packageName)};`,
@@ -1,8 +1,10 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { relative } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { createRangoDebugger, createCounter, NS } from "../debug.js";
5
+
6
+ const debug = createRangoDebugger(NS.transform);
4
7
 
5
- // Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
6
8
  const CLIENT_PKG_PROXY_PREFIX =
7
9
  "/@id/__x00__virtual:vite-rsc/client-package-proxy/";
8
10
  const CLIENT_IN_SERVER_PKG_PROXY_PREFIX =
@@ -19,6 +21,17 @@ const FS_PREFIX = "/@fs/";
19
21
  * Returns the input unchanged if it doesn't match a known dev-mode pattern
20
22
  * (e.g., already a production hash).
21
23
  */
24
+ /**
25
+ * The production client-reference key hash: `sha256(relativeId).slice(0,12)`,
26
+ * matching @vitejs/plugin-rsc's `hashString`. Exported so the client-chunks
27
+ * strategy can hash a `clientChunks` callback's `meta.normalizedId` (already the
28
+ * project-root-relative id) and compare it against fallback hashes collected
29
+ * during discovery.
30
+ */
31
+ export function hashRefKey(relativeId: string): string {
32
+ return createHash("sha256").update(relativeId).digest("hex").slice(0, 12);
33
+ }
34
+
22
35
  export function computeProductionHash(
23
36
  projectRoot: string,
24
37
  refKey: string,
@@ -26,32 +39,24 @@ export function computeProductionHash(
26
39
  let toHash: string;
27
40
 
28
41
  if (refKey.startsWith(CLIENT_PKG_PROXY_PREFIX)) {
29
- // /@id/__x00__virtual:vite-rsc/client-package-proxy/<pkg> -> hash("<pkg>")
30
42
  toHash = refKey.slice(CLIENT_PKG_PROXY_PREFIX.length);
31
43
  } else if (refKey.startsWith(CLIENT_IN_SERVER_PKG_PROXY_PREFIX)) {
32
- // /@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/<encodedAbsPath>
33
44
  const absPath = decodeURIComponent(
34
45
  refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length),
35
46
  );
36
47
  toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
37
48
  } else if (refKey.startsWith(FS_PREFIX)) {
38
- // /@fs/abs/path.tsx -> hash(relative(root, "/abs/path.tsx"))
39
49
  const absPath = refKey.slice(FS_PREFIX.length - 1); // keep leading /
40
50
  toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
41
51
  } else if (refKey.startsWith("/")) {
42
- // /src/Button.tsx -> hash("src/Button.tsx")
43
52
  toHash = refKey.slice(1);
44
53
  } else {
45
- // Already hashed or unknown format — return unchanged
46
54
  return refKey;
47
55
  }
48
56
 
49
- return createHash("sha256").update(toHash).digest("hex").slice(0, 12);
57
+ return hashRefKey(toHash);
50
58
  }
51
59
 
52
- // Regex to match registerClientReference() calls as emitted by @vitejs/plugin-rsc.
53
- // Captures the reference key (second argument) from the call.
54
- // Handles two proxy forms: parenthesized expression `(expr)` and arrow-throw `() => { ... }`.
55
60
  const REGISTER_CLIENT_REF_RE =
56
61
  /registerClientReference\(\s*(?:(?:\([^)]*\))|(?:\(\)[\s\S]*?\}))\s*,\s*"([^"]+)"\s*,\s*"[^"]+"\s*\)/g;
57
62
 
@@ -89,17 +94,25 @@ export function transformClientRefs(
89
94
  * regex replacement of Flight payloads.
90
95
  */
91
96
  export function hashClientRefs(projectRoot: string): Plugin {
97
+ const counter = createCounter(debug, "hash-client-refs");
92
98
  return {
93
99
  name: "@rangojs/router:hash-client-refs",
94
- // Run after the RSC plugin's transform (default enforce is normal)
95
100
  enforce: "post",
96
101
  applyToEnvironment(env) {
97
102
  return env.name === "rsc";
98
103
  },
99
- transform(code, _id) {
100
- const result = transformClientRefs(code, projectRoot);
101
- if (result === null) return;
102
- return { code: result, map: null };
104
+ buildEnd() {
105
+ counter?.flush();
106
+ },
107
+ transform(code, id) {
108
+ const start = counter ? performance.now() : 0;
109
+ try {
110
+ const result = transformClientRefs(code, projectRoot);
111
+ if (result === null) return;
112
+ return { code: result, map: null };
113
+ } finally {
114
+ counter?.record(id, performance.now() - start);
115
+ }
103
116
  },
104
117
  };
105
118
  }
@@ -32,14 +32,6 @@ const IMPORT_NODE_TYPES = new Set([
32
32
  "ExportAllDeclaration",
33
33
  ]);
34
34
 
35
- // Keep in sync with `STUBS` in cloudflare-protocol-loader-hook.mjs —
36
- // both paths (Vite transform and Node loader) need to hand out the same
37
- // classes. Unknown `cloudflare:*` modules fall back to an empty default
38
- // export so third-party packages (e.g. the Cloudflare Agents SDK) can
39
- // pull them into the graph without crashing discovery. Discovery only
40
- // evaluates module top-level code — no handlers run — so missing named
41
- // exports only fail if something does `class X extends Missing {}` at
42
- // module scope, which is rare outside the already-stubbed classes.
43
35
  const STUBS: Record<string, string> = {
44
36
  "cloudflare:workers": `
45
37
  export class DurableObject { constructor(_ctx, _env) {} }
@@ -65,15 +57,6 @@ export default {};
65
57
  `,
66
58
  };
67
59
 
68
- // Policy: unknown `cloudflare:*` specifiers resolve permissively (empty
69
- // default export) rather than throwing. We prioritize dependency-graph
70
- // resilience over strict validation of user imports because third-party
71
- // packages can pull `cloudflare:*` modules we haven't curated, and
72
- // discovery should not fail just because those modules appear in the graph.
73
- // Tradeoff: unsupported user-authored `cloudflare:*` imports may fail later
74
- // with a generic JS/module error instead of a tailored rango-branded hint.
75
- // The test below pins this behavior so dependency compatibility is not
76
- // regressed accidentally.
77
60
  const FALLBACK_STUB = `export default {};\n`;
78
61
 
79
62
  interface AstNode {
@@ -139,7 +122,7 @@ export function createCloudflareProtocolStubPlugin(): Plugin {
139
122
 
140
123
  let ast: AstNode;
141
124
  try {
142
- ast = this.parse(code) as unknown as AstNode;
125
+ ast = this.parse(code, { lang: "tsx" }) as unknown as AstNode;
143
126
  } catch {
144
127
  // Malformed source — let a downstream plugin surface the parse error.
145
128
  return null;
@@ -163,9 +146,6 @@ export function createCloudflareProtocolStubPlugin(): Plugin {
163
146
 
164
147
  if (hits.length === 0) return null;
165
148
 
166
- // Rewrite from last to first so earlier offsets stay valid. `start`/
167
- // `end` span the full literal including quotes, so we re-emit the
168
- // same quote character around the new specifier.
169
149
  hits.sort((a, b) => b.start - a.start);
170
150
  let out = code;
171
151
  for (const hit of hits) {