@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -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 +121 -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 +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -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 +67 -40
  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/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  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/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +121 -205
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +192 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +64 -25
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +348 -128
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -3,10 +3,10 @@ import MagicString from "magic-string";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import { normalizePath } from "./expose-id-utils.js";
6
+ import { createRangoDebugger, createCounter, NS } from "../debug.js";
7
+
8
+ const debug = createRangoDebugger(NS.transform);
6
9
 
7
- /**
8
- * Type for the RSC plugin's manager API
9
- */
10
10
  interface RscPluginManager {
11
11
  serverReferenceMetaMap: Record<
12
12
  string,
@@ -23,14 +23,9 @@ interface RscPluginApi {
23
23
  manager: RscPluginManager;
24
24
  }
25
25
 
26
- /**
27
- * Get the RSC plugin's API from Vite config
28
- */
29
26
  function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
30
- // Try by name first
31
27
  let plugin = config.plugins.find((p) => p.name === "rsc:minimal");
32
28
 
33
- // Fallback: find by API structure if name lookup fails
34
29
  if (!plugin) {
35
30
  plugin = config.plugins.find(
36
31
  (p) =>
@@ -39,7 +34,7 @@ function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
39
34
  );
40
35
  if (plugin) {
41
36
  console.warn(
42
- `[rsc-router:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
37
+ `[rango:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
43
38
  `Consider updating the name lookup if the plugin was renamed.`,
44
39
  );
45
40
  }
@@ -59,13 +54,11 @@ function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
59
54
  function isUseServerModule(filePath: string): boolean {
60
55
  try {
61
56
  const content = fs.readFileSync(filePath, "utf-8");
62
- // Remove leading comments and whitespace to find the first meaningful content
63
57
  const trimmed = content
64
- .replace(/^\s*\/\/[^\n]*\n/gm, "") // Remove single-line comments
65
- .replace(/^\s*\/\*[\s\S]*?\*\/\s*/gm, "") // Remove multi-line comments
58
+ .replace(/^\s*\/\/[^\n]*\n/gm, "")
59
+ .replace(/^\s*\/\*[\s\S]*?\*\/\s*/gm, "")
66
60
  .trimStart();
67
61
 
68
- // Check if the file starts with "use server" directive
69
62
  return (
70
63
  trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'")
71
64
  );
@@ -74,18 +67,6 @@ function isUseServerModule(filePath: string): boolean {
74
67
  }
75
68
  }
76
69
 
77
- /**
78
- * Transform code to expose action IDs on createServerReference calls.
79
- * Wraps each call with an IIFE that attaches $id to the returned function.
80
- *
81
- * @param code - The source code to transform
82
- * @param sourceId - The source file identifier (for sourcemap)
83
- * @param hashToFileMap - Optional mapping from hash to file path (for server bundles)
84
- */
85
- /**
86
- * Apply createServerReference wrapping to a MagicString instance.
87
- * Returns true if any changes were made.
88
- */
89
70
  function applyServerReferenceWrapping(
90
71
  code: string,
91
72
  s: MagicString,
@@ -95,11 +76,6 @@ function applyServerReferenceWrapping(
95
76
  return false;
96
77
  }
97
78
 
98
- // Match: createServerReference("hash#actionName", ...) or $$ReactClient.createServerReference(...)
99
- // The RSC plugin uses $$ReactClient namespace in transformed code.
100
- // Note: [^)]* cannot handle nested parens in trailing args. This is safe in practice
101
- // because the RSC plugin always generates simple variable references (e.g., callServer)
102
- // as the second argument, never nested function calls.
103
79
  const pattern =
104
80
  /((?:\$\$\w+\.)?createServerReference)\(("[^"]+#[^"]+")([^)]*)\)/g;
105
81
 
@@ -112,23 +88,19 @@ function applyServerReferenceWrapping(
112
88
  const start = match.index;
113
89
  const end = start + fullMatch.length;
114
90
 
115
- // Parse the ID to potentially replace hash with file path
116
91
  let finalIdArg = idArg;
117
92
  if (hashToFileMap) {
118
- // idArg is like '"hash#actionName"', extract the parts
119
93
  const idValue = idArg.slice(1, -1); // Remove quotes
120
94
  const hashMatch = idValue.match(/^([^#]+)#(.+)$/);
121
95
  if (hashMatch) {
122
96
  const [, hash, actionName] = hashMatch;
123
97
  const filePath = hashToFileMap.get(hash);
124
98
  if (filePath) {
125
- // Replace hash with file path for server-side
126
99
  finalIdArg = `"${filePath}#${actionName}"`;
127
100
  }
128
101
  }
129
102
  }
130
103
 
131
- // Wrap the createServerReference call to attach $$id to the returned function
132
104
  const replacement = `(function(fn) { fn.$$id = ${finalIdArg}; return fn; })(${fnCall}(${idArg}${rest}))`;
133
105
  s.overwrite(start, end, replacement);
134
106
  }
@@ -152,29 +124,6 @@ function transformServerReferences(
152
124
  };
153
125
  }
154
126
 
155
- /**
156
- * Transform registerServerReference calls in server bundles to use file paths instead of hashes.
157
- * Pattern: registerServerReference(fn, "hash", "exportName")
158
- * React's registerServerReference sets $$id = hash + "#" + exportName
159
- * By replacing the hash with file path, $$id will contain the file path for revalidation matching.
160
- *
161
- * Only actions from module-level "use server" files are transformed.
162
- * Inline actions (defined in RSC components with "use server" inside a function) are NOT in
163
- * hashToFileMap and keep their hashed IDs. This is intentional for client security:
164
- * - Module-level "use server" files: shared action modules, file path helps revalidation
165
- * - Inline actions: one-off actions in RSC, hash ID prevents file path exposure to client
166
- *
167
- * @param code - The source code to transform
168
- * @param sourceId - The source file identifier (for sourcemap)
169
- * @param hashToFileMap - Mapping from hash to file path (only module-level "use server" files)
170
- */
171
- /**
172
- * Apply registerServerReference wrapping to a MagicString instance.
173
- * Returns true if any changes were made.
174
- *
175
- * Only actions from module-level "use server" files are transformed.
176
- * Inline actions keep their hashed IDs for client security.
177
- */
178
127
  function applyRegisterReferenceWrapping(
179
128
  code: string,
180
129
  s: MagicString,
@@ -184,8 +133,6 @@ function applyRegisterReferenceWrapping(
184
133
  return false;
185
134
  }
186
135
 
187
- // Match: registerServerReference(fn, "hash", "exportName")
188
- // The hash is the second argument, exportName is the third
189
136
  const pattern =
190
137
  /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
191
138
 
@@ -197,15 +144,9 @@ function applyRegisterReferenceWrapping(
197
144
  const start = match.index;
198
145
  const end = start + fullMatch.length;
199
146
 
200
- // Look up the file path for this hash
201
147
  const filePath = hashToFileMap.get(hash);
202
148
  if (filePath) {
203
149
  hasChanges = true;
204
- // WRAP the call to add $id property with file path
205
- // Keep the original hash for React's action registry (so loadServerAction works)
206
- // Add $id (single dollar) with file path for revalidation matching
207
- // Note: We use $id instead of $$id because React's registerServerReference
208
- // sets $$id as a non-writable property
209
150
  const filePathId = `${filePath}#${exportName}`;
210
151
  const replacement = `(function(fn) { fn.$id = "${filePathId}"; return fn; })(registerServerReference(${fnArg}, "${hash}", "${exportName}"))`;
211
152
  s.overwrite(start, end, replacement);
@@ -254,6 +195,8 @@ export function exposeActionId(): Plugin {
254
195
  let isBuild = false;
255
196
  let hashToFileMap: Map<string, string> | undefined;
256
197
  let rscPluginApi: RscPluginApi | undefined;
198
+ const counterTransform = createCounter(debug, "expose-action-id transform");
199
+ const counterRender = createCounter(debug, "expose-action-id renderChunk");
257
200
 
258
201
  return {
259
202
  name: "@rangojs/router:expose-action-id",
@@ -268,6 +211,11 @@ export function exposeActionId(): Plugin {
268
211
  rscPluginApi = getRscPluginApi(config);
269
212
  },
270
213
 
214
+ buildEnd() {
215
+ counterTransform?.flush();
216
+ counterRender?.flush();
217
+ },
218
+
271
219
  buildStart() {
272
220
  // Verify RSC plugin is present at build start (after all config hooks have run)
273
221
  // This allows rsc-router:rsc-integration to dynamically add the RSC plugin
@@ -277,10 +225,8 @@ export function exposeActionId(): Plugin {
277
225
 
278
226
  if (!rscPluginApi) {
279
227
  throw new Error(
280
- "[rsc-router] Could not find @vitejs/plugin-rsc. " +
281
- "@rangojs/router requires the Vite RSC plugin.\n" +
282
- "The RSC plugin should be included automatically. If you disabled it with\n" +
283
- "rango({ rsc: false }), add rsc() before rango() in your config.",
228
+ "[rango] Could not find @vitejs/plugin-rsc. " +
229
+ "@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
284
230
  );
285
231
  }
286
232
 
@@ -326,40 +272,45 @@ export function exposeActionId(): Plugin {
326
272
  return;
327
273
  }
328
274
 
329
- // Dev mode: no hash-to-file mapping needed (IDs are already file paths)
330
- return transformServerReferences(code, id);
275
+ const start = counterTransform ? performance.now() : 0;
276
+ try {
277
+ return transformServerReferences(code, id);
278
+ } finally {
279
+ counterTransform?.record(id, performance.now() - start);
280
+ }
331
281
  },
332
282
 
333
- // Build mode: renderChunk runs after all transforms and bundling complete
334
283
  renderChunk(code, chunk) {
335
- // Only RSC bundle should get file paths for revalidation matching
336
- // SSR bundle must NOT use file paths because client components run there
337
- // and need to match the client bundle during hydration (otherwise: error #418)
338
- const isRscEnv = this.environment?.name === "rsc";
339
-
340
- // Only use file path mapping for RSC environment
341
- const effectiveMap = isRscEnv ? hashToFileMap : undefined;
342
-
343
- // For RSC bundles, both createServerReference and registerServerReference
344
- // may need transforming. Use a single MagicString for correct sourcemaps.
345
- if (isRscEnv && hashToFileMap) {
346
- const s = new MagicString(code);
347
- const changed1 = applyServerReferenceWrapping(code, s, effectiveMap);
348
- const changed2 = applyRegisterReferenceWrapping(code, s, hashToFileMap);
349
- if (changed1 || changed2) {
350
- return {
351
- code: s.toString(),
352
- map: s.generateMap({
353
- source: chunk.fileName,
354
- includeContent: true,
355
- }),
356
- };
284
+ const start = counterRender ? performance.now() : 0;
285
+ try {
286
+ const isRscEnv = this.environment?.name === "rsc";
287
+
288
+ const effectiveMap = isRscEnv ? hashToFileMap : undefined;
289
+
290
+ if (isRscEnv && hashToFileMap) {
291
+ const s = new MagicString(code);
292
+ const changed1 = applyServerReferenceWrapping(code, s, effectiveMap);
293
+ const changed2 = applyRegisterReferenceWrapping(
294
+ code,
295
+ s,
296
+ hashToFileMap,
297
+ );
298
+ if (changed1 || changed2) {
299
+ return {
300
+ code: s.toString(),
301
+ map: s.generateMap({
302
+ source: chunk.fileName,
303
+ includeContent: true,
304
+ }),
305
+ };
306
+ }
307
+ return null;
357
308
  }
358
- return null;
359
- }
360
309
 
361
- // Non-RSC environments: only transform createServerReference calls
362
- return transformServerReferences(code, chunk.fileName, effectiveMap);
310
+ return transformServerReferences(code, chunk.fileName, effectiveMap);
311
+ } finally {
312
+ counterRender?.record(chunk.fileName, performance.now() - start);
313
+ }
363
314
  },
364
315
  };
365
316
  }
@@ -8,30 +8,26 @@ export function normalizePath(p: string): string {
8
8
  return p.split(path.sep).join("/");
9
9
  }
10
10
 
11
- /**
12
- * Generate a short hash for an ID.
13
- * Uses first 8 chars of SHA-256 hash for uniqueness while keeping IDs short.
14
- * Appends export name for easier debugging in production: "abc123#ExportName"
15
- */
16
11
  export function hashId(filePath: string, exportName: string): string {
17
12
  const input = `${filePath}#${exportName}`;
18
13
  const hash = crypto.createHash("sha256").update(input).digest("hex");
19
14
  return `${hash.slice(0, 8)}#${exportName}`;
20
15
  }
21
16
 
22
- /**
23
- * Generate an 8-char hex hash for an inline static handler call site.
24
- * Uses file path and line number (plus optional index for same-line collisions).
25
- */
17
+ export function makeStubId(
18
+ filePath: string,
19
+ exportName: string,
20
+ isBuild: boolean,
21
+ ): string {
22
+ return isBuild ? hashId(filePath, exportName) : `${filePath}#${exportName}`;
23
+ }
24
+
26
25
  export function hashInlineId(
27
26
  filePath: string,
28
- lineNumber: number,
29
- index?: number,
27
+ fnName: string,
28
+ index: number,
30
29
  ): string {
31
- const input =
32
- index !== undefined && index > 0
33
- ? `${filePath}:${lineNumber}:${index}`
34
- : `${filePath}:${lineNumber}`;
30
+ const input = `${filePath}:${fnName}:${index}`;
35
31
  return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
36
32
  }
37
33
 
@@ -45,11 +41,6 @@ export interface DetectedImports {
45
41
  any: boolean;
46
42
  }
47
43
 
48
- /**
49
- * Build a map from local binding name to exported names by walking
50
- * ExportNamedDeclaration nodes. Handles `export const X`, `export { X }`,
51
- * and `export { X as Y }`. Skips re-exports (`export { X } from "..."`).
52
- */
53
44
  export function buildExportMap(program: any): Map<string, string[]> {
54
45
  const exportMap = new Map<string, string[]>();
55
46
 
@@ -89,10 +80,6 @@ export function buildExportMap(program: any): Map<string, string[]> {
89
80
  return exportMap;
90
81
  }
91
82
 
92
- /**
93
- * Single-pass detection of all create* imports from @rangojs/router.
94
- * Returns which create functions are imported so we can skip unnecessary transforms.
95
- */
96
83
  export function detectImports(code: string): DetectedImports {
97
84
  // Extract all import declarations from @rangojs/router in one scan
98
85
  const importPattern =
@@ -119,10 +106,6 @@ export function detectImports(code: string): DetectedImports {
119
106
  if (/\bcreateRouter\b/.test(imports)) result.router = true;
120
107
  }
121
108
 
122
- // createRouter has a stricter check: only from "@rangojs/router" (not sub-paths).
123
- // NOTE: This is intentional — detectImports is used as a fast pre-filter in
124
- // exposeInternalIds (which does NOT handle router transforms). The separate
125
- // exposeRouterId plugin handles createRouter and DOES accept the /server subpath.
126
109
  if (result.router) {
127
110
  result.router =
128
111
  /import\s*\{[^}]*\bcreateRouter\b[^}]*\}\s*from\s*["']@rangojs\/router["']/.test(
@@ -141,11 +124,6 @@ export function detectImports(code: string): DetectedImports {
141
124
  return result;
142
125
  }
143
126
 
144
- /**
145
- * Skip past a string literal, template literal, or comment starting at pos.
146
- * Returns the index after the closing delimiter, or pos if not at a
147
- * string/comment start. Handles escape sequences and nested ${} in templates.
148
- */
149
127
  export function skipStringOrComment(code: string, pos: number): number {
150
128
  const ch = code[pos];
151
129
 
@@ -203,11 +181,6 @@ export function skipStringOrComment(code: string, pos: number): number {
203
181
  return pos;
204
182
  }
205
183
 
206
- /**
207
- * Find the matching closing paren starting after an already-opened paren.
208
- * Skips strings, template literals, and comments so parens inside them
209
- * don't affect depth tracking. Returns the index after the closing paren.
210
- */
211
184
  export function findMatchingParen(code: string, startPos: number): number {
212
185
  let depth = 1;
213
186
  let i = startPos;
@@ -224,10 +197,6 @@ export function findMatchingParen(code: string, startPos: number): number {
224
197
  return i;
225
198
  }
226
199
 
227
- /**
228
- * Count the number of top-level arguments in a function call.
229
- * Skips nested parens, brackets, braces, strings, and comments.
230
- */
231
200
  export function countArgs(
232
201
  code: string,
233
202
  startPos: number,
@@ -263,10 +232,6 @@ export function countArgs(
263
232
  return hasContent ? argCount + 1 : 0;
264
233
  }
265
234
 
266
- /**
267
- * Find the end of a statement: skip whitespace and optional semicolon after
268
- * a closing paren position.
269
- */
270
235
  export function findStatementEnd(code: string, pos: number): number {
271
236
  let i = pos;
272
237
  while (i < code.length && /\s/.test(code[i])) {
@@ -278,10 +243,6 @@ export function findStatementEnd(code: string, pos: number): number {
278
243
  return i;
279
244
  }
280
245
 
281
- /**
282
- * Escape special regex characters in a string so it can be safely
283
- * interpolated into a RegExp pattern.
284
- */
285
246
  export function escapeRegExp(input: string): string {
286
247
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
287
248
  }
@@ -6,14 +6,9 @@ import {
6
6
  buildExportMap,
7
7
  escapeRegExp,
8
8
  } from "../expose-id-utils.js";
9
+ import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
9
10
  import type { CreateExportBinding } from "./types.js";
10
11
 
11
- /**
12
- * Check whether every non-type export in `code` is accounted for by the given
13
- * bindings. Returns false if any export exists that is not one of the known
14
- * create* call locals/exports, allowing callers to bail out for mixed-export
15
- * files.
16
- */
17
12
  export function isExportOnlyFile(
18
13
  code: string,
19
14
  bindings: CreateExportBinding[],
@@ -27,10 +22,8 @@ export function isExportOnlyFile(
27
22
  for (const e of b.exportNames) knownExports.add(e);
28
23
  }
29
24
 
30
- // Bail on star re-exports (unknown exports)
31
25
  if (/export\s*\*/.test(code)) return false;
32
26
 
33
- // Check `export const/let/var/function/class/default X` declarations
34
27
  const declExportPattern =
35
28
  /export\s+(const|let|var|function|class|default)\s+(\w+)/g;
36
29
  let match: RegExpExecArray | null;
@@ -38,8 +31,6 @@ export function isExportOnlyFile(
38
31
  if (!knownExports.has(match[2])) return false;
39
32
  }
40
33
 
41
- // Check `export { X }` and `export { X as Y }` specifiers: the local name
42
- // must reference a known create* binding.
43
34
  const specExportPattern = /export\s*\{([^}]+)\}/g;
44
35
  while ((match = specExportPattern.exec(code)) !== null) {
45
36
  const specifiers = match[1]
@@ -59,19 +50,45 @@ export function isExportOnlyFile(
59
50
  return true;
60
51
  }
61
52
 
62
- // NOTE: This regex may over-count when the fn name appears inside strings or
63
- // comments, but it's only used for the warning heuristic (totalCalls >
64
- // supportedBindings) and the inline-extraction pre-check, so over-counting
65
- // triggers a harmless extra AST parse rather than affecting correctness.
53
+ function createCallPattern(fnNames: string[]): RegExp {
54
+ return new RegExp(
55
+ `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
56
+ "g",
57
+ );
58
+ }
59
+
66
60
  export function countCreateCallsForNames(
67
61
  code: string,
68
62
  fnNames: string[],
69
63
  ): number {
70
- const pattern = new RegExp(
71
- `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
72
- "g",
73
- );
74
- return (code.match(pattern) || []).length;
64
+ return codeMatchIndices(code, createCallPattern(fnNames)).length;
65
+ }
66
+
67
+ export function offsetToLineColumn(
68
+ code: string,
69
+ index: number,
70
+ ): { line: number; column: number } {
71
+ let line = 1;
72
+ let lineStart = 0;
73
+ const end = Math.min(index, code.length);
74
+ for (let i = 0; i < end; i++) {
75
+ if (code[i] === "\n") {
76
+ line++;
77
+ lineStart = i + 1;
78
+ }
79
+ }
80
+ return { line, column: index - lineStart + 1 };
81
+ }
82
+
83
+ export function findUnsupportedCreateCallSites(
84
+ code: string,
85
+ fnNames: string[],
86
+ supportedBindings: CreateExportBinding[],
87
+ ): Array<{ line: number; column: number }> {
88
+ const supported = new Set(supportedBindings.map((b) => b.callExprStart));
89
+ return codeMatchIndices(code, createCallPattern(fnNames))
90
+ .filter((index) => !supported.has(index))
91
+ .map((index) => offsetToLineColumn(code, index));
75
92
  }
76
93
 
77
94
  export function getImportedFnNames(
@@ -119,6 +136,18 @@ export function getCalledIdentifierFromCall(callExpr: any): string | null {
119
136
  return null;
120
137
  }
121
138
 
139
+ function unwrapSignatureWrappedCall(init: any, fnNameSet: Set<string>): any {
140
+ if (init?.type !== "CallExpression") return init;
141
+ const directId = getCalledIdentifierFromCall(init);
142
+ if (directId && fnNameSet.has(directId)) return init;
143
+ const firstArg = init.arguments?.[0];
144
+ if (firstArg?.type === "CallExpression") {
145
+ const innerId = getCalledIdentifierFromCall(firstArg);
146
+ if (innerId && fnNameSet.has(innerId)) return firstArg;
147
+ }
148
+ return init;
149
+ }
150
+
122
151
  export function collectCreateExportBindingsFallback(
123
152
  code: string,
124
153
  fnNames: string[],
@@ -196,7 +225,7 @@ export function collectCreateExportBindings(
196
225
  ): CreateExportBinding[] {
197
226
  if (!program) {
198
227
  try {
199
- program = parseAst(code, { jsx: true });
228
+ program = parseAst(code, { lang: "tsx" });
200
229
  } catch {
201
230
  return collectCreateExportBindingsFallback(code, fnNames);
202
231
  }
@@ -212,10 +241,13 @@ export function collectCreateExportBindings(
212
241
  }
213
242
 
214
243
  for (const decl of varDecl.declarations ?? []) {
215
- const calledIdentifier = getCalledIdentifierFromCall(decl?.init);
244
+ // Unwrap a Fast Refresh signature wrapper (`_s(createLoader(...), ...)`)
245
+ // so injection targets the inner create* call. Falls back to decl.init.
246
+ const callExpr = unwrapSignatureWrappedCall(decl?.init, fnNameSet);
247
+ const calledIdentifier = getCalledIdentifierFromCall(callExpr);
216
248
  if (
217
249
  decl?.id?.type !== "Identifier" ||
218
- decl?.init?.type !== "CallExpression" ||
250
+ callExpr?.type !== "CallExpression" ||
219
251
  !calledIdentifier ||
220
252
  !fnNameSet.has(calledIdentifier)
221
253
  ) {
@@ -226,9 +258,8 @@ export function collectCreateExportBindings(
226
258
  const exportNames = exportMap.get(localName) ?? [];
227
259
  if (exportNames.length === 0) continue;
228
260
 
229
- const callStart = decl.init.start as number;
230
- const callEnd = decl.init.end as number;
231
- const calleeEnd = decl.init.callee.end as number;
261
+ const callEnd = callExpr.end as number;
262
+ const calleeEnd = callExpr.callee.end as number;
232
263
 
233
264
  let openParenPos = -1;
234
265
  for (let i = calleeEnd; i < callEnd; i++) {
@@ -245,10 +276,10 @@ export function collectCreateExportBindings(
245
276
  bindings.push({
246
277
  localName,
247
278
  exportNames,
248
- callExprStart: decl.init.start as number,
279
+ callExprStart: callExpr.start as number,
249
280
  callOpenParenPos: openParenPos,
250
281
  callCloseParenPos: closeParenPos,
251
- argCount: decl.init.arguments?.length ?? 0,
282
+ argCount: callExpr.arguments?.length ?? 0,
252
283
  statementEnd,
253
284
  });
254
285
  }
@@ -268,10 +299,6 @@ export function collectCreateExportBindings(
268
299
  }
269
300
  }
270
301
 
271
- // When the JS parser misidentifies TypeScript generics (e.g.,
272
- // createLocationState<{ text: string }>({...})) as binary expressions,
273
- // the AST path finds 0 bindings even though calls exist. Fall back to
274
- // regex-based detection which handles generics via <[^>]*> matching.
275
302
  if (bindings.length === 0) {
276
303
  return collectCreateExportBindingsFallback(code, fnNames);
277
304
  }
@@ -282,9 +309,23 @@ export function collectCreateExportBindings(
282
309
  export function buildUnsupportedShapeWarning(
283
310
  filePath: string,
284
311
  fnName: string,
312
+ sites: Array<{ line: number; column: number }> = [],
285
313
  ): string {
286
- return [
287
- `[rsc-router] Unsupported ${fnName} shape in "${filePath}".`,
314
+ const lines = [`[rango] Unsupported ${fnName} shape in "${filePath}".`];
315
+
316
+ if (sites.length === 1) {
317
+ const s = sites[0];
318
+ lines.push(
319
+ `The ${fnName}(...) call at ${filePath}:${s.line}:${s.column} has no stable $$id injected — it is not in a supported shape.`,
320
+ );
321
+ } else if (sites.length > 1) {
322
+ lines.push(
323
+ `These ${fnName}(...) calls have no stable $$id injected — they are not in a supported shape:`,
324
+ );
325
+ for (const s of sites) lines.push(` - ${filePath}:${s.line}:${s.column}`);
326
+ }
327
+
328
+ lines.push(
288
329
  `Supported shapes are:`,
289
330
  ` - export const X = ${fnName}(...)`,
290
331
  ` - const X = ${fnName}(...); export { X }`,
@@ -292,5 +333,6 @@ export function buildUnsupportedShapeWarning(
292
333
  `Potentially unsupported forms include:`,
293
334
  ` - export let/var X = ${fnName}(...)`,
294
335
  ` - inline ${fnName}(...) calls`,
295
- ].join("\n");
336
+ );
337
+ return lines.join("\n");
296
338
  }