@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
@@ -33,6 +33,10 @@ export function postprocessBundle(state: DiscoveryState): void {
33
33
  );
34
34
 
35
35
  // 1. Evict handler code from whichever chunks contain handler exports.
36
+ // onDemand retention needs no cross-check here: the chunk scan marks
37
+ // onDemand exports directly from state.onDemandHandlerIds (the evaluated
38
+ // source's $$id set), so retention and the runtime od trie flag share one
39
+ // derivation and cannot disagree.
36
40
  // handlerChunkInfoMap/staticHandlerChunkInfoMap are populated by generateBundle
37
41
  // after the production RSC build. In Vite 6 multi-environment builds, the
38
42
  // RSC build runs twice (analysis + production). The maps are cleared at the
@@ -337,6 +337,20 @@ export async function discoverRouters(
337
337
  (performance.now() - trieStart).toFixed(1),
338
338
  );
339
339
 
340
+ // handlerId -> route name for onDemand routes, from the evaluated source —
341
+ // the authoritative side of postprocessBundle's retention cross-check.
342
+ const newOnDemandHandlerIds = new Map<string, string>();
343
+ for (const { manifest } of allManifests) {
344
+ if (!manifest.onDemandRoutes) continue;
345
+ const defs = manifest._prerenderDefs || {};
346
+ for (const routeName of manifest.onDemandRoutes) {
347
+ const handlerId = defs[routeName]?.$$id;
348
+ if (typeof handlerId === "string") {
349
+ newOnDemandHandlerIds.set(handlerId, routeName);
350
+ }
351
+ }
352
+ }
353
+
340
354
  // Commit all local state to the shared discovery state atomically.
341
355
  // This ensures a failed re-discovery (e.g. from a transient module
342
356
  // evaluation error) preserves the last known-good state.
