@rangojs/router 0.24.4 → 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 (267) 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-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -1,6 +1,7 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { isAbsolute, relative, resolve } from "node:path";
3
- import { normalizePath, parseAst } from "vite";
4
+ import { normalizePath, parseAst, type EnvironmentModuleNode } from "vite";
4
5
  import {
5
6
  projectClientUrlsModule,
6
7
  type ClientUrlProjection,
@@ -10,6 +11,9 @@ import {
10
11
  hashRefKey,
11
12
  } from "../plugins/client-ref-hashing.js";
12
13
  import { hasUseClientDirective } from "../utils/directive-prologue.js";
14
+ import { firstCodeMatchIndex } from "../../build/route-types/source-scan.js";
15
+ import { parseLang } from "../plugins/hoist/literal.js";
16
+ import type { Node } from "../plugins/hoist/scope.js";
13
17
  import { createRangoDebugger, NS } from "../debug.js";
14
18
  import { findTsFiles } from "../../build/route-types/scan-filter.js";
15
19
  import type { DiscoveryState } from "./state.js";
@@ -30,6 +34,22 @@ interface ClientUrlProjectionServerModule {
30
34
  ): void;
31
35
  }
32
36
 
37
+ /** The RSC environment whose realm `ClientUrlProjectionServerModule` is. */
38
+ interface ClientUrlRscEnvironment {
39
+ readonly runner: { import(id: string): Promise<unknown> };
40
+ readonly moduleGraph?: {
41
+ getModuleById(id: string): EnvironmentModuleNode | undefined;
42
+ invalidateModule(mod: EnvironmentModuleNode): void;
43
+ };
44
+ }
45
+
46
+ export const CLIENT_URLS_REVALIDATE_QUERY = "rango-revalidate";
47
+
48
+ /** The id of the module registering `file`'s hoisted revalidate() predicates. */
49
+ export function hoistedRevalidateId(file: string): string {
50
+ return `${file}?${CLIENT_URLS_REVALIDATE_QUERY}`;
51
+ }
52
+
33
53
  function hasDefaultExport(code: string): boolean {
34
54
  let program: { body?: any[] };
35
55
  try {
@@ -106,6 +126,69 @@ export function recordClientUrlsModule(
106
126
  }
107
127
  }
108
128
 
129
+ /** A module that imports the router's client entry (a cheap pre-filter). */
130
+ export const CLIENT_IMPORT_RE: RegExp = /["']@rangojs\/router\/client["']/;
131
+
132
+ /**
133
+ * Whether an edited module can carry a clientUrls() component's own items: it
134
+ * imports "@rangojs/router/client" and assigns a `.use` in code (not in a
135
+ * comment or string). Such a module changes its groups' projections and
136
+ * hoisted revalidate() registrations like a group edit, with or without
137
+ * "use client".
138
+ */
139
+ export function isClientUrlsComponentUseSource(source: string): boolean {
140
+ return (
141
+ source.includes(".use") &&
142
+ CLIENT_IMPORT_RE.test(source) &&
143
+ firstCodeMatchIndex(source, /\.use\s*=(?!=)/g) >= 0
144
+ );
145
+ }
146
+
147
+ /**
148
+ * A hash of the text of every `X.use = ...` assignment of a component-use
149
+ * module (isClientUrlsComponentUseSource), or undefined for any other module.
150
+ * Equal before and after a save, the edit (markup, hooks) left what its
151
+ * groups read unchanged: the dev watcher skips re-discovery and the client
152
+ * keeps Fast Refresh. An edit outside the assignments that changes what one
153
+ * means (a helper it calls, an import's source) is not seen. A module that
154
+ * does not parse hashes whole.
155
+ */
156
+ export function componentUseSignature(
157
+ source: string,
158
+ file: string,
159
+ ): string | undefined {
160
+ if (!isClientUrlsComponentUseSource(source)) return undefined;
161
+ const hash = createHash("sha256");
162
+ let program: Node;
163
+ try {
164
+ program = parseAst(source, { lang: parseLang(file) });
165
+ } catch {
166
+ return hash.update(source).digest("base64");
167
+ }
168
+ const visit = (node: unknown): void => {
169
+ if (typeof node !== "object" || node === null) return;
170
+ if (Array.isArray(node)) {
171
+ for (const child of node) visit(child);
172
+ return;
173
+ }
174
+ const { type, left } = node as Node;
175
+ if (
176
+ type === "AssignmentExpression" &&
177
+ left.type === "MemberExpression" &&
178
+ !left.computed &&
179
+ left.property.type === "Identifier" &&
180
+ left.property.name === "use"
181
+ ) {
182
+ const { start, end } = node as Node;
183
+ hash.update(source.slice(start, end)).update("\0");
184
+ return;
185
+ }
186
+ for (const value of Object.values(node)) visit(value);
187
+ };
188
+ visit(program.body);
189
+ return hash.digest("base64");
190
+ }
191
+
109
192
  export function resolveClientUrlsSource(
110
193
  state: DiscoveryState,
111
194
  referenceId: string,
@@ -162,14 +245,17 @@ function projectionError(
162
245
  * import, so `.routes(reference)` materialized the previous pass's projection.
163
246
  *
164
247
  * Errors are deliberately swallowed per source (a module mid-edit keeps its
165
- * last-known projection); the strict discoverClientUrlProjections() pass still
166
- * surfaces real failures afterwards. No-ops on cold start (nothing recorded
167
- * yet) and when the SSR runner is unavailable.
248
+ * last-known projection), and all refreshed projections keep their
249
+ * last-known state when any registration fails to load; the strict
250
+ * discoverClientUrlProjections() pass still surfaces real failures afterwards.
251
+ * No-ops on cold start (nothing recorded yet) and when the SSR runner is
252
+ * unavailable.
168
253
  */
169
254
  export async function refreshRecordedClientUrlProjections(
170
255
  state: DiscoveryState,
171
256
  ssrEnv: ClientUrlSsrEnvironment | undefined,
172
257
  serverMod: ClientUrlProjectionServerModule,
258
+ rscEnv: ClientUrlRscEnvironment,
173
259
  ): Promise<void> {
174
260
  const sourceByReferenceId = state.clientUrlSourceByReferenceId;
175
261
  if (
@@ -189,6 +275,7 @@ export async function refreshRecordedClientUrlProjections(
189
275
  }
190
276
  }
191
277
 
278
+ const refreshed = new Map<string, ClientUrlProjection>();
192
279
  for (const [source, referenceIds] of referenceIdsBySource) {
193
280
  let module: Record<string, unknown>;
194
281
  try {
@@ -209,15 +296,29 @@ export async function refreshRecordedClientUrlProjections(
209
296
  projection.routes.map((route) => route.pattern).join(", "),
210
297
  );
211
298
  for (const referenceId of referenceIds) {
212
- serverMod.setClientUrlProjection(referenceId, projection);
213
- // The eager routes-manifest virtual module REPLAYS this state map
214
- // (clearClientUrlProjections + set) whenever it re-evaluates — and the
215
- // watcher's gen-file write invalidates it right before the entry import,
216
- // so a stale map here would clobber the registry install above before
217
- // the router materializes its mount. Keep both in sync.
218
- state.clientUrlProjectionMap?.set(referenceId, projection);
299
+ refreshed.set(referenceId, projection);
219
300
  }
220
301
  }
302
+ if (refreshed.size === 0) return;
303
+ // The eager routes-manifest virtual module REPLAYS the state map
304
+ // (clearClientUrlProjections + set) whenever it re-evaluates — and the
305
+ // watcher's gen-file write invalidates it right before the entry import, so
306
+ // a stale map would clobber the registry install before the router
307
+ // materializes its mount: the install keeps both in sync.
308
+ try {
309
+ await installClientUrlProjections(
310
+ state,
311
+ rscEnv,
312
+ serverMod,
313
+ refreshed,
314
+ false,
315
+ );
316
+ } catch (error) {
317
+ debug?.(
318
+ "pre-entry refresh: predicate registrations failed to load: %s",
319
+ error instanceof Error ? error.message : String(error),
320
+ );
321
+ }
221
322
  }
222
323
 
223
324
  /**
@@ -235,12 +336,18 @@ export async function discoverClientUrlProjections(
235
336
  state: DiscoveryState,
236
337
  ssrEnv: ClientUrlSsrEnvironment | undefined,
237
338
  serverMod: ClientUrlProjectionServerModule,
339
+ rscEnv: ClientUrlRscEnvironment,
238
340
  ): Promise<void> {
239
341
  scanForClientUrlModules(state);
240
342
  const sourceByReferenceId = state.clientUrlSourceByReferenceId;
241
343
  if (!sourceByReferenceId?.size) {
242
- serverMod.clearClientUrlProjections();
243
- state.clientUrlProjectionMap = new Map();
344
+ await installClientUrlProjections(
345
+ state,
346
+ rscEnv,
347
+ serverMod,
348
+ new Map(),
349
+ true,
350
+ );
244
351
  return;
245
352
  }
246
353
 
@@ -313,9 +420,91 @@ export async function discoverClientUrlProjections(
313
420
  }
314
421
  }
315
422
 
316
- serverMod.clearClientUrlProjections();
317
- for (const [id, projection] of nextProjections) {
423
+ await installClientUrlProjections(
424
+ state,
425
+ rscEnv,
426
+ serverMod,
427
+ nextProjections,
428
+ true,
429
+ );
430
+ }
431
+
432
+ /**
433
+ * Install `projections` in the realm of `serverMod` and in `state` (all of
434
+ * them with `replace`, else over the installed ones), once `rscEnv` has
435
+ * loaded the registrations of every revalidate() key they read
436
+ * (assignClientUrlRevalidateFiles). Node dev serves requests in that realm,
437
+ * and a request materializes the installed projection, so a key whose
438
+ * registration has not loaded must never be installed.
439
+ */
440
+ async function installClientUrlProjections(
441
+ state: DiscoveryState,
442
+ rscEnv: ClientUrlRscEnvironment,
443
+ serverMod: ClientUrlProjectionServerModule,
444
+ projections: ReadonlyMap<string, ClientUrlProjection>,
445
+ replace: boolean,
446
+ ): Promise<void> {
447
+ const next = new Map(replace ? undefined : state.clientUrlProjectionMap);
448
+ for (const [id, projection] of projections) next.set(id, projection);
449
+ await assignClientUrlRevalidateFiles(state, rscEnv, next);
450
+ if (replace) serverMod.clearClientUrlProjections();
451
+ for (const [id, projection] of projections) {
318
452
  serverMod.setClientUrlProjection(id, projection);
319
453
  }
320
- state.clientUrlProjectionMap = nextProjections;
454
+ state.clientUrlProjectionMap = next;
455
+ }
456
+
457
+ /**
458
+ * Assign each group of `projections` the other files holding the predicates
459
+ * it reads (each key's file, as the hoist transform recorded it: a key with
460
+ * none stays unregistered, and materialization names it), after loading their
461
+ * registrations into `rscEnv`. The group's own registrations load with its
462
+ * proxy, and its `?rango-revalidate` module imports the assigned files'
463
+ * (plugins/client-urls-revalidate-hoist.ts). When a group's files change, that
464
+ * module is invalidated, so a later evaluation (a reload, a request realm)
465
+ * imports the current ones. Another realm (workerd, a production bundle)
466
+ * loads a group only after discovery: its entry imports the routes-manifest
467
+ * module first, whose load waits for discovery (RSC_ENTRY_BOOTSTRAP_IMPORTS),
468
+ * and a build discovers in buildStart.
469
+ */
470
+ export async function assignClientUrlRevalidateFiles(
471
+ state: Pick<
472
+ DiscoveryState,
473
+ "clientUrlRevalidate" | "clientUrlSourceByReferenceId"
474
+ >,
475
+ rscEnv: ClientUrlRscEnvironment,
476
+ projections: ReadonlyMap<string, ClientUrlProjection>,
477
+ ): Promise<void> {
478
+ const { fileByKey, filesByGroup } = state.clientUrlRevalidate;
479
+ const next = new Map<string, readonly string[]>();
480
+ for (const [referenceId, projection] of projections) {
481
+ const group = state.clientUrlSourceByReferenceId?.get(referenceId);
482
+ if (group === undefined || next.has(group)) continue;
483
+ const files = new Set<string>();
484
+ for (const owner of [
485
+ ...projection.routes,
486
+ ...(projection.intercepts ?? []),
487
+ ]) {
488
+ for (const key of owner.loaderRevalidate?.flat() ?? []) {
489
+ const file = typeof key === "string" ? fileByKey.get(key) : undefined;
490
+ if (file !== undefined && file !== group) files.add(file);
491
+ }
492
+ }
493
+ next.set(group, [...files].sort());
494
+ }
495
+ await Promise.all(
496
+ [...new Set([...next.values()].flat())].map((file) =>
497
+ rscEnv.runner.import(hoistedRevalidateId(file)),
498
+ ),
499
+ );
500
+ const graph = rscEnv.moduleGraph;
501
+ for (const [group, files] of next) {
502
+ if ((filesByGroup.get(group) ?? []).join("\0") === files.join("\0")) {
503
+ continue;
504
+ }
505
+ const mod = graph?.getModuleById(hoistedRevalidateId(group));
506
+ if (graph && mod) graph.invalidateModule(mod);
507
+ }
508
+ filesByGroup.clear();
509
+ for (const [group, files] of next) filesByGroup.set(group, files);
321
510
  }
@@ -138,7 +138,7 @@ async function discoverRoutersPass(
138
138
  serverMod: any,
139
139
  ): Promise<any> {
140
140
  await timed(debug, "inner: refresh recorded client URL projections", () =>
141
- refreshRecordedClientUrlProjections(state, ssrEnv, serverMod),
141
+ refreshRecordedClientUrlProjections(state, ssrEnv, serverMod, rscEnv),
142
142
  );
143
143
 
144
144
  // Import the entry file via RSC environment.
@@ -194,7 +194,7 @@ async function discoverRoutersPass(
194
194
  }
195
195
 
196
196
  await timed(debug, "inner: discover client URL projections", () =>
197
- discoverClientUrlProjections(state, ssrEnv, serverMod),
197
+ discoverClientUrlProjections(state, ssrEnv, serverMod, rscEnv),
198
198
  );
199
199
 
200
200
  // generateManifestFull must run in the RSC runner realm: it invokes the
@@ -9,6 +9,7 @@
9
9
  import { contextSet, hasContextVars } from "../../context-var.js";
10
10
  import { sha256 } from "./build-versions.js";
11
11
  import {
12
+ createGetParamsReverse,
12
13
  encodePathParam,
13
14
  substituteRouteParams,
14
15
  runWithConcurrency,
@@ -76,12 +77,7 @@ export async function expandPrerenderRoutes(
76
77
  for (const { manifest: m } of allManifests) {
77
78
  if (m.routeManifest) Object.assign(allRoutes, m.routeManifest);
78
79
  }
79
- const getParamsReverse = (name: string, params?: Record<string, string>) => {
80
- const pattern = allRoutes[name];
81
- if (!pattern) throw new Error(`Unknown route: "${name}"`);
82
- if (!params) return pattern;
83
- return substituteRouteParams(pattern, params);
84
- };
80
+ const getParamsReverse = createGetParamsReverse(allRoutes);
85
81
 
86
82
  let resolvedRoutes = 0;
87
83
  let totalDynamic = 0;
@@ -52,6 +52,29 @@ export interface PluginOptions {
52
52
  * budget into its capture/render handlers.
53
53
  */
54
54
  progressiveChunkSize?: number;
55
+ /**
56
+ * The index rango()'s clientUrls() revalidate() hoist plugin shares, so
57
+ * discovery's temp server records keys into it and assigns the files the
58
+ * dev server's or build's group modules import.
59
+ */
60
+ clientUrlRevalidate?: ClientUrlRevalidateIndex;
61
+ }
62
+
63
+ /**
64
+ * What the clientUrls() revalidate() hoist
65
+ * (plugins/client-urls-revalidate-hoist.ts) shares with discovery across the
66
+ * plugin instances of one rango(), the dev server or build and discovery's
67
+ * temp server: the file of every key the hoist transform saw, in any
68
+ * environment, and each group's files, which discovery assigns from the
69
+ * group's projection and the group's `?rango-revalidate` module imports.
70
+ */
71
+ export interface ClientUrlRevalidateIndex {
72
+ readonly fileByKey: Map<string, string>;
73
+ readonly filesByGroup: Map<string, readonly string[]>;
74
+ }
75
+
76
+ export function createClientUrlRevalidateIndex(): ClientUrlRevalidateIndex {
77
+ return { fileByKey: new Map(), filesByGroup: new Map() };
55
78
  }
56
79
 
57
80
  export interface PrecomputedEntry {
@@ -129,6 +152,7 @@ export interface DiscoveryState {
129
152
  perRouterManifestDataMap: Map<string, Record<string, string>>;
130
153
  clientUrlSourceByReferenceId?: Map<string, string>;
131
154
  clientUrlProjectionMap?: Map<string, ClientUrlProjection>;
155
+ clientUrlRevalidate: ClientUrlRevalidateIndex;
132
156
 
133
157
  prerenderManifestEntries: Record<string, string> | null;
134
158
  staticManifestEntries: Record<string, string> | null;
@@ -251,6 +275,8 @@ export function createDiscoveryState(
251
275
  perRouterManifestDataMap: new Map(),
252
276
  clientUrlSourceByReferenceId: new Map(),
253
277
  clientUrlProjectionMap: new Map(),
278
+ clientUrlRevalidate:
279
+ opts?.clientUrlRevalidate ?? createClientUrlRevalidateIndex(),
254
280
 
255
281
  prerenderManifestEntries: null,
256
282
  staticManifestEntries: null,