@rangojs/router 0.24.3 → 0.25.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 (274) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -1,10 +1,17 @@
1
- import type { Plugin } from "vite";
1
+ import type { EnvironmentModuleNode, Plugin } from "vite";
2
2
  import { normalizePath, parseAst } from "vite";
3
3
  import MagicString from "magic-string";
4
4
  import { createHash } from "node:crypto";
5
5
  import { relative } from "node:path";
6
6
  import { hasDirective } from "@vitejs/plugin-rsc/transforms";
7
7
  import { HOISTED_REVALIDATE_KEY } from "../../client-urls/hoisted-revalidate.js";
8
+ import {
9
+ CLIENT_IMPORT_RE,
10
+ CLIENT_URLS_REVALIDATE_QUERY,
11
+ componentUseSignature,
12
+ hoistedRevalidateId,
13
+ } from "../discovery/client-urls-projection.js";
14
+ import type { ClientUrlRevalidateIndex } from "../discovery/state.js";
8
15
  import { hasUseClientDirective } from "../utils/directive-prologue.js";
9
16
  import { offsetToLineColumn } from "./expose-ids/export-analysis.js";
10
17
  import {
@@ -31,13 +38,15 @@ import {
31
38
  resolveSourceTarget,
32
39
  SOURCE_FILE_RE,
33
40
  type EmittedEntry,
41
+ type SourceResolver,
34
42
  } from "./hoist/source.js";
35
43
 
36
44
  /**
37
45
  * clientUrls() loader `revalidate()` runs on the server, with the arguments a
38
- * urls() loader's predicate gets. The predicate is declared in a
39
- * "use client" module, so the build moves it into a server module (the
40
- * reverse of transition-when-hoist.ts):
46
+ * urls() loader's predicate gets. The predicate is declared in client code (a
47
+ * "use client" group, or a component's `.use` in any module the group
48
+ * imports), so the build moves it into a server module (the reverse of
49
+ * transition-when-hoist.ts):
41
50
  *
42
51
  * loader(CartLoader, () => [
43
52
  * revalidate((ctx) => ctx.isAction(Cart) ? ctx.actionResult?.ok : undefined),
@@ -56,31 +65,47 @@ import {
56
65
  *
57
66
  * plugin-rsc replaces the RSC copy of a "use client" module with client
58
67
  * reference proxies and drops everything else, so a post transform appends
59
- * `import "<file>?rango-revalidate"` to the proxy: the predicates register
60
- * when the group's module loads on the server (a lazy include stays lazy).
61
- * Route discovery serializes the SSR copy, so the projection carries the
62
- * keys; materialization (client-urls/server-projection.ts) attaches the
63
- * registered predicates to the loader stubs.
68
+ * `import "<file>?rango-revalidate"` to the proxy of every group (a
69
+ * "use client" module calling clientUrls()). The proxy has none of the
70
+ * group's imports, and a component's `.use` may sit in any module the group
71
+ * reaches. Route discovery serializes the group's SSR copy, so its projection
72
+ * carries every key the group reads, and the transform records each key's
73
+ * file in every environment (ClientUrlRevalidateIndex): discovery assigns
74
+ * the group the files of its keys as it installs the projection
75
+ * (discovery/client-urls-projection.ts), and the group's `?rango-revalidate`
76
+ * module imports theirs. Their registrations load with the group and nothing
77
+ * else of those files does, so no client code loads on the server and a lazy
78
+ * include stays lazy, wherever the file sits: behind barrels or in another
79
+ * workspace package. Materialization
80
+ * (client-urls/server-projection.ts) attaches the registered predicates to
81
+ * the loader stubs; a key nothing registered fails loudly there.
64
82
  *
65
83
  * A key hashes the project-relative file and the predicate's syntax tree
66
84
  * (formatting, comments and TypeScript excluded, so a re-printing transform
67
85
  * in one environment keeps it): two environments that saw a different
68
86
  * predicate fail loudly at materialization instead of pairing a loader with
69
- * the wrong one.
87
+ * the wrong one. A module several groups import registers once.
88
+ *
89
+ * Keyed on calls of the `revalidate` helper from "@rangojs/router/client":
90
+ * imported directly (a component's `.use`), or handed out by a clientUrls()
91
+ * builder callback (destructured, renamed, or as `helpers.revalidate`). The
92
+ * argument may reference imports and globals only: a binding of the module
93
+ * or of an enclosing function, `this`/`arguments`/`super`/`new.target`,
94
+ * `import()`, `import.meta` other than `import.meta.env`, a browser-only
95
+ * global, an `async` or generator function, or an import of a "use client"
96
+ * module is a LOUD error naming file:line. Imports stay in the client module
97
+ * too (they are not pruned). Vitest does not run the plugin, so in unit
98
+ * tests the predicate stays a plain function.
70
99
  *
71
- * Keyed on calls of the `revalidate` helper a clientUrls() builder callback
72
- * receives (destructured, renamed, or as `helpers.revalidate`), with
73
- * clientUrls imported from "@rangojs/router/client". The argument may
74
- * reference imports and globals only: a binding of the module or of an
75
- * enclosing function, `this`/`arguments`/`super`/`new.target`, `import()`,
76
- * `import.meta` other than `import.meta.env`, a browser-only global, an
77
- * `async` or generator function, or an import of a "use client" module is a
78
- * LOUD error naming file:line. Imports stay in the "use client" module too
79
- * (they are not pruned). Vitest does not run the plugin, so in unit tests the
80
- * predicate stays a plain function.
100
+ * `revalidate(true)` / `revalidate(false)` LITERALS stay in place: the
101
+ * projection carries the boolean and materialization turns it into the
102
+ * constant predicate. Any other argument is hoisted whatever it evaluates
103
+ * to, because an imported boolean and an imported predicate are both
104
+ * identifiers to the build: the server evaluates the expression, and a
105
+ * boolean result means the same constant answer (`revalidate(FLAG)` with an
106
+ * imported `FLAG`). Materialization rejects a value that is neither.
81
107
  */
82
108
 
83
- const CLIENT_URLS_REVALIDATE_QUERY = "rango-revalidate";
84
109
  const REGISTER = "__rango_registerClientUrlRevalidators";
85
110
 
86
111
  const CLIENT_SOURCES: ReadonlySet<string> = new Set(["@rangojs/router/client"]);
@@ -120,19 +145,24 @@ const CAPTURE_MESSAGES: LexicalCaptureMessages = {
120
145
  "loads a module at run time, which the build cannot check is server-safe.",
121
146
  };
122
147
 
123
- // Cheap pre-filter: every site needs a "use client" module (the directive's
124
- // text), clientUrls imported from the router, and a `revalidate` helper. The
125
- // AST walk is the authority.
148
+ // Cheap pre-filters: a site imports the router's client entry and names a
149
+ // `revalidate` helper; a group also names clientUrls. The AST walk is the
150
+ // authority.
126
151
  const CLIENT_URLS_RE = /\bclientUrls\b/;
127
- const CLIENT_IMPORT_RE = /["']@rangojs\/router\/client["']/;
128
152
  const REVALIDATE_RE = /\brevalidate\b/;
153
+ const CANDIDATE_RE = /\b(?:clientUrls|revalidate)\b/;
129
154
 
130
155
  function mayHoist(code: string): boolean {
156
+ return CLIENT_IMPORT_RE.test(code) && REVALIDATE_RE.test(code);
157
+ }
158
+
159
+ /** A clientUrls() group: its RSC proxy imports the registrations. */
160
+ function isGroupModule(code: string): boolean {
131
161
  return (
132
162
  code.includes("use client") &&
133
163
  CLIENT_URLS_RE.test(code) &&
134
164
  CLIENT_IMPORT_RE.test(code) &&
135
- REVALIDATE_RE.test(code)
165
+ hasUseClientDirective(code)
136
166
  );
137
167
  }
138
168
 
@@ -253,9 +283,9 @@ interface HoistClientUrlsRevalidateResult {
253
283
  }
254
284
 
255
285
  /**
256
- * Hoist every clientUrls() `revalidate()` predicate of a "use client"
257
- * module. Returns null when there is none. Throws, naming file:line, for a
258
- * predicate that is not pure.
286
+ * Hoist every clientUrls() `revalidate()` predicate of a module. Returns null
287
+ * when there is none (boolean literals stay in place). Throws, naming
288
+ * file:line, for a predicate that is not pure.
259
289
  */
260
290
  export async function hoistClientUrlsRevalidates(
261
291
  code: string,
@@ -264,21 +294,15 @@ export async function hoistClientUrlsRevalidates(
264
294
  ): Promise<HoistClientUrlsRevalidateResult | null> {
265
295
  if (!mayHoist(code)) return null;
266
296
  const file = normalizePath(id.split("?", 1)[0]!);
267
- let program: Node;
268
- try {
269
- program = parseAst(code, { lang: parseLang(file) });
270
- } catch {
271
- return null;
272
- }
273
- if (!hasDirective(program.body, "use client")) return null;
274
-
297
+ const program = parseProgram(code, file);
298
+ if (!program) return null;
275
299
  const { moduleScope, references, scopeOf } = analyzeScopes(program);
276
300
  const imports = collectImports(program);
277
301
  const calls = findHelperCalls(program, scopeOf, moduleScope, imports, {
278
302
  sources: CLIENT_SOURCES,
279
303
  builder: "clientUrls",
280
304
  helper: "revalidate",
281
- directImport: false,
305
+ directImport: true,
282
306
  });
283
307
  if (calls.length === 0) return null;
284
308
 
@@ -294,16 +318,18 @@ export async function hoistClientUrlsRevalidates(
294
318
  file,
295
319
  code,
296
320
  call,
297
- "`revalidate()` takes exactly one predicate.",
321
+ "`revalidate()` takes exactly one predicate or boolean.",
298
322
  );
299
323
  }
300
324
  const value = unwrapExpression(arg);
325
+ // The same constant in every environment: nothing to move.
326
+ if (value.type === "Literal" && typeof value.value === "boolean") continue;
301
327
  if (NOT_A_FUNCTION.has(value.type)) {
302
328
  throw revalidateError(
303
329
  file,
304
330
  code,
305
331
  value,
306
- "`revalidate()` expects a predicate function.",
332
+ "`revalidate()` expects a predicate function or a boolean.",
307
333
  );
308
334
  }
309
335
  if (isFunctionLiteral(value) && (value.async || value.generator)) {
@@ -336,9 +362,12 @@ export async function hoistClientUrlsRevalidates(
336
362
  code,
337
363
  binding.node,
338
364
  binding.scope.lookup(name) === moduleScope
339
- ? `\`${name}\` is declared in this "use client" module. Move the ` +
340
- 'predicate to its own module (without "use client") and ' +
341
- "import it, or write it inline."
365
+ ? `\`${name}\` is declared in this ` +
366
+ (hasDirective(program.body, "use client")
367
+ ? '"use client" module'
368
+ : "module") +
369
+ ". Move the predicate to its own module (without " +
370
+ '"use client") and import it, or write it inline.'
342
371
  : `\`${name}\` is a binding of the enclosing function. Read what ` +
343
372
  "you need from the predicate's argument (`currentParams`, " +
344
373
  "`nextUrl`, `context`, `actionResult`, ...).",
@@ -391,6 +420,8 @@ export async function hoistClientUrlsRevalidates(
391
420
  );
392
421
  }
393
422
 
423
+ if (entries.size === 0) return null;
424
+
394
425
  const importLines = await emitImports(used, imports, () => {});
395
426
  const module = [
396
427
  `import { registerClientUrlRevalidators as ${REGISTER} } from "@rangojs/router/server";`,
@@ -421,6 +452,14 @@ async function isClientOnly(
421
452
  );
422
453
  }
423
454
 
455
+ function parseProgram(code: string, file: string): Node | null {
456
+ try {
457
+ return parseAst(code, { lang: parseLang(file) });
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+
424
463
  /** The file a `?rango-revalidate` id names, or null. */
425
464
  export function parseHoistedRevalidateId(id: string): string | null {
426
465
  const q = id.indexOf("?");
@@ -436,27 +475,43 @@ const HOISTED_ID_RE = new RegExp(
436
475
  );
437
476
  const SOURCE_ID_RE = /\.[cm]?[jt]sx?(?:\?|$)/;
438
477
  const NODE_MODULES_RE = /\/node_modules\//;
478
+ const USE_ASSIGNMENT_RE = /\.use\s*=/;
439
479
 
440
480
  /**
441
481
  * The plugins: a pre transform that hoists in every environment (markers in
442
- * the client and SSR copies, the server module recorded from the RSC
443
- * transform) and serves `?rango-revalidate`, and a post transform that
444
- * appends its import to the RSC client-reference proxy.
482
+ * the client and SSR copies, each key's file in `index`, the server module
483
+ * recorded from the RSC transform, the groups noted) and serves
484
+ * `?rango-revalidate`, a post transform that appends its import to the RSC
485
+ * client-reference proxy of each group, and in dev the client's `.use`
486
+ * update propagation. Pass one `index` to every instance of one rango():
487
+ * discovery's temp server records keys the dev server or build reads.
488
+ *
489
+ * `load` serves the registrations the RSC transform emitted, which hoisted
490
+ * the pipeline code; the disk source is re-hoisted when the transform has not
491
+ * seen the current file (loadEmittedOrDisk), which is always the case for a
492
+ * component module: the RSC environment never transforms a module only a
493
+ * group imports. A group's module also imports the registrations of the
494
+ * files discovery assigned it. A file with neither serves an empty module.
445
495
  *
446
- * `load` serves the module the RSC transform emitted, which hoisted the
447
- * pipeline code; the disk source is re-hoisted only when the transform has
448
- * not seen the current file (loadEmittedOrDisk). A file whose edit removed
449
- * every predicate serves an empty module to a proxy that still imports it.
496
+ * A component module that exports only components is a Fast Refresh
497
+ * boundary, so a `.use` edit would leave the browser's group records stale
498
+ * (the group never re-evaluates). In dev, an update that changes a module's
499
+ * componentUseSignature also updates its importers, as a group edit does;
500
+ * any other edit keeps Fast Refresh.
450
501
  *
451
- * The transform's code filter skips a module that never mentions clientUrls,
452
- * so a file that stops doing so would keep its `emitted` entry: watchChange
453
- * drops it, together with the cached "use client" answer for an edited
454
- * dependency (isClientModule memoizes per resolved file; buildStart resets
455
- * it).
502
+ * The transform's code filter skips a module that names neither clientUrls
503
+ * nor revalidate, so a file that stops doing so would keep its `emitted`
504
+ * entry: watchChange drops it, its group mark, and the cached "use client"
505
+ * answer for an edited dependency (isClientModule memoizes per resolved
506
+ * file; buildStart resets it).
456
507
  */
457
- export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
508
+ export function clientUrlsRevalidateHoistPlugin(
509
+ index: ClientUrlRevalidateIndex,
510
+ ): Plugin[] {
458
511
  const emitted = new Map<string, EmittedEntry<string>>();
512
+ const groups = new Set<string>();
459
513
  const clientModules = new Map<string, Promise<boolean>>();
514
+ const useSignatures = new Map<string, string>();
460
515
  let root = process.cwd();
461
516
 
462
517
  const isClientFile = (file: string): Promise<boolean> => {
@@ -472,6 +527,14 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
472
527
  }
473
528
  return known;
474
529
  };
530
+ const clientModuleCheck =
531
+ (ctx: SourceResolver, importer: string) =>
532
+ async (source: string): Promise<boolean> => {
533
+ const target = await resolveSourceTarget(ctx, source, importer, {
534
+ skipNodeModules: false,
535
+ });
536
+ return target !== null && (await isClientFile(target));
537
+ };
475
538
 
476
539
  return [
477
540
  {
@@ -479,7 +542,7 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
479
542
  enforce: "pre",
480
543
 
481
544
  configResolved(config) {
482
- root = config.root;
545
+ root = normalizePath(config.root);
483
546
  },
484
547
 
485
548
  buildStart() {
@@ -490,6 +553,7 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
490
553
  const file = normalizePath(id);
491
554
  clientModules.delete(file);
492
555
  emitted.delete(file);
556
+ groups.delete(file);
493
557
  },
494
558
 
495
559
  load: {
@@ -497,14 +561,34 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
497
561
  async handler(id) {
498
562
  const hoistedFile = parseHoistedRevalidateId(id);
499
563
  if (hoistedFile === null) return;
500
- const module = await loadEmittedOrDisk(
564
+ const file = normalizePath(hoistedFile);
565
+ const registrations = await loadEmittedOrDisk(
501
566
  emitted,
502
- hoistedFile,
503
- async (disk) =>
504
- (await hoistClientUrlsRevalidates(disk, hoistedFile, { root }))
505
- ?.module,
567
+ file,
568
+ await readSource(file),
569
+ async (source) =>
570
+ (
571
+ await hoistClientUrlsRevalidates(source, file, {
572
+ root,
573
+ isClientModule: clientModuleCheck(this, file),
574
+ })
575
+ )?.module,
506
576
  );
507
- return module ?? "export {};\n";
577
+ const assigned = index.filesByGroup.get(file) ?? [];
578
+ const code =
579
+ registrations === undefined && assigned.length === 0
580
+ ? "export {};\n"
581
+ : [
582
+ registrations ?? "",
583
+ ...assigned.map(
584
+ (target) =>
585
+ `import ${JSON.stringify(hoistedRevalidateId(target))};\n`,
586
+ ),
587
+ ].join("");
588
+ // Reached only through bare imports, and the id names an app file,
589
+ // so the app's package.json `"sideEffects": false` would let the
590
+ // build drop the registrations (#1149). load outranks that field.
591
+ return { code, moduleSideEffects: "no-treeshake" };
508
592
  },
509
593
  },
510
594
 
@@ -514,7 +598,7 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
514
598
  include: SOURCE_ID_RE,
515
599
  exclude: [NODE_MODULES_RE, HOISTED_ID_RE],
516
600
  },
517
- code: CLIENT_URLS_RE,
601
+ code: CANDIDATE_RE,
518
602
  },
519
603
  async handler(code, id) {
520
604
  if (id.includes("/node_modules/") || parseHoistedRevalidateId(id)) {
@@ -523,19 +607,17 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
523
607
  const file = normalizePath(id.split("?", 1)[0]!);
524
608
  if (!SOURCE_FILE_RE.test(file)) return;
525
609
  const rsc = this.environment?.name === "rsc";
526
- if (rsc) emitted.delete(file);
610
+ if (rsc) {
611
+ emitted.delete(file);
612
+ if (isGroupModule(code)) groups.add(file);
613
+ else groups.delete(file);
614
+ }
527
615
  const result = await hoistClientUrlsRevalidates(code, id, {
528
616
  root,
529
- isClientModule: rsc
530
- ? async (source) => {
531
- const target = await resolveSourceTarget(this, source, id, {
532
- skipNodeModules: false,
533
- });
534
- return target !== null && (await isClientFile(target));
535
- }
536
- : undefined,
617
+ isClientModule: rsc ? clientModuleCheck(this, id) : undefined,
537
618
  });
538
619
  if (!result) return;
620
+ for (const key of result.keys) index.fileByKey.set(key, file);
539
621
  if (rsc) await recordEmitted(emitted, file, result.module);
540
622
  return { code: result.code, map: result.map };
541
623
  },
@@ -551,14 +633,45 @@ export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
551
633
  handler(code, id) {
552
634
  if (parseHoistedRevalidateId(id)) return;
553
635
  const file = normalizePath(id.split("?", 1)[0]!);
554
- if (!emitted.has(file)) return;
636
+ if (!groups.has(file)) return;
555
637
  const s = new MagicString(code);
556
- s.append(
557
- `\nimport ${JSON.stringify(`${file}?${CLIENT_URLS_REVALIDATE_QUERY}`)};\n`,
558
- );
638
+ s.append(`\nimport ${JSON.stringify(hoistedRevalidateId(file))};\n`);
559
639
  return { code: s.toString(), map: s.generateMap({ hires: true }) };
560
640
  },
561
641
  },
562
642
  },
643
+ {
644
+ name: "@rangojs/router:client-urls-component-use-hmr",
645
+ apply: "serve",
646
+ applyToEnvironment: (env) => env.name === "client",
647
+
648
+ transform: {
649
+ filter: {
650
+ id: { include: SOURCE_ID_RE, exclude: [NODE_MODULES_RE] },
651
+ code: USE_ASSIGNMENT_RE,
652
+ },
653
+ async handler(_code, id) {
654
+ // The disk source, as hotUpdate reads it: an earlier transform may
655
+ // re-print the module.
656
+ const file = normalizePath(id.split("?", 1)[0]!);
657
+ const disk = await readSource(file);
658
+ const signature = disk && componentUseSignature(disk, file);
659
+ if (signature) useSignatures.set(file, signature);
660
+ },
661
+ },
662
+
663
+ async hotUpdate({ file, modules, read }) {
664
+ if (modules.length === 0 || !SOURCE_FILE_RE.test(file)) return;
665
+ const next = componentUseSignature(await read(), file);
666
+ if (next === useSignatures.get(file)) return;
667
+ if (next === undefined) useSignatures.delete(file);
668
+ else useSignatures.set(file, next);
669
+ const updated = new Set<EnvironmentModuleNode>(modules);
670
+ for (const mod of modules) {
671
+ for (const importer of mod.importers) updated.add(importer);
672
+ }
673
+ return [...updated];
674
+ },
675
+ },
563
676
  ];
564
677
  }
@@ -187,6 +187,49 @@ export function exposeInternalIds(options?: { forceBuild?: boolean }): Plugin {
187
187
 
188
188
  const counter = createCounter(debug, "expose-internal-ids");
189
189
 
190
+ /** The virtual:rsc-router/loader-manifest module. */
191
+ function loaderManifestCode(): string {
192
+ if (!isBuild) {
193
+ return `import { setLoaderImports } from "@rangojs/router/server";
194
+
195
+ // Dev mode: empty map, loaders are resolved dynamically via path parsing
196
+ setLoaderImports({});
197
+ `;
198
+ }
199
+
200
+ // Build mode: generate lazy import map. Sorted by id: the registry fills
201
+ // in directory-scan order (the pre-scan below) and transform order, both
202
+ // of which vary between machines and runs, and this module's bytes feed
203
+ // the cache version of every router bundled with it
204
+ // (discovery/build-versions.ts).
205
+ const lazyImports: string[] = [];
206
+ const sortedLoaders = [...loaderRegistry].sort(([a], [b]) =>
207
+ compareStrings(a, b),
208
+ );
209
+
210
+ for (const [hashedId, { filePath, exportName }] of sortedLoaders) {
211
+ lazyImports.push(
212
+ ` "${hashedId}": () => import("/${filePath}").then(m => m.${exportName})`,
213
+ );
214
+ }
215
+
216
+ if (lazyImports.length === 0) {
217
+ return `import { setLoaderImports } from "@rangojs/router/server";
218
+
219
+ // No fetchable loaders discovered during build
220
+ setLoaderImports({});
221
+ `;
222
+ }
223
+
224
+ return `import { setLoaderImports } from "@rangojs/router/server";
225
+
226
+ // Lazy import map - loaders are loaded on-demand when first requested
227
+ setLoaderImports({
228
+ ${lazyImports.join(",\n")}
229
+ });
230
+ `;
231
+ }
232
+
190
233
  return {
191
234
  name: "@rangojs/router:expose-internal-ids",
192
235
  enforce: "post",
@@ -239,46 +282,9 @@ export function exposeInternalIds(options?: { forceBuild?: boolean }): Plugin {
239
282
  }
240
283
 
241
284
  if (id !== RESOLVED_VIRTUAL_LOADER_MANIFEST) return;
242
-
243
- if (!isBuild) {
244
- return `import { setLoaderImports } from "@rangojs/router/server";
245
-
246
- // Dev mode: empty map, loaders are resolved dynamically via path parsing
247
- setLoaderImports({});
248
- `;
249
- }
250
-
251
- // Build mode: generate lazy import map. Sorted by id: the registry fills
252
- // in directory-scan order (the pre-scan below) and transform order, both
253
- // of which vary between machines and runs, and this module's bytes feed
254
- // the cache version of every router bundled with it
255
- // (discovery/build-versions.ts).
256
- const lazyImports: string[] = [];
257
- const sortedLoaders = [...loaderRegistry].sort(([a], [b]) =>
258
- compareStrings(a, b),
259
- );
260
-
261
- for (const [hashedId, { filePath, exportName }] of sortedLoaders) {
262
- lazyImports.push(
263
- ` "${hashedId}": () => import("/${filePath}").then(m => m.${exportName})`,
264
- );
265
- }
266
-
267
- if (lazyImports.length === 0) {
268
- return `import { setLoaderImports } from "@rangojs/router/server";
269
-
270
- // No fetchable loaders discovered during build
271
- setLoaderImports({});
272
- `;
273
- }
274
-
275
- return `import { setLoaderImports } from "@rangojs/router/server";
276
-
277
- // Lazy import map - loaders are loaded on-demand when first requested
278
- setLoaderImports({
279
- ${lazyImports.join(",\n")}
280
- });
281
- `;
285
+ // Only ever a bare import (the RSC entry's), so a build whose
286
+ // treeshake.moduleSideEffects is false would drop it; load outranks it.
287
+ return { code: loaderManifestCode(), moduleSideEffects: "no-treeshake" };
282
288
  },
283
289
 
284
290
  // --------------- Loader pre-scan (build mode) ---------------
@@ -65,19 +65,20 @@ export async function recordEmitted<T>(
65
65
  }
66
66
 
67
67
  /**
68
- * What a `?rango-*` load serves for `hoistedFile`: the transform's emission
69
- * when it saw the current disk source (or the file is gone), else
70
- * `rehoist(disk)`. The transform hoists the pipeline code, which an earlier
71
- * plugin may have changed, so re-hoisting the disk source is the fallback only
72
- * for an environment loading before the RSC transform ran or for an edit the
73
- * RSC environment has not re-transformed yet. Undefined when neither exists.
68
+ * What a `?rango-*` load serves for `hoistedFile`, whose source on disk is
69
+ * `disk` (readSource): the transform's emission when it saw that source (or
70
+ * the file is gone), else `rehoist(disk)`. The transform hoists the pipeline
71
+ * code, which an earlier plugin may have changed, so re-hoisting the disk
72
+ * source is the fallback only for an environment loading before the RSC
73
+ * transform ran or for an edit the RSC environment has not re-transformed
74
+ * yet. Undefined when neither exists.
74
75
  */
75
76
  export async function loadEmittedOrDisk<T>(
76
77
  emitted: ReadonlyMap<string, EmittedEntry<T>>,
77
78
  hoistedFile: string,
79
+ disk: string | null,
78
80
  rehoist: (disk: string) => Promise<T | undefined>,
79
81
  ): Promise<T | undefined> {
80
- const disk = await readSource(hoistedFile);
81
82
  const entry = emitted.get(normalizePath(hoistedFile));
82
83
  if (entry && (disk === null || entry.disk === disk)) return entry.value;
83
84
  return disk === null ? undefined : rehoist(disk);
@@ -362,6 +362,7 @@ export function transitionWhenHoistPlugin(): Plugin {
362
362
  const modules = await loadEmittedOrDisk(
363
363
  emitted,
364
364
  hoisted.file,
365
+ await readSource(hoisted.file),
365
366
  async (disk) =>
366
367
  (await hoistTransitionWhens(disk, hoisted.file))?.modules,
367
368
  );
@@ -2,6 +2,7 @@ import { parseAst, type Plugin } from "vite";
2
2
  import {
3
3
  VIRTUAL_IDS,
4
4
  getVirtualBuildVersionContent,
5
+ getVirtualPackageVersionContent,
5
6
  getVirtualVersionContent,
6
7
  } from "./virtual-entries.js";
7
8
  import { hasUseClientDirective } from "../utils/directive-prologue.js";
@@ -170,6 +171,12 @@ export function createVersionPlugin(): Plugin {
170
171
 
171
172
  load(id) {
172
173
  if (id === "\0" + VIRTUAL_IDS.version) {
174
+ const environment = this?.environment?.name;
175
+ // The browser gets PACKAGE_VERSION only (ROUTER_VERSIONS is filled in
176
+ // server chunks); every other environment keeps the full module.
177
+ if (environment === "client") {
178
+ return getVirtualPackageVersionContent();
179
+ }
173
180
  return isDev
174
181
  ? getVirtualVersionContent(currentVersion)
175
182
  : getVirtualBuildVersionContent();
@@ -16,6 +16,13 @@ declare module "@rangojs/router:version" {
16
16
  */
17
17
  export const VERSION: string;
18
18
 
19
+ /**
20
+ * The npm semver of @rangojs/router (`1.2.3`). Informational: the browser
21
+ * entry publishes it as `window.__rango`. Every environment exports it; only
22
+ * the RSC environment also exports `VERSION` and `ROUTER_VERSIONS`.
23
+ */
24
+ export const PACKAGE_VERSION: string;
25
+
19
26
  /**
20
27
  * Per-router cache versions computed at build time: router id to
21
28
  * `[data, document]`, with `"*"` holding the whole-build pair.
@@ -1,5 +1,6 @@
1
1
  import type { HeadScriptsOption } from "../plugin-types.js";
2
2
  import { DEFAULT_ROUTER_VERSIONS_KEY } from "../../router-versions.js";
3
+ import { rangoVersion } from "../utils/banner.js";
3
4
 
4
5
  export const VIRTUAL_ENTRY_BROWSER: string = `
5
6
  import {
@@ -320,9 +321,27 @@ export const VIRTUAL_IDS = {
320
321
  version: "@rangojs/router:version",
321
322
  } as const;
322
323
 
324
+ /**
325
+ * The npm semver of @rangojs/router, for the informational `window.__rango`
326
+ * marker (browser/rsc-router.tsx). Not a cache or build version.
327
+ */
328
+ function getPackageVersionExport(): string {
329
+ return `export const PACKAGE_VERSION = ${JSON.stringify(rangoVersion)};`;
330
+ }
331
+
332
+ /**
333
+ * Non-RSC environments (client, ssr) get only PACKAGE_VERSION: the build
334
+ * module's ROUTER_VERSIONS token is replaced in server chunks only, so a
335
+ * client import of the full module would throw a ReferenceError.
336
+ */
337
+ export function getVirtualPackageVersionContent(): string {
338
+ return getPackageVersionExport();
339
+ }
340
+
323
341
  /** Dev: one stamp for both versions of every router, bumped on RSC edits. */
324
342
  export function getVirtualVersionContent(version: string): string {
325
343
  return [
344
+ getPackageVersionExport(),
326
345
  `export const VERSION = ${JSON.stringify(version)};`,
327
346
  `export const ROUTER_VERSIONS = undefined;`,
328
347
  ].join("\n");
@@ -346,6 +365,7 @@ export const ROUTER_VERSIONS_PLACEHOLDER = "__RANGO_ROUTER_VERSIONS__";
346
365
  */
347
366
  export function getVirtualBuildVersionContent(): string {
348
367
  return [
368
+ getPackageVersionExport(),
349
369
  `export const ROUTER_VERSIONS = ${ROUTER_VERSIONS_PLACEHOLDER};`,
350
370
  `export const VERSION = ROUTER_VERSIONS[${JSON.stringify(DEFAULT_ROUTER_VERSIONS_KEY)}][1];`,
351
371
  ].join("\n");