@@ -345,6 +359,7 @@ export async function discoverRouters(
345
359
  state.perRouterManifestDataMap = newPerRouterManifestDataMap;
346
360
  state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
347
361
  state.perRouterTrieMap = newPerRouterTrieMap;
362
+ state.onDemandHandlerIds = newOnDemandHandlerIds;
348
363
 
349
364
  // Install the route tries into the RSC realm BEFORE prerender collection.
350
365
  // matchForPrerender resolves each enumerated URL via findMatch, and without
@@ -0,0 +1,159 @@
1
+ /**
2
+ * What is installed for a package a server build leaves external.
3
+ *
4
+ * The node preset bundles only packages that need the React server condition
5
+ * (plugin-rsc's `noExternal`); every other dependency stays an import the
6
+ * deployed process resolves from node_modules. The built chunk then says
7
+ * `from "pkg"` whichever version is installed, so a lockfile bump changes what
8
+ * the server renders and nothing the cache versions hash
9
+ * (discovery/build-versions.ts). This module supplies the missing input: the
10
+ * `name@version` of the package and of everything it depends on, as installed
11
+ * next to the project at build time.
12
+ *
13
+ * A registry package's code is fixed by its version. A linked package (a
14
+ * workspace package, resolved to a directory outside node_modules) is not.
15
+ * Vite bundles those unless the config lists one in `resolve.external`; then
16
+ * it comes through here and its files are digested instead.
17
+ */
18
+
19
+ import { createHash } from "node:crypto";
20
+ import {
21
+ existsSync,
22
+ readdirSync,
23
+ readFileSync,
24
+ realpathSync,
25
+ statSync,
26
+ type Stats,
27
+ } from "node:fs";
28
+ import { dirname, join, sep } from "node:path";
29
+ import { compareStrings } from "../utils/compare-strings.js";
30
+
31
+ interface InstalledPackage {
32
+ readonly label: string;
33
+ readonly dependencies: readonly string[];
34
+ }
35
+
36
+ /** What a symlink points at, or nothing for a dangling or circular one: an
37
+ * import could not load it either. */
38
+ function linkTarget(path: string): Stats | undefined {
39
+ try {
40
+ return statSync(path);
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Digest of every file under `dir`, by relative path, skipping installs.
48
+ * Symlinks are followed (a linked file is importable like any other); a
49
+ * directory is entered once, so a link back up the tree ends there.
50
+ */
51
+ function digestDirectory(dir: string): string {
52
+ const hash = createHash("sha256");
53
+ const entered = new Set<string>();
54
+ const visit = (current: string, prefix: string): void => {
55
+ const real = realpathSync(current);
56
+ if (entered.has(real)) return;
57
+ entered.add(real);
58
+ const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) =>
59
+ compareStrings(a.name, b.name),
60
+ );
61
+ for (const entry of entries) {
62
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
63
+ const path = join(current, entry.name);
64
+ const name = `${prefix}${entry.name}`;
65
+ const kind = entry.isSymbolicLink() ? linkTarget(path) : entry;
66
+ if (kind?.isDirectory()) {
67
+ visit(path, `${name}/`);
68
+ } else if (kind?.isFile()) {
69
+ const bytes = readFileSync(path);
70
+ hash.update(`${name}\0${bytes.byteLength}\0`);
71
+ hash.update(bytes);
72
+ }
73
+ }
74
+ };
75
+ visit(dir, "");
76
+ return hash.digest("hex");
77
+ }
78
+
79
+ /** Node's lookup for a bare specifier: the nearest node_modules holding it. */
80
+ function findPackageDir(name: string, from: string): string | undefined {
81
+ for (let dir = from; ; ) {
82
+ const candidate = join(dir, "node_modules", name);
83
+ if (existsSync(join(candidate, "package.json"))) {
84
+ return realpathSync(candidate);
85
+ }
86
+ const parent = dirname(dir);
87
+ if (parent === dir) return undefined;
88
+ dir = parent;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Returns a function describing an installed package by name: the sorted
94
+ * `name@version` lines of the package and its transitive dependencies
95
+ * (dependencies, optionalDependencies and peerDependencies, each resolved from
96
+ * the package that declares it), with a digest of its files for a linked
97
+ * one, or `undefined` when the package is not installed. Results are kept per
98
+ * name for the life of the returned function.
99
+ */
100
+ export function createInstalledPackageDescriber(
101
+ projectRoot: string,
102
+ ): (name: string) => string | undefined {
103
+ const packages = new Map<string, InstalledPackage | undefined>();
104
+ const read = (dir: string): InstalledPackage | undefined => {
105
+ if (packages.has(dir)) return packages.get(dir);
106
+ let manifest: any;
107
+ try {
108
+ manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
109
+ } catch {
110
+ packages.set(dir, undefined);
111
+ return undefined;
112
+ }
113
+ // Outside the try: a linked package whose files cannot be read must fail
114
+ // the build, not drop out of the version.
115
+ const linked = !dir.split(sep).includes("node_modules");
116
+ const found: InstalledPackage = {
117
+ label:
118
+ `${manifest.name}@${manifest.version}` +
119
+ (linked ? ` files:${digestDirectory(dir)}` : ""),
120
+ dependencies: Object.keys({
121
+ ...manifest.dependencies,
122
+ ...manifest.optionalDependencies,
123
+ ...manifest.peerDependencies,
124
+ }),
125
+ };
126
+ packages.set(dir, found);
127
+ return found;
128
+ };
129
+
130
+ // Packages share most of their dependencies (the same peers, the same
131
+ // utilities): each lookup is done once for every package described.
132
+ const located = new Map<string, string | undefined>();
133
+ const locate = (name: string, from: string): string | undefined => {
134
+ const key = `${from}\0${name}`;
135
+ if (!located.has(key)) located.set(key, findPackageDir(name, from));
136
+ return located.get(key);
137
+ };
138
+
139
+ const described = new Map<string, string | undefined>();
140
+ return (name) => {
141
+ if (described.has(name)) return described.get(name);
142
+ const labels = new Set<string>();
143
+ const seen = new Set<string>();
144
+ const pending: Array<[name: string, from: string]> = [[name, projectRoot]];
145
+ while (pending.length > 0) {
146
+ const [dependency, from] = pending.pop()!;
147
+ const dir = locate(dependency, from);
148
+ if (dir === undefined || seen.has(dir)) continue;
149
+ seen.add(dir);
150
+ const installed = read(dir);
151
+ if (!installed) continue;
152
+ labels.add(installed.label);
153
+ for (const next of installed.dependencies) pending.push([next, dir]);
154
+ }
155
+ const text = labels.size > 0 ? [...labels].sort().join("\n") : undefined;
156
+ described.set(name, text);
157
+ return text;
158
+ };
159
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { contextSet, hasContextVars } from "../../context-var.js";
10
+ import { sha256 } from "./build-versions.js";
10
11
  import {
11
12
  encodePathParam,
12
13
  substituteRouteParams,
@@ -21,6 +22,27 @@ import { createRangoDebugger, NS } from "../debug.js";
21
22
 
22
23
  const debug = createRangoDebugger(NS.prerender);
23
24
 
25
+ /**
26
+ * Note a payload the build rendered and will ship as an asset module, for the
27
+ * owning router's cache version (build-versions.ts). The handler that produced
28
+ * it is evicted from the bundle, so the payload is the only thing left in the
29
+ * server output that changes when the handler or its build-time data does.
30
+ */
31
+ function recordBuildData(
32
+ state: DiscoveryState,
33
+ kind: "prerender" | "static",
34
+ key: string,
35
+ value: string,
36
+ owner: { routerId?: string; moduleId?: string },
37
+ ): void {
38
+ state.buildData.push({
39
+ kind,
40
+ key,
41
+ digest: sha256(value),
42
+ ...owner,
43
+ });
44
+ }
45
+
24
46
  /**
25
47
  * Expand prerender routes into concrete URLs and render them via the
26
48
  * RSC runner. Stages asset modules and stores key-to-file entries in
@@ -248,7 +270,7 @@ export async function expandPrerenderRoutes(
248
270
  group.concurrency,
249
271
  async (entry) => {
250
272
  const startUrl = performance.now();
251
- for (const [, routerInstance] of registry) {
273
+ for (const [routerId, routerInstance] of registry) {
252
274
  if (!routerInstance.matchForPrerender) continue;
253
275
  try {
254
276
  const result = await routerInstance.matchForPrerender(
@@ -281,6 +303,9 @@ export async function expandPrerenderRoutes(
281
303
  "__pr",
282
304
  mainValue,
283
305
  );
306
+ recordBuildData(state, "prerender", mainKey, mainValue, {
307
+ routerId,
308
+ });
284
309
  // Prerender + ppr composition: flag the URL as a build-time shell
285
310
  // candidate for the post-build capture phase (producer B, #699).
286
311
  // The payload JSON is retained in memory so that phase can seed an
@@ -310,6 +335,15 @@ export async function expandPrerenderRoutes(
310
335
  "__pr",
311
336
  interceptValue,
312
337
  );
338
+ recordBuildData(
339
+ state,
340
+ "prerender",
341
+ interceptKey,
342
+ interceptValue,
343
+ {
344
+ routerId,
345
+ },
346
+ );
313
347
  }
314
348
  const elapsed = (performance.now() - startUrl).toFixed(0);
315
349
  console.log(
@@ -447,6 +481,9 @@ export async function renderStaticHandlers(
447
481
  "__st",
448
482
  exportValue,
449
483
  );
484
+ recordBuildData(state, "static", def.$$id, exportValue, {
485
+ moduleId,
486
+ });
450
487
  const elapsed = (performance.now() - startHandler).toFixed(0);
451
488
  console.log(`[rango] OK ${name.padEnd(40)} (${elapsed}ms)`);
452
489
  staticDone++;
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Post-build cache version phase.
3
+ *
4
+ * Runs from the buildApp post hook, after every environment bundle is written
5
+ * and postprocessBundle has rewritten the RSC chunks, and before the shell
6
+ * capture phase, which stamps its entries with the document versions computed
7
+ * here. Reads the bytes that ship, computes each router's versions
8
+ * (build-versions.ts) and writes the table into the built
9
+ * `@rangojs/router:version` module in place of its placeholder.
10
+ */
11
+
12
+ import { randomUUID } from "node:crypto";
13
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import {
16
+ DEFAULT_ROUTER_VERSIONS_KEY,
17
+ type RouterVersionsTable,
18
+ } from "../../router-versions.js";
19
+ import { isEncryptionKeyStable } from "../encryption-key.js";
20
+ import { createRangoDebugger, NS } from "../debug.js";
21
+ import {
22
+ defaultExportProperties,
23
+ literalProperties,
24
+ } from "../plugins/stable-rsc-output.js";
25
+ import { RANGO_BUILD_DIR } from "../utils/prerender-utils.js";
26
+ import {
27
+ computeRouterVersions,
28
+ fillRouterVersions,
29
+ portableModuleId,
30
+ sha256,
31
+ type BuildDataRecord,
32
+ type ComputedRouterVersions,
33
+ } from "./build-versions.js";
34
+ import { createInstalledPackageDescriber } from "./installed-packages.js";
35
+ import type { DiscoveryState } from "./state.js";
36
+
37
+ const debug = createRangoDebugger(NS.build);
38
+
39
+ /** plugin-rsc's client asset manifest, written into the SSR and RSC out dirs. */
40
+ const ASSETS_MANIFEST_FILE = "__vite_rsc_assets_manifest.js";
41
+ const EMPTY_SERVER_RESOURCES = /"serverResources":\s*\{\s*\}/;
42
+
43
+ /** Minimal builder surface the post-build phases read. */
44
+ export interface BuilderLike {
45
+ config?: { base?: string };
46
+ environments?: Record<
47
+ string,
48
+ { config?: { build?: { outDir?: string } } } | undefined
49
+ >;
50
+ }
51
+
52
+ /**
53
+ * An environment's output directory, or Vite's default layout without one.
54
+ * Resolved against the root: a relative `build.outDir` is relative to it, not
55
+ * to the directory `vite build <root>` was run from.
56
+ */
57
+ export function environmentOutDir(
58
+ builder: BuilderLike | undefined,
59
+ projectRoot: string,
60
+ environment: string,
61
+ ): string {
62
+ return resolve(
63
+ projectRoot,
64
+ builder?.environments?.[environment]?.config?.build?.outDir ??
65
+ join("dist", environment),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Where the build leaves what each version was computed from. Outside the
71
+ * output directories, so it never ships.
72
+ */
73
+ export const VERSIONS_REPORT_FILE: string = `${RANGO_BUILD_DIR}/cache-versions.json`;
74
+
75
+ function readIfPresent(path: string): Buffer | undefined {
76
+ try {
77
+ return readFileSync(path);
78
+ } catch (err: any) {
79
+ if (err?.code === "ENOENT") return undefined;
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * The `serverResources` map of plugin-rsc's assets manifest
86
+ * (`export default { ..., serverResources: { "<module>": { js, css } } }`):
87
+ * each server module that imports CSS, to the source text of its entry. Text,
88
+ * not a parsed value: with `renderBuiltUrl` an entry holds runtime expressions.
89
+ * Throws when the manifest is not that shape, so a plugin-rsc that moves it
90
+ * fails the build instead of dropping stylesheets from the versions.
91
+ */
92
+ export function readServerResources(manifest: string): Map<string, string> {
93
+ // Most apps import no CSS from a server component: skip parsing a manifest
94
+ // that lists every client reference. (Inside a JSON string the quotes would
95
+ // be escaped, so this text can only be the property itself.)
96
+ if (EMPTY_SERVER_RESOURCES.test(manifest)) return new Map();
97
+ const entries = literalProperties(
98
+ defaultExportProperties(manifest)?.find(
99
+ (property) => property.key === "serverResources",
100
+ )?.value,
101
+ );
102
+ if (!entries) {
103
+ throw new Error(
104
+ `${ASSETS_MANIFEST_FILE} has no serverResources object literal: the installed ` +
105
+ `@vitejs/plugin-rsc writes a manifest @rangojs/router cannot read. ` +
106
+ `Report it with the plugin-rsc version.`,
107
+ );
108
+ }
109
+ return new Map(
110
+ entries.map((entry) => [
111
+ entry.key,
112
+ manifest.slice(entry.value.start, entry.value.end),
113
+ ]),
114
+ );
115
+ }
116
+
117
+ /**
118
+ * One record per clientUrls() module: the projection the server materializes
119
+ * the group from. The map holds it under each of the module's reference ids.
120
+ */
121
+ function clientUrlRecords(s: DiscoveryState): BuildDataRecord[] {
122
+ const projectionOf = new Map<string, string>();
123
+ for (const [referenceId, projection] of s.clientUrlProjectionMap ?? []) {
124
+ projectionOf.set(
125
+ s.clientUrlSourceByReferenceId?.get(referenceId) ?? referenceId,
126
+ JSON.stringify(projection),
127
+ );
128
+ }
129
+ return [...projectionOf].map(([source, projection]) => ({
130
+ kind: "client-urls",
131
+ key: portableModuleId(source, s.projectRoot),
132
+ digest: sha256(projection),
133
+ moduleId: source,
134
+ }));
135
+ }
136
+
137
+ /**
138
+ * Write every version's inputs as `name: digest`. A version is one hash, so a
139
+ * changed version says nothing about what changed; diffing two builds' reports
140
+ * names the file, payload or key that did.
141
+ */
142
+ function writeVersionsReport(
143
+ projectRoot: string,
144
+ computed: ComputedRouterVersions,
145
+ sourceOf: ReadonlyMap<string, string>,
146
+ ): void {
147
+ const routers: Record<string, unknown> = {};
148
+ for (const detail of computed.details) {
149
+ routers[detail.routerId] = {
150
+ source: sourceOf.get(detail.routerId) ?? "",
151
+ data: detail.data,
152
+ document: detail.document,
153
+ dataInputs: Object.fromEntries(detail.dataInputs),
154
+ documentInputs: Object.fromEntries(detail.documentInputs),
155
+ };
156
+ }
157
+ // Listed once, not per router. `ssrAndClient`: what the `ssr-and-client`
158
+ // input of every document version is a digest of. `unownedFiles`: server
159
+ // files in no router's version but the whole build's (the host entry, the
160
+ // registries); a change there keeps every app's cache.
161
+ const report = {
162
+ routers,
163
+ ssrAndClient: {
164
+ base: computed.ssrAndClient.base,
165
+ ssr: Object.fromEntries(computed.ssrAndClient.ssr),
166
+ externals: Object.fromEntries(computed.ssrAndClient.externals),
167
+ client: computed.ssrAndClient.client,
168
+ },
169
+ unownedFiles: computed.unownedFiles,
170
+ };
171
+ try {
172
+ const path = resolve(projectRoot, VERSIONS_REPORT_FILE);
173
+ mkdirSync(dirname(path), { recursive: true });
174
+ writeFileSync(path, JSON.stringify(report, null, 2) + "\n");
175
+ } catch (err: any) {
176
+ debug?.("could not write %s: %s", VERSIONS_REPORT_FILE, err?.message);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Compute the versions, write them into the built version module and return
182
+ * the table (`undefined` outside a build). Fails the build when an input was
183
+ * not recorded or cannot be read: a version computed without it would stay
184
+ * the same when it changes.
185
+ */
186
+ export function runRouterVersionsPhase(
187
+ s: DiscoveryState,
188
+ builder: BuilderLike | undefined,
189
+ ): RouterVersionsTable | undefined {
190
+ if (!s.isBuildMode) return undefined;
191
+ const { serverBuildGraph: server, clientBuildGraph: client } = s;
192
+ const ssr = s.ssrBundle;
193
+ const rscVersionFiles = s.versionModuleFiles.get("rsc") ?? [];
194
+ // The built version module holds a placeholder only this phase replaces: a
195
+ // build that skipped it would throw a ReferenceError on its first import.
196
+ if (!server || !client || !ssr || rscVersionFiles.length === 0) {
197
+ const missing = [
198
+ !server && "the RSC bundle",
199
+ !client && "the client bundle",
200
+ !ssr && "the SSR bundle",
201
+ rscVersionFiles.length === 0 &&
202
+ "the RSC chunk that holds the version module",
203
+ ].filter(Boolean);
204
+ throw new Error(
205
+ `[rango] The build did not record ${missing.join(", ")}, so the cache versions cannot be computed. ` +
206
+ "This is a bug in @rangojs/router; please report it.",
207
+ );
208
+ }
209
+
210
+ const start = performance.now();
211
+ const rscOutDir = environmentOutDir(builder, s.projectRoot, "rsc");
212
+ const ssrOutDir = environmentOutDir(builder, s.projectRoot, "ssr");
213
+
214
+ let computed: ComputedRouterVersions;
215
+ try {
216
+ const assetsManifest = readIfPresent(join(rscOutDir, ASSETS_MANIFEST_FILE));
217
+ if (assetsManifest === undefined) {
218
+ throw new Error(
219
+ `${ASSETS_MANIFEST_FILE} is not in the RSC output directory, so the stylesheets ` +
220
+ `server components link cannot be read`,
221
+ );
222
+ }
223
+ computed = computeRouterVersions({
224
+ projectRoot: s.projectRoot,
225
+ server,
226
+ client,
227
+ // plugin-rsc writes its manifest next to the SSR bundle, not into it.
228
+ ssr: {
229
+ chunks: ssr.chunks,
230
+ assets: [
231
+ ...ssr.assets.filter(
232
+ (asset) => asset.fileName !== ASSETS_MANIFEST_FILE,
233
+ ),
234
+ { fileName: ASSETS_MANIFEST_FILE, name: ASSETS_MANIFEST_FILE },
235
+ ],
236
+ },
237
+ routers: s.perRouterManifests.map((entry) => ({
238
+ id: entry.id,
239
+ moduleId: entry.sourceFile,
240
+ })),
241
+ buildData: [...s.buildData, ...clientUrlRecords(s)],
242
+ readServerFile: (fileName) => readIfPresent(join(rscOutDir, fileName)),
243
+ readSsrFile: (fileName) => readIfPresent(join(ssrOutDir, fileName)),
244
+ base: builder?.config?.base ?? "/",
245
+ serverResources: readServerResources(assetsManifest.toString("utf-8")),
246
+ externals: {
247
+ describePackage: createInstalledPackageDescriber(s.projectRoot),
248
+ readFile: readIfPresent,
249
+ buildId: randomUUID(),
250
+ },
251
+ });
252
+ } catch (err: any) {
253
+ throw new Error(
254
+ `[rango] Could not compute the cache versions: ${err?.message ?? err}`,
255
+ { cause: err },
256
+ );
257
+ }
258
+ const { table, details } = computed;
259
+
260
+ for (const [environment, fileNames] of s.versionModuleFiles) {
261
+ const outDir = environmentOutDir(builder, s.projectRoot, environment);
262
+ let filled = 0;
263
+ for (const fileName of fileNames) {
264
+ const path = join(outDir, fileName);
265
+ const source = readIfPresent(path);
266
+ const next =
267
+ source === undefined
268
+ ? undefined
269
+ : fillRouterVersions(source.toString("utf-8"), table);
270
+ if (next === undefined) continue;
271
+ writeFileSync(path, next);
272
+ filled++;
273
+ }
274
+ if (environment === "rsc" && filled === 0) {
275
+ throw new Error(
276
+ `[rango] Could not write the cache versions into the server build: no placeholder in ${fileNames.join(", ")} under ${outDir}. ` +
277
+ `The build would fail to start. This is a bug in @rangojs/router; please report it.`,
278
+ );
279
+ }
280
+ }
281
+
282
+ const elapsed = (performance.now() - start).toFixed(1);
283
+ const sourceOf = new Map(
284
+ s.perRouterManifests.map((entry) => [
285
+ entry.id,
286
+ entry.sourceFile ? portableModuleId(entry.sourceFile, s.projectRoot) : "",
287
+ ]),
288
+ );
289
+ const detailOf = new Map(details.map((detail) => [detail.routerId, detail]));
290
+ const wholeBuild = detailOf.get(DEFAULT_ROUTER_VERSIONS_KEY)!;
291
+ let keyed = 0;
292
+ if (s.perRouterManifests.length > 0) {
293
+ console.log(
294
+ `[rango] Cache versions for ${s.perRouterManifests.length} router(s), data / document (${elapsed}ms):`,
295
+ );
296
+ for (const { id } of s.perRouterManifests) {
297
+ // A router whose module is not in the server bundle serves with the
298
+ // whole-build pair: any change clears it. Say so, it is not isolated.
299
+ const own = detailOf.get(id);
300
+ const detail = own ?? wholeBuild;
301
+ if (detail.usesEncryptionKey) keyed++;
302
+ console.log(
303
+ `[rango] ${detail.data} / ${detail.document} ${sourceOf.get(id) || id}${own ? "" : " (whole build)"}`,
304
+ );
305
+ }
306
+ }
307
+ debug?.(
308
+ "router versions: %d server file(s), %sms, table=%o",
309
+ server.chunks.length + server.assets.length,
310
+ elapsed,
311
+ table,
312
+ );
313
+ writeVersionsReport(s.projectRoot, computed, sourceOf);
314
+
315
+ if (computed.undetermined.length > 0) {
316
+ console.log(
317
+ `[rango] ${computed.undetermined.length} import(s) the server build leaves external could not be read ` +
318
+ `at build time (${computed.undetermined.join(", ")}): a package that is not installed where the build ` +
319
+ `can find it, a path that does not resolve, or a URL. What they resolve to at run time is unknown, so ` +
320
+ `the routers importing them get a new cache version on every build.`,
321
+ );
322
+ }
323
+
324
+ // The one condition under which a deploy of unchanged code still clears a
325
+ // cache: the key is part of these routers' versions and this build made its
326
+ // own. Said once per build, and only when it applies.
327
+ if (keyed > 0 && !isEncryptionKeyStable()) {
328
+ console.log(
329
+ `[rango] No stable encryption key: ${keyed} router(s) encrypt server-action arguments ` +
330
+ `with a key generated for this build, so their cache version changes on every build and each ` +
331
+ `deploy clears their cache. To keep it, set rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY }) ` +
332
+ `with a base64 32-byte key (openssl rand -base64 32).`,
333
+ );
334
+ }
335
+ return table;
336
+ }