@rangojs/router 0.20.0 → 0.22.0

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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -24,6 +24,14 @@ import { join, resolve } from "node:path";
24
24
  import { jsonParseExpression } from "../utils/manifest-utils.js";
25
25
  import { writeBuildAssetModule } from "../utils/prerender-utils.js";
26
26
  import { buildShellManifestKey } from "../../prerender/shell-manifest-key.js";
27
+ import {
28
+ resolveVersionsFrom,
29
+ type RouterVersionsTable,
30
+ } from "../../router-versions.js";
31
+ import {
32
+ environmentOutDir,
33
+ type BuilderLike,
34
+ } from "./router-versions-phase.js";
27
35
  // Type-only: the producer stages exactly the record shape the runtime
28
36
  // read-through consumes, so the contract cannot drift (the KEY half of that
29
37
  // contract is shell-manifest-key.ts). No runtime coupling — the RSC-runtime
@@ -35,30 +43,22 @@ import { createRangoDebugger, NS } from "../debug.js";
35
43
  const debug = createRangoDebugger(NS.prerender);
36
44
 
37
45
  /**
38
- * Minimal builder surface the phase reads: resolved plugins (for the main
39
- * build's version plugin) and per-environment outDirs.
46
+ * @param versionsTable The versions the shipped build serves with
47
+ * (router-versions-phase.ts), `undefined` outside a build. Never the temp
48
+ * server's own version-plugin stamp, which is a dev Date.now() and would
49
+ * fail the serve-side isValidShellHit gate forever.
40
50
  */
41
- interface BuilderLike {
42
- config?: {
43
- base?: string;
44
- plugins?: readonly unknown[];
45
- };
46
- environments?: Record<
47
- string,
48
- { config?: { build?: { outDir?: string } } } | undefined
49
- >;
50
- }
51
-
52
51
  export async function runShellPrerenderPhase(
53
52
  s: DiscoveryState,
54
53
  builder: BuilderLike | undefined,
54
+ versionsTable: RouterVersionsTable | undefined,
55
55
  ): Promise<void> {
56
56
  // The kept temp server (and the buildEnv deferred with it) is OWNED by the
57
57
  // callers — the buildApp post hook's finally on success, buildEnd on an
58
58
  // aborted build — so this function stays a pure producer: it only tears
59
59
  // down the globals it installs itself.
60
60
  const tempServer = s.shellPhaseTempServer;
61
- if (!s.isBuildMode || !s.shellCandidates?.length || !tempServer) return;
61
+ if (!versionsTable || !s.shellCandidates?.length || !tempServer) return;
62
62
 
63
63
  const candidates: ShellPrerenderCandidate[] = s.shellCandidates;
64
64
  const startTotal = performance.now();
@@ -87,21 +87,6 @@ export async function runShellPrerenderPhase(
87
87
  return;
88
88
  }
89
89
 
90
- // The MAIN build's version (the value folded into the shipped worker) —
91
- // never the temp server's own version-plugin stamp, which is a different
92
- // Date.now() and would fail the serve-side isValidShellHit gate forever.
93
- const versionPlugin = (builder?.config?.plugins ?? []).find(
94
- (p: any) => p?.name === "@rangojs/router:version",
95
- ) as { api?: { getBuildVersion?: () => string } } | undefined;
96
- const buildVersion = versionPlugin?.api?.getBuildVersion?.();
97
- if (!buildVersion) {
98
- console.warn(
99
- "[rango] shell prerender: main build version unavailable; skipping — " +
100
- "routes keep runtime shell capture.",
101
- );
102
- return;
103
- }
104
-
105
90
  // In-realm prerender store over the retained phase-A payloads, so the
106
91
  // capture's match() re-enters withCacheLookup and REPLAYS the build-time
107
92
  // segments (globalThis is shared between the plugin process and the temp
@@ -130,9 +115,7 @@ export async function runShellPrerenderPhase(
130
115
  );
131
116
 
132
117
  // Built client bootstrap: the prelude must embed the BUILT entry URL.
133
- const clientOutDir =
134
- builder?.environments?.client?.config?.build?.outDir ??
135
- resolve(s.projectRoot, "dist/client");
118
+ const clientOutDir = environmentOutDir(builder, s.projectRoot, "client");
136
119
  const clientAssetsDir = join(clientOutDir, "assets");
137
120
  const entryFile = existsSync(clientAssetsDir)
138
121
  ? readdirSync(clientAssetsDir).find((f) => /^index-.*\.js$/.test(f))
@@ -223,10 +206,10 @@ export async function runShellPrerenderPhase(
223
206
  const staged: Array<{ key: string; value: string }> = [];
224
207
  let skipCount = 0;
225
208
 
226
- // Manifest keys are pathname-only (see shell-manifest-key.ts): two
227
- // candidates on one pathname (two routers prerendering the same path)
228
- // would be ambiguous at serve time — decline ALL of that pathname's
229
- // entries rather than bake a wrong-router shell.
209
+ // A candidate names no router (the sweep below gives it to the first one
210
+ // whose match lands on its route): two candidates on one pathname (two
211
+ // routers prerendering the same path) would be ambiguous — decline ALL of
212
+ // that pathname's entries rather than bake a wrong-router shell.
230
213
  const pathCounts = new Map<string, number>();
231
214
  for (const cand of candidates) {
232
215
  pathCounts.set(cand.urlPath, (pathCounts.get(cand.urlPath) ?? 0) + 1);
@@ -245,8 +228,14 @@ export async function runShellPrerenderPhase(
245
228
  const mainKey = `${cand.routeName}/${cand.paramHash}`;
246
229
  let handled = false;
247
230
  const mismatches: string[] = [];
248
- for (const [, routerInstance] of registry) {
231
+ for (const [routerId, routerInstance] of registry) {
249
232
  if (typeof routerInstance.match !== "function") continue;
233
+ // The build's table always holds the whole-build pair.
234
+ const versions = resolveVersionsFrom(
235
+ versionsTable,
236
+ routerId,
237
+ routerInstance.version,
238
+ )!;
250
239
  try {
251
240
  const res = await captureMod.captureShellForBuild({
252
241
  router: routerInstance,
@@ -259,7 +248,7 @@ export async function runShellPrerenderPhase(
259
248
  maxSnapshotBytes: policy.maxSnapshotBytes,
260
249
  captureTimeout: policy.captureTimeout,
261
250
  buildEnv: s.resolvedBuildEnv,
262
- buildVersion,
251
+ versions,
263
252
  captureShellHTML,
264
253
  debug: !!debug,
265
254
  });
@@ -293,7 +282,7 @@ export async function runShellPrerenderPhase(
293
282
  tags: res.tags,
294
283
  };
295
284
  staged.push({
296
- key: buildShellManifestKey(cand.urlPath),
285
+ key: buildShellManifestKey(routerId, cand.urlPath),
297
286
  value: JSON.stringify(value),
298
287
  });
299
288
  console.log(
@@ -323,10 +312,11 @@ export async function runShellPrerenderPhase(
323
312
  }
324
313
 
325
314
  if (staged.length > 0) {
326
- const rscOutDir =
327
- builder?.environments?.rsc?.config?.build?.outDir ??
328
- resolve(s.projectRoot, "dist/rsc");
329
- writeShellManifest(s, rscOutDir, staged);
315
+ writeShellManifest(
316
+ s,
317
+ environmentOutDir(builder, s.projectRoot, "rsc"),
318
+ staged,
319
+ );
330
320
  }
331
321
 
332
322
  const totalElapsed = (performance.now() - startTotal).toFixed(0);
@@ -10,6 +10,7 @@ import type { ScanFilter } from "../../build/generate-route-types.js";
10
10
  import type { ClientUrlProjection } from "../../client-urls/server-projection.js";
11
11
 
12
12
  export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
13
+ export const VIRTUAL_LOADER_MANIFEST_ID = "virtual:rsc-router/loader-manifest";
13
14
 
14
15
  export interface PluginOptions {
15
16
  enableBuildPrerender?: boolean;
@@ -60,7 +61,12 @@ export interface PrecomputedEntry {
60
61
 
61
62
  export interface ChunkInfo {
62
63
  fileName: string;
63
- exports: Array<{ name: string; handlerId: string; passthrough: boolean }>;
64
+ exports: Array<{
65
+ name: string;
66
+ handlerId: string;
67
+ passthrough: boolean;
68
+ onDemand: boolean;
69
+ }>;
64
70
  }
65
71
 
66
72
  export interface PerRouterManifestEntry {
@@ -147,8 +153,31 @@ export interface DiscoveryState {
147
153
  * buildEnd (aborted build).
148
154
  */
149
155
  shellPhaseTempServer: import("vite").ViteDevServer | null;
156
+ /**
157
+ * Inputs of the per-router cache versions (build-versions.ts), recorded by
158
+ * generateBundle. plugin-rsc builds the RSC and SSR environments twice (a
159
+ * scan pass, then the real pass); each pass overwrites its slot, so the
160
+ * buildApp post hook reads the real pass.
161
+ */
162
+ serverBuildGraph: import("./build-versions.js").ServerBuildGraph | null;
163
+ clientBuildGraph: import("./build-versions.js").ClientBuildGraph | null;
164
+ ssrBundle: import("./build-versions.js").BundleFiles | null;
165
+ /** Per environment, the chunk files holding the version module. */
166
+ versionModuleFiles: Map<string, string[]>;
167
+ /** Prerender and Static payloads the build rendered, by content digest. */
168
+ buildData: import("./build-versions.js").BuildDataRecord[];
150
169
  handlerChunkInfoMap: Map<string, ChunkInfo>;
151
170
  staticHandlerChunkInfoMap: Map<string, ChunkInfo>;
171
+ /**
172
+ * handlerId ($$id) -> route name for every route the evaluated source marks
173
+ * onDemand. Written during discovery (buildStart), consumed by the
174
+ * generateBundle chunk scan (extractHandlerExportsFromChunk) as the
175
+ * authoritative onDemand set driving producer retention — the same source
176
+ * that sets the runtime od trie flag, so any truthy onDemand spelling
177
+ * (literal, spread, imported const) both marks the route and retains its
178
+ * producer.
179
+ */
180
+ onDemandHandlerIds: Map<string, string>;
152
181
  rscEntryFileName: string | null;
153
182
  resolvedPrerenderModules: Map<string, string[]> | undefined;
154
183
  resolvedStaticModules: Map<string, string[]> | undefined;
@@ -204,8 +233,14 @@ export function createDiscoveryState(
204
233
  shellCandidates: null,
205
234
  prerenderPayloadValues: null,
206
235
  shellPhaseTempServer: null,
236
+ serverBuildGraph: null,
237
+ clientBuildGraph: null,
238
+ ssrBundle: null,
239
+ versionModuleFiles: new Map(),
240
+ buildData: [],
207
241
  handlerChunkInfoMap: new Map(),
208
242
  staticHandlerChunkInfoMap: new Map(),
243
+ onDemandHandlerIds: new Map(),
209
244
  rscEntryFileName: null,
210
245
  resolvedPrerenderModules: undefined,
211
246
  resolvedStaticModules: undefined,
@@ -8,6 +8,7 @@
8
8
  import { mkdirSync, writeFileSync } from "node:fs";
9
9
  import { dirname, basename, join } from "node:path";
10
10
  import { jsonParseExpression } from "../utils/manifest-utils.js";
11
+ import { compareStrings } from "../utils/compare-strings.js";
11
12
  import { VIRTUAL_ROUTES_MANIFEST_ID } from "./state.js";
12
13
  import type { DiscoveryState } from "./state.js";
13
14
 
@@ -111,8 +112,12 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
111
112
 
112
113
  if (hasClientUrlModules) {
113
114
  lines.push(`clearClientUrlProjections();`);
114
- for (const [referenceId, projection] of state.clientUrlProjectionMap ??
115
- []) {
115
+ // Sorted: the map fills in transform order, which varies between runs,
116
+ // and this module's bytes feed cache versions (build-versions.ts).
117
+ const projections = [...(state.clientUrlProjectionMap ?? [])].sort(
118
+ ([a], [b]) => compareStrings(a, b),
119
+ );
120
+ for (const [referenceId, projection] of projections) {
116
121
  lines.push(
117
122
  `setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
118
123
  );
@@ -9,17 +9,104 @@ import { randomBytes } from "node:crypto";
9
9
  // via plugin-rsc's `defineEncryptionKey` so build-time-encrypted bound args
10
10
  // decrypt at runtime.
11
11
  //
12
- // Cached per process so the temp server (created during the main build's
13
- // buildStart) and the main build share one key. Overridable via
14
- // RANGO_ENCRYPTION_KEY for a stable key across builds/deploys; otherwise a fresh
15
- // key is generated per build (matching plugin-rsc's default posture, where the
16
- // key is emitted into the build output regardless).
17
- let cachedKey: string | undefined;
12
+ // Resolved once per process so the temp server (created during the main
13
+ // build's buildStart) and the main build share one key. Source order:
14
+ // rango({ encryptionKey }), then RANGO_ENCRYPTION_KEY, then a key generated for
15
+ // this build. Only the first two are stable across builds.
16
+ //
17
+ // The key is part of the cache version of every router whose server code
18
+ // encrypts with it (discovery/build-versions.ts adds the emitted key file to
19
+ // that router's hash): a payload cached under the old key carries bound args
20
+ // the new build cannot decrypt. So a generated key gives those routers a new
21
+ // version on every build, which is correct and also means their cache is
22
+ // cleared on every deploy.
23
+
24
+ /** AES key sizes Web Crypto imports for AES-GCM, in bytes. */
25
+ const AES_KEY_BYTES: ReadonlySet<number> = new Set([16, 24, 32]);
26
+
27
+ const EXPECTED =
28
+ "a base64-encoded AES key of 16, 24 or 32 bytes (openssl rand -base64 32)";
29
+
30
+ interface ResolvedEncryptionKey {
31
+ key: string;
32
+ /** True when the consumer supplied the key (option or env variable). */
33
+ stable: boolean;
34
+ }
35
+
36
+ let resolvedKey: ResolvedEncryptionKey | undefined;
37
+
38
+ /**
39
+ * Throws unless `value` is the base64 form of an AES key (16, 24 or 32 bytes).
40
+ * `source` names where the value came from, for the message.
41
+ */
42
+ export function assertValidEncryptionKey(value: unknown, source: string): void {
43
+ if (typeof value !== "string") {
44
+ throw new Error(
45
+ `[rango] ${source} must be a string, received ${typeof value}. Expected ${EXPECTED}.`,
46
+ );
47
+ }
48
+ if (value === "") {
49
+ throw new Error(
50
+ `[rango] ${source} is an empty string. Unset it to generate a key per build, ` +
51
+ `or set it to ${EXPECTED}.`,
52
+ );
53
+ }
54
+ const bytes = /^[A-Za-z0-9+/]+={0,2}$/.test(value)
55
+ ? Buffer.from(value, "base64")
56
+ : undefined;
57
+ // Buffer.from tolerates malformed base64 by dropping what it cannot decode;
58
+ // the round-trip rejects those inputs instead of using a shorter key.
59
+ if (
60
+ !bytes ||
61
+ bytes.toString("base64").replace(/=+$/, "") !== value.replace(/=+$/, "")
62
+ ) {
63
+ throw new Error(
64
+ `[rango] ${source} is not valid base64. Expected ${EXPECTED}.`,
65
+ );
66
+ }
67
+ if (!AES_KEY_BYTES.has(bytes.length)) {
68
+ throw new Error(
69
+ `[rango] ${source} decodes to ${bytes.length} bytes. Expected ${EXPECTED}.`,
70
+ );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Resolve the build's encryption key from rango({ encryptionKey }) or the
76
+ * RANGO_ENCRYPTION_KEY fallback, validating whichever is set. Called by
77
+ * rango() at config time, before any plugin-rsc instance reads the key, so an
78
+ * invalid key fails the config load instead of the first encrypted action.
79
+ * A key generated earlier in the process is kept when neither source is set:
80
+ * the discovery temp server must encrypt with the main build's key.
81
+ */
82
+ export function configureEncryptionKey(option: string | undefined): void {
83
+ if (option !== undefined) {
84
+ assertValidEncryptionKey(option, "rango({ encryptionKey })");
85
+ resolvedKey = { key: option, stable: true };
86
+ return;
87
+ }
88
+ const fromEnv = process.env.RANGO_ENCRYPTION_KEY;
89
+ if (fromEnv !== undefined) {
90
+ assertValidEncryptionKey(fromEnv, "RANGO_ENCRYPTION_KEY");
91
+ resolvedKey = { key: fromEnv, stable: true };
92
+ return;
93
+ }
94
+ if (resolvedKey && !resolvedKey.stable) return;
95
+ resolvedKey = { key: randomBytes(32).toString("base64"), stable: false };
96
+ }
97
+
98
+ function resolved(): ResolvedEncryptionKey {
99
+ if (!resolvedKey) configureEncryptionKey(undefined);
100
+ return resolvedKey!;
101
+ }
18
102
 
19
103
  export function buildEncryptionKey(): string {
20
- cachedKey ??=
21
- process.env.RANGO_ENCRYPTION_KEY ?? randomBytes(32).toString("base64");
22
- return cachedKey;
104
+ return resolved().key;
105
+ }
106
+
107
+ /** False when this build generated its own key (it differs on every build). */
108
+ export function isEncryptionKeyStable(): boolean {
109
+ return resolved().stable;
23
110
  }
24
111
 
25
112
  // The value plugin-rsc inlines as `export default () => (<expr>)`. A JSON string
@@ -27,3 +114,8 @@ export function buildEncryptionKey(): string {
27
114
  export function defineEncryptionKeyExpr(): string {
28
115
  return JSON.stringify(buildEncryptionKey());
29
116
  }
117
+
118
+ /** @internal Test seam: forget the process-wide key. */
119
+ export function resetEncryptionKeyForTests(): void {
120
+ resolvedKey = undefined;
121
+ }
@@ -12,37 +12,59 @@ import type { Connect } from "vite";
12
12
  * runs on the module regardless of how (or whether) the define is delivered, in
13
13
  * both dev and build and for every environment, so the discovery plugin uses this
14
14
  * to replace the module with the resolved literal.
15
+ *
16
+ * internal-suspense-audit.ts takes the same path for INTERNAL_RANGO_SUSPENSE_AUDIT,
17
+ * the repo-only switch of the dev suspense audit.
15
18
  */
16
19
 
20
+ // The router's flag modules, by file name: each exports one boolean named after
21
+ // the env var it is baked from.
22
+ const FLAG_MODULE =
23
+ /\/(internal-debug|internal-suspense-audit)\.[cm]?[jt]sx?(\?|$)/;
24
+ const FLAG_NAMES: Record<string, string> = {
25
+ "internal-debug": "INTERNAL_RANGO_DEBUG",
26
+ "internal-suspense-audit": "INTERNAL_RANGO_SUSPENSE_AUDIT",
27
+ };
28
+
17
29
  /**
18
- * Scope to the router's own internal-debug module: the published package
19
- * (`/@rangojs/router/`, incl. pnpm's nested layout) or the monorepo workspace
20
- * (`/packages/rangojs-router/`). The package-anchored path avoids matching a
21
- * consumer file that merely sits under a directory named `rangojs-router`.
22
- * Accepts module ids and dev-server URLs (`/@fs/...internal-debug.ts?v=abc`).
30
+ * The env var a router flag module is baked from, or undefined. Scoped to the
31
+ * router's own modules: the published package (`/@rangojs/router/`, incl.
32
+ * pnpm's nested layout) or the monorepo workspace (`/packages/rangojs-router/`).
33
+ * The package-anchored path avoids matching a consumer file that merely sits
34
+ * under a directory named `rangojs-router`. Accepts module ids and dev-server
35
+ * URLs (`/@fs/...internal-debug.ts?v=abc`).
23
36
  */
24
- export function isRouterInternalDebugId(id: string): boolean {
25
- if (!id.includes("internal-debug")) return false;
37
+ function routerFlagName(id: string): string | undefined {
38
+ if (!id.includes("internal-")) return undefined;
26
39
  const norm = id.replace(/\\/g, "/");
27
- return (
28
- /\/internal-debug\.[cm]?[jt]sx?(\?|$)/.test(norm) &&
29
- (norm.includes("/@rangojs/router/") ||
30
- norm.includes("/packages/rangojs-router/"))
31
- );
40
+ if (
41
+ !norm.includes("/@rangojs/router/") &&
42
+ !norm.includes("/packages/rangojs-router/")
43
+ ) {
44
+ return undefined;
45
+ }
46
+ const match = FLAG_MODULE.exec(norm);
47
+ return match ? FLAG_NAMES[match[1]!] : undefined;
48
+ }
49
+
50
+ /** A router flag module: internal-debug.ts or internal-suspense-audit.ts. */
51
+ export function isRouterInternalDebugId(id: string): boolean {
52
+ return routerFlagName(id) !== undefined;
32
53
  }
33
54
 
34
55
  /**
35
- * Transform: replace the module with the resolved literal.
36
- * Returns null for any module that is not the router's internal-debug module.
56
+ * Transform: replace a router flag module with the resolved literal.
57
+ * Returns null for any other module.
37
58
  */
38
59
  export function injectClientDebugFlag(
39
60
  id: string,
40
61
  ): { code: string; map: null } | null {
41
62
  // Cheap early-out: this hook runs on every module in every environment.
42
- if (!isRouterInternalDebugId(id)) return null;
43
- // Emit the whole module: internal-debug.ts has a single export, kept in sync.
63
+ const name = routerFlagName(id);
64
+ if (name === undefined) return null;
65
+ // Emit the whole module: each flag module has a single export, kept in sync.
44
66
  return {
45
- code: `export const INTERNAL_RANGO_DEBUG = ${!!process.env.INTERNAL_RANGO_DEBUG};\n`,
67
+ code: `export const ${name} = ${!!process.env[name]};\n`,
46
68
  map: null,
47
69
  };
48
70
  }
@@ -63,9 +85,9 @@ export function injectClientDebugFlag(
63
85
  * immune -- workspace source is outside node_modules and served no-cache --
64
86
  * which is why this bit only npm consumers.
65
87
  *
66
- * internal-debug.ts is the ONLY flag-varying module in the graph (its importers
67
- * are byte-identical across flag states), so forcing revalidation for this one
68
- * tiny module is sufficient and costs one conditional request per session. The
88
+ * The two flag modules are the only flag-varying modules in the graph (their
89
+ * importers are byte-identical across flag states), so forcing revalidation for
90
+ * them is sufficient and costs a conditional request each per session. The
69
91
  * middleware is unconditional (not gated on the flag) so an already-poisoned
70
92
  * cache heals in both directions. Alternatives that do not work: a plugin
71
93
  * `resolveId` appending a flag query never fires in Vite 8 dev for
@@ -239,6 +239,34 @@ interface RangoBaseOptions {
239
239
  prerender?: {
240
240
  onError?: "fail" | "warn";
241
241
  };
242
+
243
+ /**
244
+ * The key inline server actions encrypt their bound arguments with:
245
+ * base64-encoded 32 bytes (`openssl rand -base64 32`). Pass it from the
246
+ * environment, never as a literal in the config file:
247
+ *
248
+ * ```ts
249
+ * rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY })
250
+ * ```
251
+ *
252
+ * Why set it: the key is part of the cache version of every router whose
253
+ * server code encrypts with it, because a payload cached under one key
254
+ * carries arguments another key cannot decrypt. Without a stable key rango
255
+ * generates one per build, so those routers get a new version, and a cleared
256
+ * cache, on every deploy. With a stable key their cache survives a deploy
257
+ * that does not change their server code. It also lets a build decrypt
258
+ * arguments an earlier build encrypted (a cached or prerendered payload, an
259
+ * open tab).
260
+ *
261
+ * Validated when `rango()` is called: a value that is not base64 or does not
262
+ * decode to an AES key size (16, 24 or 32 bytes) throws. `undefined` (the
263
+ * variable is unset) falls back to the `RANGO_ENCRYPTION_KEY` environment
264
+ * variable, then to a generated key. Rotating the key clears the cache of
265
+ * the routers that use it.
266
+ *
267
+ * @default process.env.RANGO_ENCRYPTION_KEY, else a key generated per build
268
+ */
269
+ encryptionKey?: string;
242
270
  }
243
271
 
244
272
  /**
@@ -5,11 +5,9 @@ import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
5
5
  import { RSC_ENTRY_BOOTSTRAP_IMPORTS } from "./virtual-entries.js";
6
6
 
7
7
  /**
8
- * Plugin that auto-injects VERSION, routes-manifest, and loader-manifest into
9
- * custom entry.rsc files. If a custom entry.rsc file uses createRSCHandler but
10
- * doesn't pass version, this transform adds the import and property
11
- * automatically. It also ensures the routes-manifest and loader-manifest
12
- * virtual modules are always imported.
8
+ * Plugin that auto-injects the routes-manifest and loader-manifest imports
9
+ * into a custom entry.rsc file (the Cloudflare preset, where the consumer owns
10
+ * the worker entry).
13
11
  *
14
12
  * The loader-manifest import is what makes fetchable loaders resolvable on a
15
13
  * custom worker entry. The virtual RSC entry (getVirtualEntryRSC) imports the
@@ -23,13 +21,13 @@ import { RSC_ENTRY_BOOTSTRAP_IMPORTS } from "./virtual-entries.js";
23
21
  * manifest, hence the production-only failure this fixes.
24
22
  * @internal
25
23
  */
26
- export function createVersionInjectorPlugin(
24
+ export function createEntryBootstrapInjectorPlugin(
27
25
  rscEntryPath: string | undefined,
28
26
  ): Plugin {
29
27
  let resolvedEntryPath = "";
30
28
 
31
29
  return {
32
- name: "@rangojs/router:version-injector",
30
+ name: "@rangojs/router:entry-bootstrap-injector",
33
31
  enforce: "pre",
34
32
 
35
33
  configResolved(config) {
@@ -54,21 +52,8 @@ export function createVersionInjectorPlugin(
54
52
  const prepend: string[] = RSC_ENTRY_BOOTSTRAP_IMPORTS.map(
55
53
  (id) => `import "${id}";`,
56
54
  );
57
- let newCode = code;
58
- const needsVersion =
59
- code.includes("createRSCHandler") &&
60
- !code.includes("@rangojs/router:version") &&
61
- /createRSCHandler\s*\(\s*\{/.test(code);
62
55
 
63
- if (needsVersion) {
64
- prepend.push(`import { VERSION } from "@rangojs/router:version";`);
65
- newCode = newCode.replace(
66
- /createRSCHandler\s*\(\s*\{/,
67
- "createRSCHandler({\n version: VERSION,",
68
- );
69
- }
70
-
71
- const lines = newCode.split("\n");
56
+ const lines = code.split("\n");
72
57
  let insertAt = 0;
73
58
  while (insertAt < lines.length) {
74
59
  const trimmed = lines[insertAt]!.trim();
@@ -78,7 +63,7 @@ export function createVersionInjectorPlugin(
78
63
  break;
79
64
  }
80
65
  }
81
- newCode = [
66
+ const newCode = [
82
67
  ...lines.slice(0, insertAt),
83
68
  ...prepend,
84
69
  ...lines.slice(insertAt),
@@ -55,7 +55,7 @@ export function isExportOnlyFile(
55
55
  // (which may be nested, e.g. createLoader<A<B>>(...)) and the call paren are
56
56
  // resolved per match via findCallParenAfterGenerics, so a nested `>` no longer
57
57
  // defeats the scan the way `<[^>]*>` did (it stopped at the first `>`).
58
- function createCallPattern(fnNames: string[]): RegExp {
58
+ export function createCallPattern(fnNames: string[]): RegExp {
59
59
  return new RegExp(`\\b(?:${fnNames.map(escapeRegExp).join("|")})\\b`, "g");
60
60
  }
61
61