@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -119,10 +119,10 @@ __export(client_ref_hashing_exports, {
119
119
  hashRefKey: () => hashRefKey,
120
120
  transformClientRefs: () => transformClientRefs
121
121
  });
122
- import { relative as relative2 } from "node:path";
123
- import { createHash as createHash2 } from "node:crypto";
122
+ import { relative as relative4 } from "node:path";
123
+ import { createHash as createHash3 } from "node:crypto";
124
124
  function hashRefKey(relativeId) {
125
- return createHash2("sha256").update(relativeId).digest("hex").slice(0, 12);
125
+ return createHash3("sha256").update(relativeId).digest("hex").slice(0, 12);
126
126
  }
127
127
  function computeProductionHash(projectRoot, refKey) {
128
128
  let toHash;
@@ -132,10 +132,10 @@ function computeProductionHash(projectRoot, refKey) {
132
132
  const absPath = decodeURIComponent(
133
133
  refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length)
134
134
  );
135
- toHash = relative2(projectRoot, absPath).replaceAll("\\", "/");
135
+ toHash = relative4(projectRoot, absPath).replaceAll("\\", "/");
136
136
  } else if (refKey.startsWith(FS_PREFIX)) {
137
137
  const absPath = refKey.slice(FS_PREFIX.length - 1);
138
- toHash = relative2(projectRoot, absPath).replaceAll("\\", "/");
138
+ toHash = relative4(projectRoot, absPath).replaceAll("\\", "/");
139
139
  } else if (refKey.startsWith("/")) {
140
140
  toHash = refKey.slice(1);
141
141
  } else {
@@ -733,6 +733,15 @@ var init_segment_fragments = __esm({
733
733
  }
734
734
  });
735
735
 
736
+ // src/prefetch-markers.ts
737
+ var PREFETCH_HEADER;
738
+ var init_prefetch_markers = __esm({
739
+ "src/prefetch-markers.ts"() {
740
+ "use strict";
741
+ PREFETCH_HEADER = "X-Rango-Prefetch";
742
+ }
743
+ });
744
+
736
745
  // src/browser/prefetch/fetch.ts
737
746
  function setPrefetchDecoder(fn) {
738
747
  decoder = fn;
@@ -826,7 +835,7 @@ function executePrefetchFetch(wildcardKey, sourceKey, fetchUrl, forceSourceScope
826
835
  headers: {
827
836
  "X-Rango-State": rangoState,
828
837
  "X-RSC-Router-Client-Path": window.location.href,
829
- "X-Rango-Prefetch": "1",
838
+ [PREFETCH_HEADER]: "1",
830
839
  ...isFragmentPassthroughEnabled() && {
831
840
  [SEGMENT_FRAGMENT_CAPABILITY_HEADER]: "1"
832
841
  }
@@ -1006,6 +1015,7 @@ var init_fetch = __esm({
1006
1015
  init_logging();
1007
1016
  init_response_adapter();
1008
1017
  init_segment_fragments();
1018
+ init_prefetch_markers();
1009
1019
  decoder = null;
1010
1020
  PREFETCH_FETCH_TIMEOUT_MS = 3e4;
1011
1021
  }
@@ -1633,10 +1643,10 @@ import path4 from "node:path";
1633
1643
  function isDirectivePrologueStatement(node) {
1634
1644
  return node?.type === "ExpressionStatement" && typeof node.directive === "string";
1635
1645
  }
1636
- function findImportInsertionPos(code, parseAst8) {
1646
+ function findImportInsertionPos(code, parseAst9) {
1637
1647
  let program;
1638
1648
  try {
1639
- program = parseAst8(code, { lang: "tsx" });
1649
+ program = parseAst9(code, { lang: "tsx" });
1640
1650
  } catch {
1641
1651
  return 0;
1642
1652
  }
@@ -1673,10 +1683,10 @@ function walkNode(node, parent, ancestors, enter) {
1673
1683
  }
1674
1684
  ancestors.pop();
1675
1685
  }
1676
- function findHandlerCalls(code, fnName, parseAst8) {
1686
+ function findHandlerCalls(code, fnName, parseAst9) {
1677
1687
  let program;
1678
1688
  try {
1679
- program = parseAst8(code, { lang: "tsx" });
1689
+ program = parseAst9(code, { lang: "tsx" });
1680
1690
  } catch {
1681
1691
  return [];
1682
1692
  }
@@ -1748,18 +1758,18 @@ function getImportedLocalNamesFromProgram(program, importedName) {
1748
1758
  }
1749
1759
  return localNames;
1750
1760
  }
1751
- function getImportedLocalNames(code, importedName, parseAst8) {
1761
+ function getImportedLocalNames(code, importedName, parseAst9) {
1752
1762
  try {
1753
- const program = parseAst8(code, { lang: "tsx" });
1763
+ const program = parseAst9(code, { lang: "tsx" });
1754
1764
  return getImportedLocalNamesFromProgram(program, importedName);
1755
1765
  } catch {
1756
1766
  return /* @__PURE__ */ new Set();
1757
1767
  }
1758
1768
  }
1759
- function extractImportDeclarations(code, parseAst8) {
1769
+ function extractImportDeclarations(code, parseAst9) {
1760
1770
  let program;
1761
1771
  try {
1762
- program = parseAst8(code, { lang: "tsx" });
1772
+ program = parseAst9(code, { lang: "tsx" });
1763
1773
  } catch {
1764
1774
  return [];
1765
1775
  }
@@ -1816,10 +1826,10 @@ function isSafeVariableDeclaration(node, handlerNames) {
1816
1826
  (d) => isSafeDeclaratorInit(d.init) && !(d.init?.type === "CallExpression" && d.init.callee?.type === "Identifier" && handlerNames.has(d.init.callee.name))
1817
1827
  );
1818
1828
  }
1819
- function extractModuleLevelDeclarations(code, parseAst8, handlerNames) {
1829
+ function extractModuleLevelDeclarations(code, parseAst9, handlerNames) {
1820
1830
  let program;
1821
1831
  try {
1822
- program = parseAst8(code, { lang: "tsx" });
1832
+ program = parseAst9(code, { lang: "tsx" });
1823
1833
  } catch {
1824
1834
  return [];
1825
1835
  }
@@ -1850,17 +1860,17 @@ function extractModuleLevelDeclarations(code, parseAst8, handlerNames) {
1850
1860
  }
1851
1861
  return declarations;
1852
1862
  }
1853
- function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtualRegistry, moduleId, parseAst8) {
1854
- const sites = findHandlerCalls(code, fnName, parseAst8);
1863
+ function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtualRegistry, moduleId, parseAst9) {
1864
+ const sites = findHandlerCalls(code, fnName, parseAst9);
1855
1865
  const inlineSites = sites.filter((site) => site.exportInfo === null);
1856
1866
  if (inlineSites.length === 0) return false;
1857
- const imports = extractImportDeclarations(code, parseAst8);
1858
- const staticNames = getImportedLocalNames(code, "Static", parseAst8);
1859
- const prerenderNames = getImportedLocalNames(code, "Prerender", parseAst8);
1867
+ const imports = extractImportDeclarations(code, parseAst9);
1868
+ const staticNames = getImportedLocalNames(code, "Static", parseAst9);
1869
+ const prerenderNames = getImportedLocalNames(code, "Prerender", parseAst9);
1860
1870
  const handlerNames = /* @__PURE__ */ new Set([...staticNames, ...prerenderNames]);
1861
1871
  const declarations = extractModuleLevelDeclarations(
1862
1872
  code,
1863
- parseAst8,
1873
+ parseAst9,
1864
1874
  handlerNames
1865
1875
  );
1866
1876
  const importStatements = [];
@@ -1883,7 +1893,7 @@ function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtu
1883
1893
  }
1884
1894
  if (importStatements.length > 0) {
1885
1895
  const importBlock = importStatements.join("\n") + "\n";
1886
- const insertionPos = findImportInsertionPos(code, parseAst8);
1896
+ const insertionPos = findImportInsertionPos(code, parseAst9);
1887
1897
  if (insertionPos === 0) {
1888
1898
  s.prepend(importBlock);
1889
1899
  } else {
@@ -1893,6 +1903,70 @@ function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtu
1893
1903
  return true;
1894
1904
  }
1895
1905
 
1906
+ // src/build/route-types/scan-filter.ts
1907
+ import { isAbsolute, join, relative } from "node:path";
1908
+ import { readdirSync } from "node:fs";
1909
+ import picomatch from "picomatch";
1910
+ var DEFAULT_EXCLUDE_PATTERNS = [
1911
+ "**/__tests__/**",
1912
+ "**/__mocks__/**",
1913
+ "**/dist/**",
1914
+ "**/coverage/**",
1915
+ "**/*.test.{ts,tsx,js,jsx}",
1916
+ "**/*.spec.{ts,tsx,js,jsx}"
1917
+ ];
1918
+ function createScanFilter(root, opts) {
1919
+ const { include, exclude } = opts;
1920
+ const hasInclude = include && include.length > 0;
1921
+ const hasCustomExclude = exclude !== void 0;
1922
+ if (!hasInclude && !hasCustomExclude) return void 0;
1923
+ const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
1924
+ const includeMatcher = hasInclude ? picomatch(include) : null;
1925
+ const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
1926
+ return (absolutePath) => {
1927
+ const rel = relative(root, absolutePath);
1928
+ if (excludeMatcher && excludeMatcher(rel)) return false;
1929
+ if (includeMatcher) return includeMatcher(rel);
1930
+ return true;
1931
+ };
1932
+ }
1933
+ function isUniversallySkippedScanDir(name) {
1934
+ return name === "node_modules" || name === "dist" || name === "coverage" || name.startsWith(".");
1935
+ }
1936
+ function isInSkippedScanDir(root, absolutePath) {
1937
+ const rel = relative(root, absolutePath);
1938
+ if (isAbsolute(rel)) return false;
1939
+ const dirs = rel.split(/[\\/]/).slice(0, -1);
1940
+ if (dirs[0] === "..") return false;
1941
+ return dirs.some(isUniversallySkippedScanDir);
1942
+ }
1943
+ function isSkippedSourceScanDir(name) {
1944
+ return isUniversallySkippedScanDir(name) || name === "build";
1945
+ }
1946
+ function findTsFiles(dir, filter) {
1947
+ const results = [];
1948
+ let entries;
1949
+ try {
1950
+ entries = readdirSync(dir, { withFileTypes: true });
1951
+ } catch (err) {
1952
+ console.warn(
1953
+ `[rango] Failed to scan directory ${dir}: ${err.message}`
1954
+ );
1955
+ return results;
1956
+ }
1957
+ for (const entry of entries) {
1958
+ const fullPath = join(dir, entry.name);
1959
+ if (entry.isDirectory()) {
1960
+ if (isSkippedSourceScanDir(entry.name)) continue;
1961
+ results.push(...findTsFiles(fullPath, filter));
1962
+ } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
1963
+ if (filter && !filter(fullPath)) continue;
1964
+ results.push(fullPath);
1965
+ }
1966
+ }
1967
+ return results;
1968
+ }
1969
+
1896
1970
  // src/vite/plugins/expose-ids/types.ts
1897
1971
  var PRERENDER_CONFIG = {
1898
1972
  fnName: "Prerender",
@@ -2433,6 +2507,15 @@ function compareStrings(a, b) {
2433
2507
  // src/vite/discovery/state.ts
2434
2508
  var VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
2435
2509
  var VIRTUAL_LOADER_MANIFEST_ID = "virtual:rsc-router/loader-manifest";
2510
+ function recordDiscoveryFailure(state, err) {
2511
+ state.lastDiscoveryError = {
2512
+ message: err?.message ?? String(err),
2513
+ at: Date.now()
2514
+ };
2515
+ }
2516
+ function canRefreshRuntimeDiscovery(state) {
2517
+ return state.perRouterManifests.length > 0 || !!state.lastDiscoveryError;
2518
+ }
2436
2519
  function createDiscoveryState(entryPath, opts) {
2437
2520
  return {
2438
2521
  resolvedEntryPath: entryPath,
@@ -2733,7 +2816,6 @@ ${lazyImports.join(",\n")}
2733
2816
  if (!isBuild) return;
2734
2817
  if (this.environment && this.environment.name !== "rsc") return;
2735
2818
  const fs2 = await import("node:fs/promises");
2736
- const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage"]);
2737
2819
  async function scanDir(dir) {
2738
2820
  const results = [];
2739
2821
  try {
@@ -2741,7 +2823,7 @@ ${lazyImports.join(",\n")}
2741
2823
  for (const entry of entries) {
2742
2824
  const fullPath = path4.join(dir, entry.name);
2743
2825
  if (entry.isDirectory()) {
2744
- if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
2826
+ if (!isSkippedSourceScanDir(entry.name)) {
2745
2827
  results.push(...await scanDir(fullPath));
2746
2828
  }
2747
2829
  } else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
@@ -3201,18 +3283,18 @@ async function transformUseCache(code, id, options) {
3201
3283
  return;
3202
3284
  }
3203
3285
  }
3204
- const { hasDirective: hasDirective2, transformWrapExport, transformHoistInlineDirective } = rscTransforms;
3286
+ const { hasDirective: hasDirective4, transformWrapExport, transformHoistInlineDirective } = rscTransforms;
3205
3287
  let ast;
3206
3288
  try {
3207
- const { parseAst: parseAst8 } = await import("vite");
3208
- ast = parseAst8(code, { lang: "tsx" });
3289
+ const { parseAst: parseAst9 } = await import("vite");
3290
+ ast = parseAst9(code, { lang: "tsx" });
3209
3291
  } catch {
3210
3292
  return;
3211
3293
  }
3212
3294
  stripNullDirectiveFields(ast);
3213
3295
  const filePath = normalizePath(path5.relative(options.root, id));
3214
3296
  const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
3215
- if (hasDirective2(ast.body, "use cache")) {
3297
+ if (hasDirective4(ast.body, "use cache")) {
3216
3298
  return transformFileLevelUseCache(
3217
3299
  code,
3218
3300
  ast,
@@ -3221,7 +3303,7 @@ async function transformUseCache(code, id, options) {
3221
3303
  options.isBuild,
3222
3304
  isLayoutOrTemplate,
3223
3305
  transformWrapExport,
3224
- hasDirective2
3306
+ hasDirective4
3225
3307
  );
3226
3308
  }
3227
3309
  const functionResult = transformFunctionLevelUseCache(
@@ -3235,7 +3317,7 @@ async function transformUseCache(code, id, options) {
3235
3317
  warnOnNearMissDirectives(ast, id, options.warn);
3236
3318
  if (functionResult) return functionResult;
3237
3319
  }
3238
- async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLayoutOrTemplate, transformWrapExport, hasDirective2) {
3320
+ async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLayoutOrTemplate, transformWrapExport, hasDirective4) {
3239
3321
  const unconfirmedExports = [];
3240
3322
  const { exportNames, output } = transformWrapExport(code, ast, {
3241
3323
  runtime: (value, name) => {
@@ -3247,7 +3329,7 @@ async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild
3247
3329
  if (name === "default" && isLayoutOrTemplate) return false;
3248
3330
  if (name.startsWith("$$hoist_")) return false;
3249
3331
  if (isHoistedServerReferenceRebind(meta.valueNode)) return false;
3250
- if (functionHasUseServerDirective(meta.valueNode, hasDirective2)) {
3332
+ if (functionHasUseServerDirective(meta.valueNode, hasDirective4)) {
3251
3333
  return false;
3252
3334
  }
3253
3335
  if (meta.isFunction !== true) {
@@ -3264,8 +3346,8 @@ async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild
3264
3346
  );
3265
3347
  }
3266
3348
  if (exportNames.length === 0) {
3267
- const { default: MagicString5 } = await import("magic-string");
3268
- const s = new MagicString5(code);
3349
+ const { default: MagicString6 } = await import("magic-string");
3350
+ const s = new MagicString6(code);
3269
3351
  const directive2 = findFileLevelDirective(ast);
3270
3352
  if (directive2) {
3271
3353
  s.overwrite(
@@ -3341,13 +3423,13 @@ function isHoistedServerReferenceRebind(valueNode) {
3341
3423
  const first = valueNode.arguments[0];
3342
3424
  return first !== void 0 && first.type === "Identifier" && first.name.startsWith("$$hoist_");
3343
3425
  }
3344
- function functionHasUseServerDirective(valueNode, hasDirective2) {
3426
+ function functionHasUseServerDirective(valueNode, hasDirective4) {
3345
3427
  if (!valueNode || !("body" in valueNode)) return false;
3346
3428
  const { body } = valueNode;
3347
3429
  if (!body || Array.isArray(body) || body.type !== "BlockStatement") {
3348
3430
  return false;
3349
3431
  }
3350
- return hasDirective2(
3432
+ return hasDirective4(
3351
3433
  body.body,
3352
3434
  "use server"
3353
3435
  );
@@ -3390,10 +3472,10 @@ function warnOnNearMissDirectives(ast, fileId, warn) {
3390
3472
  }
3391
3473
 
3392
3474
  // src/vite/plugins/transition-when-hoist.ts
3393
- import { normalizePath as normalizePath2, parseAst as parseAst3 } from "vite";
3475
+ import { normalizePath as normalizePath3, parseAst as parseAst3 } from "vite";
3394
3476
  import MagicString4 from "magic-string";
3395
3477
  import { builtinModules } from "node:module";
3396
- import { readFile } from "node:fs/promises";
3478
+ import { hasDirective as hasDirective2 } from "@vitejs/plugin-rsc/transforms";
3397
3479
 
3398
3480
  // src/route-name.ts
3399
3481
  var AUTO_GENERATED_ROUTE_PREFIX = "$path_";
@@ -3424,10 +3506,7 @@ function findTransitionWhenError(error) {
3424
3506
  return void 0;
3425
3507
  }
3426
3508
 
3427
- // src/vite/plugins/transition-when-hoist.ts
3428
- var TRANSITION_WHEN_QUERY = "rango-when";
3429
- var EXPORT_NAME = "__rango_when";
3430
- var LOCAL_PREFIX = "__rango_when_";
3509
+ // src/vite/plugins/hoist/scope.ts
3431
3510
  var Scope = class {
3432
3511
  constructor(parent, isFunction, node) {
3433
3512
  this.parent = parent;
@@ -3733,29 +3812,23 @@ function collectImports(program) {
3733
3812
  }
3734
3813
  return imports;
3735
3814
  }
3736
- function hasUseClientDirective(program) {
3815
+
3816
+ // src/vite/plugins/hoist/literal.ts
3817
+ function directivePrologueEnd(program) {
3818
+ let end = 0;
3737
3819
  for (const statement of program.body) {
3738
3820
  if (statement.type !== "ExpressionStatement" || typeof statement.directive !== "string") {
3739
- return false;
3821
+ break;
3740
3822
  }
3741
- if (statement.directive === "use client") return true;
3823
+ end = statement.end;
3742
3824
  }
3743
- return false;
3825
+ return end;
3744
3826
  }
3745
- function lineColumn(code, offset) {
3746
- let line = 1;
3747
- let column = 1;
3748
- for (let i = 0; i < offset && i < code.length; i++) {
3749
- if (code[i] === "\n") {
3750
- line++;
3751
- column = 1;
3752
- } else {
3753
- column++;
3754
- }
3755
- }
3756
- return `${line}:${column}`;
3827
+ function parseLang(file) {
3828
+ if (/\.[cm]?ts$/.test(file)) return "ts";
3829
+ if (/\.[cm]?jsx?$/.test(file)) return "jsx";
3830
+ return "tsx";
3757
3831
  }
3758
- var ROUTER_SOURCE = "@rangojs/router";
3759
3832
  var EXPRESSION_WRAPPERS = /* @__PURE__ */ new Set([
3760
3833
  "TSAsExpression",
3761
3834
  "TSSatisfiesExpression",
@@ -3773,51 +3846,50 @@ function isFunctionLiteral(node) {
3773
3846
  function propertyKeyIs(prop, name) {
3774
3847
  return prop?.type === "Property" && !prop.computed && (prop.key.type === "Identifier" && prop.key.name === name || prop.key.type === "Literal" && prop.key.value === name);
3775
3848
  }
3776
- function findWhenSites(program, scopeOf, moduleScope, imports) {
3777
- const sites = [];
3849
+ function findHelperCalls(program, scopeOf, moduleScope, imports, spec) {
3850
+ const calls = [];
3778
3851
  const helpers = /* @__PURE__ */ new Map();
3779
- const routerExport = (callee, scope) => {
3780
- const routerImport = (id) => {
3852
+ const sourceExport = (callee, scope) => {
3853
+ const sourceImport = (id) => {
3781
3854
  if (id?.type !== "Identifier") return null;
3782
3855
  if (scope.lookup(id.name) !== moduleScope) return null;
3783
3856
  const binding = imports.get(id.name);
3784
- return binding?.source === ROUTER_SOURCE && !binding.typeOnly ? binding : null;
3857
+ return binding && spec.sources.has(binding.source) && !binding.typeOnly ? binding : null;
3785
3858
  };
3786
3859
  if (callee.type === "Identifier") {
3787
- const imported = routerImport(callee)?.imported;
3860
+ const imported = sourceImport(callee)?.imported;
3788
3861
  return imported && imported !== "*" && imported !== "default" ? imported : null;
3789
3862
  }
3790
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && routerImport(callee.object)?.imported === "*") {
3863
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && sourceImport(callee.object)?.imported === "*") {
3791
3864
  return callee.property.name;
3792
3865
  }
3793
3866
  return null;
3794
3867
  };
3795
- const registerUrlsCallback = (callback) => {
3868
+ const registerBuilderCallback = (callback) => {
3796
3869
  const fnScope = scopeOf.get(callback);
3797
3870
  let param = callback.params[0];
3798
3871
  if (param?.type === "AssignmentPattern") param = param.left;
3799
3872
  if (!fnScope || !param) return;
3800
- const bindings = {
3801
- transitions: /* @__PURE__ */ new Set(),
3802
- objects: /* @__PURE__ */ new Set()
3803
- };
3873
+ const bindings = { names: /* @__PURE__ */ new Set(), objects: /* @__PURE__ */ new Set() };
3804
3874
  if (param.type === "Identifier") bindings.objects.add(param.name);
3805
3875
  if (param.type === "ObjectPattern") {
3806
3876
  for (const prop of param.properties) {
3807
- if (!propertyKeyIs(prop, "transition")) continue;
3877
+ if (!propertyKeyIs(prop, spec.helper)) continue;
3808
3878
  const target = prop.value.type === "AssignmentPattern" ? prop.value.left : prop.value;
3809
- if (target.type === "Identifier") bindings.transitions.add(target.name);
3879
+ if (target.type === "Identifier") bindings.names.add(target.name);
3810
3880
  }
3811
3881
  }
3812
3882
  helpers.set(fnScope, bindings);
3813
3883
  };
3814
- const isRouterTransition = (callee, scope) => {
3815
- if (routerExport(callee, scope) === "transition") return true;
3884
+ const isHelper = (callee, scope) => {
3885
+ if (spec.directImport && sourceExport(callee, scope) === spec.helper) {
3886
+ return true;
3887
+ }
3816
3888
  if (callee.type === "Identifier") {
3817
3889
  const decl = scope.lookup(callee.name);
3818
- return !!decl && !!helpers.get(decl)?.transitions.has(callee.name);
3890
+ return !!decl && !!helpers.get(decl)?.names.has(callee.name);
3819
3891
  }
3820
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "transition" && callee.object.type === "Identifier") {
3892
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === spec.helper && callee.object.type === "Identifier") {
3821
3893
  const decl = scope.lookup(callee.object.name);
3822
3894
  return !!decl && !!helpers.get(decl)?.objects.has(callee.object.name);
3823
3895
  }
@@ -3833,20 +3905,10 @@ function findWhenSites(program, scopeOf, moduleScope, imports) {
3833
3905
  const own = scopeOf.get(node) ?? scope;
3834
3906
  if (node.type === "CallExpression") {
3835
3907
  const callback = node.arguments[0];
3836
- if (isFunctionLiteral(callback) && routerExport(node.callee, own) === "urls") {
3837
- registerUrlsCallback(callback);
3838
- }
3839
- if (node.arguments[0]?.type === "ObjectExpression" && isRouterTransition(node.callee, own)) {
3840
- for (const prop of node.arguments[0].properties) {
3841
- if (propertyKeyIs(prop, "when")) {
3842
- sites.push({
3843
- prop,
3844
- value: unwrapExpression(prop.value),
3845
- scope: own
3846
- });
3847
- }
3848
- }
3908
+ if (isFunctionLiteral(callback) && sourceExport(node.callee, own) === spec.builder) {
3909
+ registerBuilderCallback(callback);
3849
3910
  }
3911
+ if (isHelper(node.callee, own)) calls.push({ call: node, scope: own });
3850
3912
  }
3851
3913
  for (const key of Object.keys(node)) {
3852
3914
  if (key === "type" || key === "start" || key === "end") continue;
@@ -3854,27 +3916,21 @@ function findWhenSites(program, scopeOf, moduleScope, imports) {
3854
3916
  }
3855
3917
  };
3856
3918
  visit(program.body, moduleScope);
3857
- return sites;
3919
+ return calls;
3858
3920
  }
3859
- var LEXICAL_CAPTURE = {
3860
- this: "`this` in an arrow is the enclosing server function's `this`.",
3861
- arguments: "`arguments` in an arrow is the enclosing server function's `arguments`.",
3862
- super: "`super` refers to the enclosing server object.",
3863
- "new.target": "`new.target` in an arrow is the enclosing server function's."
3864
- };
3865
- function findLexicalCapture(literal, references, code) {
3921
+ function findLexicalCapture(literal, references, code, messages) {
3866
3922
  const ownArguments = [];
3867
3923
  let found = null;
3868
3924
  const capture = (node, what) => {
3869
3925
  found = {
3870
3926
  node,
3871
- detail: `${LEXICAL_CAPTURE[what]} Read what you need from \`to.params\`/\`to.state\`, or export the predicate from a "use client" module.`
3927
+ detail: `${messages.lexical[what]} ${messages.captureHint}`
3872
3928
  };
3873
3929
  };
3874
3930
  const loads = (node, expression, why) => {
3875
3931
  found = {
3876
3932
  node,
3877
- detail: `\`${expression}\` ${why} Import client-safe modules statically, or export the predicate from a "use client" module.`
3933
+ detail: `\`${expression}\` ${why} ${messages.loadsHint}`
3878
3934
  };
3879
3935
  };
3880
3936
  const visit = (node, boundary) => {
@@ -3907,7 +3963,7 @@ function findLexicalCapture(literal, references, code) {
3907
3963
  }
3908
3964
  case "MetaProperty":
3909
3965
  if (node.meta.name === "import") {
3910
- loads(node, "import.meta", "is the server module's.");
3966
+ loads(node, "import.meta", messages.importMetaWhy);
3911
3967
  } else if (!boundary) {
3912
3968
  capture(node, "new.target");
3913
3969
  }
@@ -3919,7 +3975,7 @@ function findLexicalCapture(literal, references, code) {
3919
3975
  loads(
3920
3976
  node,
3921
3977
  node.computed ? "import.meta[...]" : `import.meta.${node.property.name}`,
3922
- "is resolved against the server module; only `import.meta.env` is available to an inline predicate."
3978
+ messages.importMetaMemberWhy
3923
3979
  );
3924
3980
  return;
3925
3981
  }
@@ -3928,7 +3984,7 @@ function findLexicalCapture(literal, references, code) {
3928
3984
  loads(
3929
3985
  node,
3930
3986
  `import(${code.slice(node.source.start, node.source.end)})`,
3931
- "loads a module at run time, which the build cannot check is client-safe."
3987
+ messages.dynamicImportWhy
3932
3988
  );
3933
3989
  return;
3934
3990
  }
@@ -3953,32 +4009,150 @@ function findLexicalCapture(literal, references, code) {
3953
4009
  }
3954
4010
  return null;
3955
4011
  }
4012
+ function collectFreeReferences(literal, references, moduleScope, imports) {
4013
+ const used = /* @__PURE__ */ new Map();
4014
+ const bindings = [];
4015
+ const globals = [];
4016
+ for (const ref of references) {
4017
+ if (ref.node.start < literal.start || ref.node.end > literal.end) continue;
4018
+ const decl = ref.scope.lookup(ref.node.name);
4019
+ if (!decl) {
4020
+ if (!ref.typeOnly) globals.push(ref);
4021
+ continue;
4022
+ }
4023
+ if (decl.node.start >= literal.start && decl.node.end <= literal.end && decl !== moduleScope) {
4024
+ continue;
4025
+ }
4026
+ if (decl === moduleScope && imports.has(ref.node.name)) {
4027
+ const valueUse = !ref.typeOnly && !imports.get(ref.node.name).typeOnly;
4028
+ used.set(ref.node.name, (used.get(ref.node.name) ?? false) || valueUse);
4029
+ continue;
4030
+ }
4031
+ if (ref.typeOnly) continue;
4032
+ bindings.push(ref);
4033
+ }
4034
+ return { imports: used, bindings, globals };
4035
+ }
4036
+ async function emitImports(used, imports, checkValueImport) {
4037
+ const bySource = /* @__PURE__ */ new Map();
4038
+ for (const [local, valueUse] of used) {
4039
+ const binding = imports.get(local);
4040
+ if (valueUse) await checkValueImport(local, binding);
4041
+ const lines = bySource.get(binding.source) ?? [];
4042
+ const typePrefix = valueUse ? "" : "type ";
4043
+ if (binding.imported === "*") {
4044
+ lines.push(
4045
+ `import ${typePrefix}* as ${local} from ${JSON.stringify(binding.source)};`
4046
+ );
4047
+ } else if (binding.imported === "default") {
4048
+ lines.push(
4049
+ `import ${typePrefix}${local} from ${JSON.stringify(binding.source)};`
4050
+ );
4051
+ } else {
4052
+ const spec = binding.imported === local ? local : `${JSON.stringify(binding.imported)} as ${local}`;
4053
+ lines.push(
4054
+ `import ${typePrefix}{ ${spec} } from ${JSON.stringify(binding.source)};`
4055
+ );
4056
+ }
4057
+ bySource.set(binding.source, lines);
4058
+ }
4059
+ return [...bySource.values()].flat();
4060
+ }
4061
+
4062
+ // src/vite/plugins/hoist/source.ts
4063
+ import { readFile } from "node:fs/promises";
4064
+ import { normalizePath as normalizePath2 } from "vite";
4065
+ var SOURCE_FILE_RE = /\.[cm]?[jt]sx?$/;
4066
+ async function readSource(file) {
4067
+ try {
4068
+ return await readFile(file, "utf-8");
4069
+ } catch {
4070
+ return null;
4071
+ }
4072
+ }
4073
+ async function resolveSourceTarget(ctx, source, importer, options) {
4074
+ const resolved2 = await ctx.resolve(source, importer);
4075
+ if (!resolved2 || resolved2.external) return null;
4076
+ const target = resolved2.id.split("?", 1)[0];
4077
+ if (target.startsWith("\0")) return null;
4078
+ if (options.skipNodeModules && target.includes("/node_modules/")) {
4079
+ return null;
4080
+ }
4081
+ return target;
4082
+ }
4083
+ async function recordEmitted(emitted, file, value) {
4084
+ emitted.set(file, { disk: await readSource(file), value });
4085
+ }
4086
+ async function loadEmittedOrDisk(emitted, hoistedFile, rehoist) {
4087
+ const disk = await readSource(hoistedFile);
4088
+ const entry = emitted.get(normalizePath2(hoistedFile));
4089
+ if (entry && (disk === null || entry.disk === disk)) return entry.value;
4090
+ return disk === null ? void 0 : rehoist(disk);
4091
+ }
4092
+
4093
+ // src/vite/plugins/transition-when-hoist.ts
4094
+ var TRANSITION_WHEN_QUERY = "rango-when";
4095
+ var EXPORT_NAME = "__rango_when";
4096
+ var LOCAL_PREFIX = "__rango_when_";
4097
+ var ROUTER_SOURCES = /* @__PURE__ */ new Set(["@rangojs/router"]);
4098
+ function findWhenSites(program, scopeOf, moduleScope, imports) {
4099
+ const sites = [];
4100
+ for (const { call, scope } of findHelperCalls(
4101
+ program,
4102
+ scopeOf,
4103
+ moduleScope,
4104
+ imports,
4105
+ {
4106
+ sources: ROUTER_SOURCES,
4107
+ builder: "urls",
4108
+ helper: "transition",
4109
+ directImport: true
4110
+ }
4111
+ )) {
4112
+ if (call.arguments[0]?.type !== "ObjectExpression") continue;
4113
+ for (const prop of call.arguments[0].properties) {
4114
+ if (propertyKeyIs(prop, "when")) {
4115
+ sites.push({ prop, value: unwrapExpression(prop.value), scope });
4116
+ }
4117
+ }
4118
+ }
4119
+ return sites;
4120
+ }
4121
+ var WHEN_CAPTURE_MESSAGES = {
4122
+ lexical: {
4123
+ this: "`this` in an arrow is the enclosing server function's `this`.",
4124
+ arguments: "`arguments` in an arrow is the enclosing server function's `arguments`.",
4125
+ super: "`super` refers to the enclosing server object.",
4126
+ "new.target": "`new.target` in an arrow is the enclosing server function's."
4127
+ },
4128
+ captureHint: 'Read what you need from `to.params`/`to.state`, or export the predicate from a "use client" module.',
4129
+ loadsHint: 'Import client-safe modules statically, or export the predicate from a "use client" module.',
4130
+ importMetaWhy: "is the server module's.",
4131
+ importMetaMemberWhy: "is resolved against the server module; only `import.meta.env` is available to an inline predicate.",
4132
+ dynamicImportWhy: "loads a module at run time, which the build cannot check is client-safe."
4133
+ };
3956
4134
  var NODE_BUILTINS = new Set(builtinModules);
3957
4135
  function isServerOnlySpecifier(source) {
3958
4136
  return source === "server-only" || source.startsWith("node:") || source.startsWith("cloudflare:") || NODE_BUILTINS.has(source);
3959
4137
  }
3960
4138
  var ROUTER_IMPORT_RE = /["']@rangojs\/router["']/;
3961
4139
  var WHEN_RE = /\bwhen\b/;
3962
- function parseLang(file) {
3963
- if (/\.[cm]?ts$/.test(file)) return "ts";
3964
- if (/\.[cm]?jsx?$/.test(file)) return "jsx";
3965
- return "tsx";
3966
- }
3967
4140
  function whenError(file, code, node, detail) {
4141
+ const { line, column } = offsetToLineColumn(code, node.start);
3968
4142
  return new TransitionWhenError(
3969
- `[rango] transition({ when }) at ${file}:${lineColumn(code, node.start)}: \`when\` runs in the browser; ${detail}`
4143
+ `[rango] transition({ when }) at ${file}:${line}:${column}: \`when\` runs in the browser; ${detail}`
3970
4144
  );
3971
4145
  }
3972
4146
  async function hoistTransitionWhens(code, id, deps = {}) {
3973
4147
  if (!ROUTER_IMPORT_RE.test(code) || !WHEN_RE.test(code)) return null;
3974
- const file = normalizePath2(id.split("?", 1)[0]);
4148
+ const file = normalizePath3(id.split("?", 1)[0]);
3975
4149
  let program;
3976
4150
  try {
3977
4151
  program = parseAst3(code, { lang: parseLang(file) });
3978
4152
  } catch {
3979
4153
  return null;
3980
4154
  }
3981
- if (hasUseClientDirective(program)) return null;
4155
+ if (hasDirective2(program.body, "use client")) return null;
3982
4156
  const { moduleScope, references, scopeOf } = analyzeScopes(program);
3983
4157
  const imports = collectImports(program);
3984
4158
  const sites = findWhenSites(program, scopeOf, moduleScope, imports);
@@ -4009,66 +4183,43 @@ async function hoistTransitionWhens(code, id, deps = {}) {
4009
4183
  continue;
4010
4184
  }
4011
4185
  if (!isFunctionLiteral(value)) continue;
4012
- const lexical = findLexicalCapture(value, references, code);
4186
+ const lexical = findLexicalCapture(
4187
+ value,
4188
+ references,
4189
+ code,
4190
+ WHEN_CAPTURE_MESSAGES
4191
+ );
4013
4192
  if (lexical) throw whenError(file, code, lexical.node, lexical.detail);
4014
- const used = /* @__PURE__ */ new Map();
4015
- for (const ref of references) {
4016
- if (ref.node.start < value.start || ref.node.end > value.end) continue;
4017
- const decl = ref.scope.lookup(ref.node.name);
4018
- if (!decl) continue;
4019
- if (decl.node.start >= value.start && decl.node.end <= value.end && decl !== moduleScope) {
4020
- continue;
4021
- }
4022
- if (decl === moduleScope && imports.has(ref.node.name)) {
4023
- const valueUse = !ref.typeOnly && !imports.get(ref.node.name).typeOnly;
4024
- used.set(ref.node.name, (used.get(ref.node.name) ?? false) || valueUse);
4025
- continue;
4026
- }
4027
- if (ref.typeOnly) continue;
4193
+ const free = collectFreeReferences(value, references, moduleScope, imports);
4194
+ const binding = free.bindings[0];
4195
+ if (binding) {
4028
4196
  throw whenError(
4029
4197
  file,
4030
4198
  code,
4031
- ref.node,
4032
- `\`${ref.node.name}\` is a server-module binding. Read it from \`to.params\`/\`to.state\`, or export the predicate from a "use client" module.`
4199
+ binding.node,
4200
+ `\`${binding.node.name}\` is a server-module binding. Read it from \`to.params\`/\`to.state\`, or export the predicate from a "use client" module.`
4033
4201
  );
4034
4202
  }
4035
- const bySource = /* @__PURE__ */ new Map();
4036
- for (const [local2, valueUse] of used) {
4037
- const binding = imports.get(local2);
4038
- if (valueUse) {
4039
- if (isServerOnlySpecifier(binding.source) || await deps.isServerOnlyImport?.(binding.source)) {
4203
+ const importLines = await emitImports(
4204
+ free.imports,
4205
+ imports,
4206
+ async (local2, imported) => {
4207
+ if (isServerOnlySpecifier(imported.source) || await deps.isServerOnlyImport?.(imported.source)) {
4040
4208
  throw whenError(
4041
4209
  file,
4042
4210
  code,
4043
4211
  value,
4044
- `\`${local2}\` is imported from "${binding.source}", a server-only module. Import only client-safe modules into an inline predicate, or export it from a "use client" module.`
4212
+ `\`${local2}\` is imported from "${imported.source}", a server-only module. Import only client-safe modules into an inline predicate, or export it from a "use client" module.`
4045
4213
  );
4046
4214
  }
4047
4215
  }
4048
- const lines = bySource.get(binding.source) ?? [];
4049
- const typePrefix = valueUse ? "" : "type ";
4050
- if (binding.imported === "*") {
4051
- lines.push(
4052
- `import ${typePrefix}* as ${local2} from ${JSON.stringify(binding.source)};`
4053
- );
4054
- } else if (binding.imported === "default") {
4055
- lines.push(
4056
- `import ${typePrefix}${local2} from ${JSON.stringify(binding.source)};`
4057
- );
4058
- } else {
4059
- const spec = binding.imported === local2 ? local2 : `${JSON.stringify(binding.imported)} as ${local2}`;
4060
- lines.push(
4061
- `import ${typePrefix}{ ${spec} } from ${JSON.stringify(binding.source)};`
4062
- );
4063
- }
4064
- bySource.set(binding.source, lines);
4065
- }
4216
+ );
4066
4217
  const source = prop.method ? `${value.async ? "async " : ""}function${value.generator ? "*" : ""} ` + code.slice(value.start, value.end) : code.slice(value.start, value.end);
4067
4218
  const index = modules.length;
4068
4219
  modules.push(
4069
4220
  [
4070
4221
  `"use client";`,
4071
- ...[...bySource.values()].flat(),
4222
+ ...importLines,
4072
4223
  `export const ${EXPORT_NAME} = ${source};`,
4073
4224
  ""
4074
4225
  ].join("\n")
@@ -4086,13 +4237,7 @@ async function hoistTransitionWhens(code, id, deps = {}) {
4086
4237
  );
4087
4238
  }
4088
4239
  if (modules.length === 0) return null;
4089
- let insertAt = 0;
4090
- for (const statement of program.body) {
4091
- if (statement.type !== "ExpressionStatement" || typeof statement.directive !== "string") {
4092
- break;
4093
- }
4094
- insertAt = statement.end;
4095
- }
4240
+ const insertAt = directivePrologueEnd(program);
4096
4241
  s.appendLeft(insertAt, `${insertAt ? "\n" : ""}${header.join("\n")}
4097
4242
  `);
4098
4243
  return {
@@ -4110,13 +4255,6 @@ function parseHoistedWhenId(id) {
4110
4255
  return { file: id.slice(0, q), index: Number(raw) };
4111
4256
  }
4112
4257
  var SERVER_ONLY_IMPORT_RE = /\bimport\s+["']server-only["']/;
4113
- async function readSource(file) {
4114
- try {
4115
- return await readFile(file, "utf-8");
4116
- } catch {
4117
- return null;
4118
- }
4119
- }
4120
4258
  function transitionWhenHoistPlugin() {
4121
4259
  const emitted = /* @__PURE__ */ new Map();
4122
4260
  return {
@@ -4125,10 +4263,11 @@ function transitionWhenHoistPlugin() {
4125
4263
  async load(id) {
4126
4264
  const hoisted = parseHoistedWhenId(id);
4127
4265
  if (!hoisted) return;
4128
- const file = normalizePath2(hoisted.file);
4129
- const disk = await readSource(hoisted.file);
4130
- const entry = emitted.get(file);
4131
- const modules = entry && (disk === null || entry.disk === disk) ? entry.modules : disk === null ? void 0 : (await hoistTransitionWhens(disk, hoisted.file))?.modules;
4266
+ const modules = await loadEmittedOrDisk(
4267
+ emitted,
4268
+ hoisted.file,
4269
+ async (disk) => (await hoistTransitionWhens(disk, hoisted.file))?.modules
4270
+ );
4132
4271
  const module = modules?.[hoisted.index];
4133
4272
  if (module === void 0) {
4134
4273
  throw new TransitionWhenError(
@@ -4140,35 +4279,397 @@ function transitionWhenHoistPlugin() {
4140
4279
  async transform(code, id) {
4141
4280
  if (this.environment?.name !== "rsc") return;
4142
4281
  if (id.includes("/node_modules/") || parseHoistedWhenId(id)) return;
4143
- const file = normalizePath2(id.split("?", 1)[0]);
4144
- if (!/\.[cm]?[jt]sx?$/.test(file)) return;
4282
+ const file = normalizePath3(id.split("?", 1)[0]);
4283
+ if (!SOURCE_FILE_RE.test(file)) return;
4145
4284
  emitted.delete(file);
4146
4285
  const result = await hoistTransitionWhens(code, id, {
4147
4286
  isServerOnlyImport: async (source) => {
4148
- const resolved2 = await this.resolve(source, id);
4149
- if (!resolved2 || resolved2.external) return false;
4150
- const target = resolved2.id.split("?", 1)[0];
4151
- if (target.includes("/node_modules/") || target.startsWith("\0")) {
4152
- return false;
4153
- }
4287
+ const target = await resolveSourceTarget(this, source, id, {
4288
+ skipNodeModules: true
4289
+ });
4290
+ if (target === null) return false;
4154
4291
  const text = await readSource(target);
4155
4292
  return text !== null && SERVER_ONLY_IMPORT_RE.test(text);
4156
4293
  }
4157
4294
  });
4158
4295
  if (!result) return;
4159
- emitted.set(file, {
4160
- disk: await readSource(file),
4161
- modules: result.modules
4162
- });
4296
+ await recordEmitted(emitted, file, result.modules);
4163
4297
  return { code: result.code, map: result.map };
4164
4298
  }
4165
4299
  };
4166
4300
  }
4167
4301
 
4302
+ // src/vite/plugins/client-urls-revalidate-hoist.ts
4303
+ import { normalizePath as normalizePath4, parseAst as parseAst5 } from "vite";
4304
+ import MagicString5 from "magic-string";
4305
+ import { createHash as createHash2 } from "node:crypto";
4306
+ import { relative as relative2 } from "node:path";
4307
+ import { hasDirective as hasDirective3 } from "@vitejs/plugin-rsc/transforms";
4308
+
4309
+ // src/client-urls/hoisted-revalidate.ts
4310
+ var HOISTED_REVALIDATE_KEY = "$$rangoRevalidate";
4311
+ function isHoistedRevalidateRef(value) {
4312
+ return typeof value === "object" && value !== null && typeof value[HOISTED_REVALIDATE_KEY] === "string";
4313
+ }
4314
+
4315
+ // src/vite/utils/directive-prologue.ts
4316
+ import { parseAst as parseAst4 } from "vite";
4317
+ function hasUseClientDirective(source) {
4318
+ let program;
4319
+ try {
4320
+ program = parseAst4(source, { lang: "tsx" });
4321
+ } catch {
4322
+ return false;
4323
+ }
4324
+ for (const node of program.body ?? []) {
4325
+ if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
4326
+ if (node.expression.value === "use client") return true;
4327
+ continue;
4328
+ }
4329
+ break;
4330
+ }
4331
+ return false;
4332
+ }
4333
+
4334
+ // src/vite/plugins/client-urls-revalidate-hoist.ts
4335
+ var CLIENT_URLS_REVALIDATE_QUERY = "rango-revalidate";
4336
+ var REGISTER = "__rango_registerClientUrlRevalidators";
4337
+ var CLIENT_SOURCES = /* @__PURE__ */ new Set(["@rangojs/router/client"]);
4338
+ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
4339
+ "window",
4340
+ "document",
4341
+ "navigator",
4342
+ "localStorage",
4343
+ "sessionStorage",
4344
+ "location",
4345
+ "history"
4346
+ ]);
4347
+ var NOT_A_FUNCTION = /* @__PURE__ */ new Set([
4348
+ "ObjectExpression",
4349
+ "ArrayExpression",
4350
+ "Literal",
4351
+ "TemplateLiteral",
4352
+ "ClassExpression"
4353
+ ]);
4354
+ var CAPTURE_MESSAGES = {
4355
+ lexical: {
4356
+ this: "`this` in an arrow is the enclosing function's `this`.",
4357
+ arguments: "`arguments` in an arrow is the enclosing function's.",
4358
+ super: "`super` refers to the enclosing object.",
4359
+ "new.target": "`new.target` in an arrow is the enclosing function's."
4360
+ },
4361
+ captureHint: "Read what you need from the predicate's argument.",
4362
+ loadsHint: "Import server-safe modules statically.",
4363
+ importMetaWhy: `is the "use client" module's.`,
4364
+ importMetaMemberWhy: 'is resolved against the "use client" module; only `import.meta.env` is available to a hoisted predicate.',
4365
+ dynamicImportWhy: "loads a module at run time, which the build cannot check is server-safe."
4366
+ };
4367
+ var CLIENT_URLS_RE = /\bclientUrls\b/;
4368
+ var CLIENT_IMPORT_RE = /["']@rangojs\/router\/client["']/;
4369
+ var REVALIDATE_RE = /\brevalidate\b/;
4370
+ function mayHoist(code) {
4371
+ return code.includes("use client") && CLIENT_URLS_RE.test(code) && CLIENT_IMPORT_RE.test(code) && REVALIDATE_RE.test(code);
4372
+ }
4373
+ function revalidateError(file, code, node, detail) {
4374
+ const { line, column } = offsetToLineColumn(code, node.start);
4375
+ return new Error(
4376
+ `[rango] clientUrls() revalidate() at ${file}:${line}:${column}: revalidate() runs on the server; ${detail}`
4377
+ );
4378
+ }
4379
+ var FINGERPRINT_SKIP = /* @__PURE__ */ new Set([
4380
+ "type",
4381
+ "start",
4382
+ "end",
4383
+ "loc",
4384
+ "range",
4385
+ "raw",
4386
+ "extra",
4387
+ "shorthand",
4388
+ // TS-only flags (`let x!: T`), lost with the types.
4389
+ "definite",
4390
+ ...TYPE_KEYS
4391
+ ]);
4392
+ function fingerprint(node) {
4393
+ const out = [];
4394
+ const visit = (value) => {
4395
+ if (value === null || value === void 0) return;
4396
+ if (Array.isArray(value)) {
4397
+ out.push("[");
4398
+ for (const item of value) visit(item);
4399
+ out.push("]");
4400
+ return;
4401
+ }
4402
+ if (typeof value !== "object") {
4403
+ out.push(JSON.stringify(typeof value === "bigint" ? `${value}n` : value));
4404
+ return;
4405
+ }
4406
+ let entry = value;
4407
+ if (typeof entry.type === "string") {
4408
+ entry = unwrapExpression(entry);
4409
+ if (entry.type.startsWith("TS")) {
4410
+ visit(entry.expression);
4411
+ return;
4412
+ }
4413
+ out.push(entry.type, "(");
4414
+ if (entry.type === "TemplateElement") {
4415
+ out.push(JSON.stringify(entry.value.raw));
4416
+ }
4417
+ }
4418
+ for (const key of Object.keys(entry)) {
4419
+ if (FINGERPRINT_SKIP.has(key)) continue;
4420
+ if (key === "optional" && entry.type === "Identifier") continue;
4421
+ out.push(key);
4422
+ visit(entry[key]);
4423
+ }
4424
+ if (typeof entry.type === "string") out.push(")");
4425
+ };
4426
+ visit(node);
4427
+ return out.join(" ");
4428
+ }
4429
+ function keyOf(relativeFile, predicate) {
4430
+ return createHash2("sha256").update(relativeFile).update("\0").update(fingerprint(predicate)).digest("hex").slice(0, 12);
4431
+ }
4432
+ async function hoistClientUrlsRevalidates(code, id, deps) {
4433
+ if (!mayHoist(code)) return null;
4434
+ const file = normalizePath4(id.split("?", 1)[0]);
4435
+ let program;
4436
+ try {
4437
+ program = parseAst5(code, { lang: parseLang(file) });
4438
+ } catch {
4439
+ return null;
4440
+ }
4441
+ if (!hasDirective3(program.body, "use client")) return null;
4442
+ const { moduleScope, references, scopeOf } = analyzeScopes(program);
4443
+ const imports = collectImports(program);
4444
+ const calls = findHelperCalls(program, scopeOf, moduleScope, imports, {
4445
+ sources: CLIENT_SOURCES,
4446
+ builder: "clientUrls",
4447
+ helper: "revalidate",
4448
+ directImport: false
4449
+ });
4450
+ if (calls.length === 0) return null;
4451
+ const relativeFile = normalizePath4(relative2(deps.root, file));
4452
+ const s = new MagicString5(code);
4453
+ const used = /* @__PURE__ */ new Map();
4454
+ const entries = /* @__PURE__ */ new Map();
4455
+ for (const { call } of calls) {
4456
+ const [arg, extra] = call.arguments;
4457
+ if (!arg || extra || arg.type === "SpreadElement") {
4458
+ throw revalidateError(
4459
+ file,
4460
+ code,
4461
+ call,
4462
+ "`revalidate()` takes exactly one predicate."
4463
+ );
4464
+ }
4465
+ const value = unwrapExpression(arg);
4466
+ if (NOT_A_FUNCTION.has(value.type)) {
4467
+ throw revalidateError(
4468
+ file,
4469
+ code,
4470
+ value,
4471
+ "`revalidate()` expects a predicate function."
4472
+ );
4473
+ }
4474
+ if (isFunctionLiteral(value) && (value.async || value.generator)) {
4475
+ throw revalidateError(
4476
+ file,
4477
+ code,
4478
+ value,
4479
+ `a${value.async ? "n `async`" : " generator"} function returns ${value.async ? "a Promise" : "an iterator"}, which the router ignores: return a boolean, \`{ defaultShouldRevalidate }\`, or nothing.`
4480
+ );
4481
+ }
4482
+ const lexical = findLexicalCapture(
4483
+ value,
4484
+ references,
4485
+ code,
4486
+ CAPTURE_MESSAGES
4487
+ );
4488
+ if (lexical)
4489
+ throw revalidateError(file, code, lexical.node, lexical.detail);
4490
+ const free = collectFreeReferences(value, references, moduleScope, imports);
4491
+ const binding = free.bindings[0];
4492
+ if (binding) {
4493
+ const name = binding.node.name;
4494
+ throw revalidateError(
4495
+ file,
4496
+ code,
4497
+ binding.node,
4498
+ binding.scope.lookup(name) === moduleScope ? `\`${name}\` is declared in this "use client" module. Move the predicate to its own module (without "use client") and import it, or write it inline.` : `\`${name}\` is a binding of the enclosing function. Read what you need from the predicate's argument (\`currentParams\`, \`nextUrl\`, \`context\`, \`actionResult\`, ...).`
4499
+ );
4500
+ }
4501
+ const browserGlobal = free.globals.find(
4502
+ (ref) => BROWSER_GLOBALS.has(ref.node.name)
4503
+ );
4504
+ if (browserGlobal) {
4505
+ throw revalidateError(
4506
+ file,
4507
+ code,
4508
+ browserGlobal.node,
4509
+ `\`${browserGlobal.node.name}\` is a browser global. Put the input on the request (a cookie or a search param) and read it from the predicate's argument, or refresh the loader with useRefreshLoaders.`
4510
+ );
4511
+ }
4512
+ const unchecked = [...free.imports].filter(
4513
+ ([local, valueUse]) => valueUse && !used.get(local)
4514
+ );
4515
+ const clientOnly = await Promise.all(
4516
+ unchecked.map(([local]) => isClientOnly(imports.get(local), deps))
4517
+ );
4518
+ const offender = unchecked.find((_, i) => clientOnly[i]);
4519
+ if (offender) {
4520
+ const [local] = offender;
4521
+ throw revalidateError(
4522
+ file,
4523
+ code,
4524
+ value,
4525
+ `\`${local}\` is imported from "${imports.get(local).source}", a client-only module: on the server a "use client" module's exports are client references and cannot be called. Import the predicate's inputs from a module without "use client".`
4526
+ );
4527
+ }
4528
+ for (const [local, valueUse] of free.imports) {
4529
+ used.set(local, (used.get(local) ?? false) || valueUse);
4530
+ }
4531
+ const source = code.slice(value.start, value.end);
4532
+ const key = keyOf(relativeFile, value);
4533
+ entries.set(key, source);
4534
+ s.overwrite(
4535
+ arg.start,
4536
+ arg.end,
4537
+ `{ ${HOISTED_REVALIDATE_KEY}: ${JSON.stringify(key)} }`
4538
+ );
4539
+ }
4540
+ const importLines = await emitImports(used, imports, () => {
4541
+ });
4542
+ const module = [
4543
+ `import { registerClientUrlRevalidators as ${REGISTER} } from "@rangojs/router/server";`,
4544
+ ...importLines,
4545
+ `${REGISTER}({`,
4546
+ ...[...entries].map(
4547
+ ([key, source]) => ` ${JSON.stringify(key)}: ${source},`
4548
+ ),
4549
+ `});`,
4550
+ ""
4551
+ ].join("\n");
4552
+ return {
4553
+ code: s.toString(),
4554
+ map: s.generateMap({ hires: true, source: file }),
4555
+ module,
4556
+ keys: [...entries.keys()]
4557
+ };
4558
+ }
4559
+ async function isClientOnly(binding, deps) {
4560
+ return binding.source === "client-only" || Boolean(await deps.isClientModule?.(binding.source));
4561
+ }
4562
+ function parseHoistedRevalidateId(id) {
4563
+ const q = id.indexOf("?");
4564
+ if (q === -1) return null;
4565
+ const params = new URLSearchParams(id.slice(q + 1));
4566
+ return params.has(CLIENT_URLS_REVALIDATE_QUERY) ? id.slice(0, q) : null;
4567
+ }
4568
+ var HOISTED_ID_RE = new RegExp(
4569
+ `[?&]${CLIENT_URLS_REVALIDATE_QUERY}(?:[=&]|$)`
4570
+ );
4571
+ var SOURCE_ID_RE = /\.[cm]?[jt]sx?(?:\?|$)/;
4572
+ var NODE_MODULES_RE = /\/node_modules\//;
4573
+ function clientUrlsRevalidateHoistPlugin() {
4574
+ const emitted = /* @__PURE__ */ new Map();
4575
+ const clientModules = /* @__PURE__ */ new Map();
4576
+ let root = process.cwd();
4577
+ const isClientFile = (file) => {
4578
+ let known = clientModules.get(file);
4579
+ if (!known) {
4580
+ known = readSource(file).then(
4581
+ (text) => text !== null && text.includes("use client") && hasUseClientDirective(text)
4582
+ );
4583
+ clientModules.set(file, known);
4584
+ }
4585
+ return known;
4586
+ };
4587
+ return [
4588
+ {
4589
+ name: "@rangojs/router:client-urls-revalidate-hoist",
4590
+ enforce: "pre",
4591
+ configResolved(config) {
4592
+ root = config.root;
4593
+ },
4594
+ buildStart() {
4595
+ clientModules.clear();
4596
+ },
4597
+ watchChange(id) {
4598
+ const file = normalizePath4(id);
4599
+ clientModules.delete(file);
4600
+ emitted.delete(file);
4601
+ },
4602
+ load: {
4603
+ filter: { id: HOISTED_ID_RE },
4604
+ async handler(id) {
4605
+ const hoistedFile = parseHoistedRevalidateId(id);
4606
+ if (hoistedFile === null) return;
4607
+ const module = await loadEmittedOrDisk(
4608
+ emitted,
4609
+ hoistedFile,
4610
+ async (disk) => (await hoistClientUrlsRevalidates(disk, hoistedFile, { root }))?.module
4611
+ );
4612
+ return module ?? "export {};\n";
4613
+ }
4614
+ },
4615
+ transform: {
4616
+ filter: {
4617
+ id: {
4618
+ include: SOURCE_ID_RE,
4619
+ exclude: [NODE_MODULES_RE, HOISTED_ID_RE]
4620
+ },
4621
+ code: CLIENT_URLS_RE
4622
+ },
4623
+ async handler(code, id) {
4624
+ if (id.includes("/node_modules/") || parseHoistedRevalidateId(id)) {
4625
+ return;
4626
+ }
4627
+ const file = normalizePath4(id.split("?", 1)[0]);
4628
+ if (!SOURCE_FILE_RE.test(file)) return;
4629
+ const rsc = this.environment?.name === "rsc";
4630
+ if (rsc) emitted.delete(file);
4631
+ const result = await hoistClientUrlsRevalidates(code, id, {
4632
+ root,
4633
+ isClientModule: rsc ? async (source) => {
4634
+ const target = await resolveSourceTarget(this, source, id, {
4635
+ skipNodeModules: false
4636
+ });
4637
+ return target !== null && await isClientFile(target);
4638
+ } : void 0
4639
+ });
4640
+ if (!result) return;
4641
+ if (rsc) await recordEmitted(emitted, file, result.module);
4642
+ return { code: result.code, map: result.map };
4643
+ }
4644
+ }
4645
+ },
4646
+ {
4647
+ name: "@rangojs/router:client-urls-revalidate-register",
4648
+ enforce: "post",
4649
+ applyToEnvironment: (env) => env.name === "rsc",
4650
+ transform: {
4651
+ filter: { code: "registerClientReference" },
4652
+ handler(code, id) {
4653
+ if (parseHoistedRevalidateId(id)) return;
4654
+ const file = normalizePath4(id.split("?", 1)[0]);
4655
+ if (!emitted.has(file)) return;
4656
+ const s = new MagicString5(code);
4657
+ s.append(
4658
+ `
4659
+ import ${JSON.stringify(`${file}?${CLIENT_URLS_REVALIDATE_QUERY}`)};
4660
+ `
4661
+ );
4662
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
4663
+ }
4664
+ }
4665
+ }
4666
+ ];
4667
+ }
4668
+
4168
4669
  // src/vite/plugins/client-ref-dedup.ts
4169
4670
  init_debug();
4170
4671
  import { existsSync, readFileSync } from "node:fs";
4171
- import { isAbsolute, join, normalize, relative } from "node:path";
4672
+ import { isAbsolute as isAbsolute2, join as join2, normalize, relative as relative3 } from "node:path";
4172
4673
  var debug4 = createRangoDebugger(NS.transform);
4173
4674
  var CLIENT_IN_SERVER_PROXY_PREFIX = "virtual:vite-rsc/client-in-server-package-proxy/";
4174
4675
  var DEDUP_PREFIX = "\0rango:dedup/";
@@ -4201,7 +4702,7 @@ function readPackageMetadata(source, cache3) {
4201
4702
  if (!root) return;
4202
4703
  const cached = cache3.get(root);
4203
4704
  if (cached !== void 0) return cached ?? void 0;
4204
- const packageJsonPath = join(root, "package.json");
4705
+ const packageJsonPath = join2(root, "package.json");
4205
4706
  if (!existsSync(packageJsonPath)) {
4206
4707
  cache3.set(root, null);
4207
4708
  return;
@@ -4254,8 +4755,8 @@ function getPublicSpecifierCandidates(metadata, packageSpecifier, source) {
4254
4755
  return [];
4255
4756
  }
4256
4757
  const candidates = /* @__PURE__ */ new Set();
4257
- const sourceRelative = `./${relative(metadata.root, source).replaceAll("\\", "/")}`;
4258
- if (sourceRelative.startsWith("./../") || isAbsolute(sourceRelative.slice(2))) {
4758
+ const sourceRelative = `./${relative3(metadata.root, source).replaceAll("\\", "/")}`;
4759
+ if (sourceRelative.startsWith("./../") || isAbsolute2(sourceRelative.slice(2))) {
4259
4760
  return [];
4260
4761
  }
4261
4762
  for (const [exportKey, target] of Object.entries(metadata.exports)) {
@@ -4342,7 +4843,7 @@ function clientRefDedup() {
4342
4843
  configResolved(config) {
4343
4844
  const clientEnv = config.environments?.["client"];
4344
4845
  clientExclude = clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
4345
- rootImporter = join(config.root, "index.html");
4846
+ rootImporter = join2(config.root, "index.html");
4346
4847
  resolvePackage = config.createResolver({ scan: true });
4347
4848
  },
4348
4849
  buildEnd() {
@@ -4411,12 +4912,12 @@ function clientRefDedup() {
4411
4912
  }
4412
4913
 
4413
4914
  // src/vite/plugins/stable-rsc-output.ts
4414
- import { parseAst as parseAst4 } from "vite";
4915
+ import { parseAst as parseAst6 } from "vite";
4415
4916
 
4416
4917
  // src/vite/discovery/build-versions.ts
4417
- import { createHash as createHash3 } from "node:crypto";
4918
+ import { createHash as createHash4 } from "node:crypto";
4418
4919
  import { isBuiltin } from "node:module";
4419
- import { posix, relative as relative3 } from "node:path";
4920
+ import { posix, relative as relative5 } from "node:path";
4420
4921
 
4421
4922
  // src/router-versions.ts
4422
4923
  var DEFAULT_ROUTER_VERSIONS_KEY = "*";
@@ -4990,7 +5491,7 @@ function resolveRouterMembers(server, client, routers) {
4990
5491
  return members;
4991
5492
  }
4992
5493
  function sha256(value) {
4993
- return createHash3("sha256").update(value).digest("hex");
5494
+ return createHash4("sha256").update(value).digest("hex");
4994
5495
  }
4995
5496
  var baseName = posix.basename;
4996
5497
  function isAbsolutePath(path6) {
@@ -5011,7 +5512,7 @@ function portableModuleId(id, projectRoot) {
5011
5512
  return path6.slice(nodeModules + 1).replace(PNPM_PEER_SUFFIX, "$1");
5012
5513
  }
5013
5514
  if (isAbsolutePath(path6)) {
5014
- return relative3(projectRoot, path6).replaceAll("\\", "/");
5515
+ return relative5(projectRoot, path6).replaceAll("\\", "/");
5015
5516
  }
5016
5517
  return withoutWorkingDirectory(path6).replace(
5017
5518
  /([?&][^=&]+=)([^&]+)/g,
@@ -5372,7 +5873,7 @@ function literalProperties(object) {
5372
5873
  function defaultExportProperties(code) {
5373
5874
  let program;
5374
5875
  try {
5375
- program = parseAst4(code);
5876
+ program = parseAst6(code);
5376
5877
  } catch {
5377
5878
  return void 0;
5378
5879
  }
@@ -5428,7 +5929,7 @@ import { resolve } from "node:path";
5428
5929
  // package.json
5429
5930
  var package_default = {
5430
5931
  name: "@rangojs/router",
5431
- version: "0.22.1",
5932
+ version: "0.24.0",
5432
5933
  description: "Django-inspired RSC router with composable URL patterns",
5433
5934
  keywords: [
5434
5935
  "react",
@@ -5887,58 +6388,6 @@ declare global {
5887
6388
  `;
5888
6389
  }
5889
6390
 
5890
- // src/build/route-types/scan-filter.ts
5891
- import { join as join2, relative as relative4 } from "node:path";
5892
- import { readdirSync } from "node:fs";
5893
- import picomatch from "picomatch";
5894
- var DEFAULT_EXCLUDE_PATTERNS = [
5895
- "**/__tests__/**",
5896
- "**/__mocks__/**",
5897
- "**/dist/**",
5898
- "**/coverage/**",
5899
- "**/*.test.{ts,tsx,js,jsx}",
5900
- "**/*.spec.{ts,tsx,js,jsx}"
5901
- ];
5902
- function createScanFilter(root, opts) {
5903
- const { include, exclude } = opts;
5904
- const hasInclude = include && include.length > 0;
5905
- const hasCustomExclude = exclude !== void 0;
5906
- if (!hasInclude && !hasCustomExclude) return void 0;
5907
- const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
5908
- const includeMatcher = hasInclude ? picomatch(include) : null;
5909
- const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
5910
- return (absolutePath) => {
5911
- const rel = relative4(root, absolutePath);
5912
- if (excludeMatcher && excludeMatcher(rel)) return false;
5913
- if (includeMatcher) return includeMatcher(rel);
5914
- return true;
5915
- };
5916
- }
5917
- function findTsFiles(dir, filter) {
5918
- const results = [];
5919
- let entries;
5920
- try {
5921
- entries = readdirSync(dir, { withFileTypes: true });
5922
- } catch (err) {
5923
- console.warn(
5924
- `[rango] Failed to scan directory ${dir}: ${err.message}`
5925
- );
5926
- return results;
5927
- }
5928
- for (const entry of entries) {
5929
- const fullPath = join2(dir, entry.name);
5930
- if (entry.isDirectory()) {
5931
- if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist" || entry.name === "build" || entry.name === "coverage")
5932
- continue;
5933
- results.push(...findTsFiles(fullPath, filter));
5934
- } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
5935
- if (filter && !filter(fullPath)) continue;
5936
- results.push(fullPath);
5937
- }
5938
- }
5939
- return results;
5940
- }
5941
-
5942
6391
  // src/build/route-types/per-module-writer.ts
5943
6392
  import ts4 from "typescript";
5944
6393
 
@@ -6513,7 +6962,7 @@ function isRoutableSourceFile(name) {
6513
6962
  return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
6514
6963
  }
6515
6964
  function isExcludedScanDir(name) {
6516
- return name === "node_modules" || name === "dist" || name === "coverage" || name === "__tests__" || name === "__mocks__" || name.startsWith(".");
6965
+ return isUniversallySkippedScanDir(name) || name === "__tests__" || name === "__mocks__";
6517
6966
  }
6518
6967
  function findCallSiteFilesRecursive(dir, pattern, patternG, stopAtMatchDir, filter, results) {
6519
6968
  let entries;
@@ -6896,28 +7345,7 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
6896
7345
  }
6897
7346
 
6898
7347
  // src/vite/plugins/version-plugin.ts
6899
- import { parseAst as parseAst6 } from "vite";
6900
-
6901
- // src/vite/utils/directive-prologue.ts
6902
- import { parseAst as parseAst5 } from "vite";
6903
- function hasUseClientDirective2(source) {
6904
- let program;
6905
- try {
6906
- program = parseAst5(source, { lang: "tsx" });
6907
- } catch {
6908
- return false;
6909
- }
6910
- for (const node of program.body ?? []) {
6911
- if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
6912
- if (node.expression.value === "use client") return true;
6913
- continue;
6914
- }
6915
- break;
6916
- }
6917
- return false;
6918
- }
6919
-
6920
- // src/vite/plugins/version-plugin.ts
7348
+ import { parseAst as parseAst7 } from "vite";
6921
7349
  function isCodeModule(id) {
6922
7350
  return /\.(tsx?|jsx?)($|\?)/.test(id);
6923
7351
  }
@@ -6925,10 +7353,10 @@ function normalizeModuleId(id) {
6925
7353
  return id.split("?", 1)[0];
6926
7354
  }
6927
7355
  function getClientModuleSignature(source) {
6928
- if (!hasUseClientDirective2(source)) return void 0;
7356
+ if (!hasUseClientDirective(source)) return void 0;
6929
7357
  let program;
6930
7358
  try {
6931
- program = parseAst6(source, { lang: "tsx" });
7359
+ program = parseAst7(source, { lang: "tsx" });
6932
7360
  } catch {
6933
7361
  return void 0;
6934
7362
  }
@@ -7103,7 +7531,7 @@ function isViteDepCachePath(filePath, cacheDir) {
7103
7531
 
7104
7532
  // src/vite/utils/shared-utils.ts
7105
7533
  import * as Vite from "vite";
7106
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
7534
+ import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
7107
7535
 
7108
7536
  // src/vite/plugins/performance-tracks.ts
7109
7537
  init_debug();
@@ -7190,7 +7618,7 @@ var sharedRolldownOptions = {
7190
7618
  function normalizeHostRouterEntry(rawInput, root, exists) {
7191
7619
  const raw = rawInput.replaceAll("\\", "/");
7192
7620
  const isRelative = raw.startsWith("./") || raw.startsWith("../");
7193
- const isRooted = raw.startsWith("/") || isAbsolute2(rawInput);
7621
+ const isRooted = raw.startsWith("/") || isAbsolute3(rawInput);
7194
7622
  if (isRooted) return raw;
7195
7623
  if (isRelative) {
7196
7624
  if (!exists(resolve4(root, raw))) {
@@ -7680,7 +8108,7 @@ function createCjsToEsmPlugin() {
7680
8108
  }
7681
8109
 
7682
8110
  // src/vite/router-discovery.ts
7683
- import { createServer as createViteServer } from "vite";
8111
+ import { createServer as createViteServer, normalizePath as normalizePath8 } from "vite";
7684
8112
  import { dirname as dirname7, join as join10, resolve as resolve13 } from "node:path";
7685
8113
  import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
7686
8114
  import { createRequire as createRequire3, register } from "node:module";
@@ -8073,10 +8501,10 @@ function evictHandlerCode(code, exports, fnName, brand) {
8073
8501
  }
8074
8502
 
8075
8503
  // src/vite/discovery/self-gen-tracking.ts
8076
- import { createHash as createHash4 } from "node:crypto";
8504
+ import { createHash as createHash5 } from "node:crypto";
8077
8505
  import { readFileSync as readFileSync4 } from "node:fs";
8078
8506
  function markSelfGenWrite(state, filePath, content) {
8079
- const hash = createHash4("sha256").update(content).digest("hex");
8507
+ const hash = createHash5("sha256").update(content).digest("hex");
8080
8508
  state.selfWrittenGenFiles.set(filePath, { at: Date.now(), hash });
8081
8509
  }
8082
8510
  function consumeSelfGenWrite(state, filePath) {
@@ -8094,7 +8522,7 @@ function checkSelfGenWrite(state, filePath, consume) {
8094
8522
  }
8095
8523
  try {
8096
8524
  const current = readFileSync4(filePath, "utf-8");
8097
- const currentHash = createHash4("sha256").update(current).digest("hex");
8525
+ const currentHash = createHash5("sha256").update(current).digest("hex");
8098
8526
  if (currentHash === info.hash) {
8099
8527
  if (consume) state.selfWrittenGenFiles.delete(filePath);
8100
8528
  return true;
@@ -8262,7 +8690,7 @@ function insertSegments(node, segments, index, leafBase, paramNames) {
8262
8690
  }
8263
8691
  const segment = segments[index];
8264
8692
  if (segment.type === "static") {
8265
- if (!node.s) node.s = {};
8693
+ if (!node.s) node.s = /* @__PURE__ */ Object.create(null);
8266
8694
  if (!node.s[segment.value]) node.s[segment.value] = {};
8267
8695
  insertSegments(
8268
8696
  node.s[segment.value],
@@ -8562,6 +8990,13 @@ function jsonParseExpression(value) {
8562
8990
  }
8563
8991
 
8564
8992
  // src/context-var.ts
8993
+ function createVar(options) {
8994
+ return {
8995
+ __brand: "context-var",
8996
+ key: /* @__PURE__ */ Symbol(),
8997
+ cache: options?.cache !== false
8998
+ };
8999
+ }
8565
9000
  function hasContextVars(variables) {
8566
9001
  return Object.keys(variables).length > 0 || Object.getOwnPropertySymbols(variables).length > 0;
8567
9002
  }
@@ -8574,7 +9009,11 @@ function getNonCacheableKeys(variables) {
8574
9009
  }
8575
9010
  return variables[NON_CACHEABLE_KEYS];
8576
9011
  }
8577
- var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
9012
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
9013
+ "__proto__",
9014
+ "constructor",
9015
+ "prototype"
9016
+ ]);
8578
9017
  function contextSet(variables, keyOrVar, value, options) {
8579
9018
  if (typeof keyOrVar === "string") {
8580
9019
  if (FORBIDDEN_KEYS.has(keyOrVar)) {
@@ -8595,7 +9034,7 @@ function contextSet(variables, keyOrVar, value, options) {
8595
9034
  }
8596
9035
 
8597
9036
  // src/vite/utils/prerender-utils.ts
8598
- import { createHash as createHash5 } from "node:crypto";
9037
+ import { createHash as createHash6 } from "node:crypto";
8599
9038
  import {
8600
9039
  copyFileSync,
8601
9040
  existsSync as existsSync6,
@@ -8768,7 +9207,7 @@ function resetStagedBuildAssets(projectRoot) {
8768
9207
  }
8769
9208
  function writeBuildAssetModule(dir, prefix, exportValue) {
8770
9209
  mkdirSync(dir, { recursive: true });
8771
- const contentHash = createHash5("sha256").update(exportValue).digest("hex").slice(0, 8);
9210
+ const contentHash = createHash6("sha256").update(exportValue).digest("hex").slice(0, 8);
8772
9211
  const fileName = `${prefix}-${contentHash}.js`;
8773
9212
  const filePath = resolve7(dir, fileName);
8774
9213
  if (!existsSync6(filePath)) {
@@ -9303,8 +9742,8 @@ init_client_ref_hashing();
9303
9742
 
9304
9743
  // src/vite/discovery/client-urls-projection.ts
9305
9744
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
9306
- import { isAbsolute as isAbsolute3, relative as relative5, resolve as resolve8 } from "node:path";
9307
- import { normalizePath as normalizePath5, parseAst as parseAst7 } from "vite";
9745
+ import { isAbsolute as isAbsolute4, relative as relative6, resolve as resolve8 } from "node:path";
9746
+ import { normalizePath as normalizePath7, parseAst as parseAst8 } from "vite";
9308
9747
 
9309
9748
  // src/client-urls/server-projection.ts
9310
9749
  import * as React5 from "react";
@@ -9614,7 +10053,7 @@ import { createContext as createContext4 } from "react";
9614
10053
  var OutletContext = createContext4(null);
9615
10054
 
9616
10055
  // src/route-content-wrapper.tsx
9617
- import { Component, Suspense, use, useMemo as useMemo3 } from "react";
10056
+ import { Component, Suspense, use as use2, useMemo as useMemo3 } from "react";
9618
10057
 
9619
10058
  // src/outlet-provider.tsx
9620
10059
  import {
@@ -9662,10 +10101,27 @@ function getAppVersion() {
9662
10101
  return currentVersion2;
9663
10102
  }
9664
10103
 
10104
+ // src/router/basename.ts
10105
+ function prefixBasename(path6, basename2) {
10106
+ if (!basename2 || !path6.startsWith("/") || path6 === basename2 || path6.startsWith(basename2 + "/") || path6.startsWith(basename2 + "?") || path6.startsWith(basename2 + "#")) {
10107
+ return path6;
10108
+ }
10109
+ return path6 === "/" ? basename2 : basename2 + path6;
10110
+ }
10111
+
9665
10112
  // src/loader-redirect.tsx
9666
10113
  init_validate_redirect_origin();
9667
10114
  init_redirect_origin();
9668
10115
 
10116
+ // src/browser/defer.ts
10117
+ import { use } from "react";
10118
+
10119
+ // src/thenable-status.ts
10120
+ var SETTLED_THENABLE = Object.assign(
10121
+ Promise.resolve(void 0),
10122
+ { status: "fulfilled", value: void 0, _debugInfo: [] }
10123
+ );
10124
+
9669
10125
  // src/route-content-wrapper.tsx
9670
10126
  import { jsx as jsx4 } from "react/jsx-runtime";
9671
10127
  import { createElement as createElement3 } from "react";
@@ -9674,7 +10130,7 @@ import { createElement as createElement3 } from "react";
9674
10130
  import {
9675
10131
  isValidElement,
9676
10132
  startTransition as startTransition2,
9677
- use as use2,
10133
+ use as use3,
9678
10134
  useCallback,
9679
10135
  useContext as useContext7,
9680
10136
  useEffect as useEffect5,
@@ -9684,12 +10140,6 @@ import {
9684
10140
  useState as useState3
9685
10141
  } from "react";
9686
10142
 
9687
- // src/thenable-status.ts
9688
- var SETTLED_THENABLE = Object.assign(
9689
- Promise.resolve(void 0),
9690
- { status: "fulfilled", value: void 0, _debugInfo: [] }
9691
- );
9692
-
9693
10143
  // src/loader-store.ts
9694
10144
  var EMPTY_SNAPSHOT = Object.freeze({
9695
10145
  value: void 0,
@@ -9751,7 +10201,7 @@ import {
9751
10201
  useCallback as useCallback4,
9752
10202
  useMemo as useMemo7,
9753
10203
  useRef as useRef11,
9754
- use as use3,
10204
+ use as use4,
9755
10205
  startTransition as startTransition5
9756
10206
  } from "react";
9757
10207
 
@@ -10068,13 +10518,10 @@ var Link = forwardRef(function Link2({
10068
10518
  }, ref) {
10069
10519
  const ctx = useContext15(NavigationStoreContext);
10070
10520
  const isExternal = isExternalUrl(to, ctx?.origin);
10071
- const resolvedTo = useMemo8(() => {
10072
- if (isExternal) return to;
10073
- const bn = ctx?.basename;
10074
- if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
10075
- return to;
10076
- return to === "/" ? bn : bn + to;
10077
- }, [to, isExternal, ctx?.basename]);
10521
+ const resolvedTo = useMemo8(
10522
+ () => isExternal ? to : prefixBasename(to, ctx?.basename),
10523
+ [to, isExternal, ctx?.basename]
10524
+ );
10078
10525
  const configuredStrategy = prefetch ?? ctx?.defaultPrefetch ?? ENVIRONMENT_DEFAULT_PREFETCH;
10079
10526
  const internalRef = useRef13(null);
10080
10527
  const setRef = useCallback5(
@@ -10331,6 +10778,19 @@ var ALL_USE_ITEM_TYPES = new Set(
10331
10778
 
10332
10779
  // src/server/request-context.ts
10333
10780
  import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
10781
+
10782
+ // src/server/slot-accessors.ts
10783
+ function slotAccessor(slot, enumerable) {
10784
+ return {
10785
+ get() {
10786
+ return this[slot]();
10787
+ },
10788
+ enumerable,
10789
+ configurable: true
10790
+ };
10791
+ }
10792
+
10793
+ // src/server/request-context.ts
10334
10794
  init_cookie_name();
10335
10795
 
10336
10796
  // src/router/segment-resolution/mask-nested.ts
@@ -10395,12 +10855,41 @@ var THEME_READ = {
10395
10855
  }
10396
10856
  };
10397
10857
 
10858
+ // src/router/segment-resolution/prefetch-deferral.ts
10859
+ init_prefetch_markers();
10860
+
10861
+ // src/cache/document-cache-policy.ts
10862
+ init_cookie_name();
10863
+
10864
+ // src/server/rewrite-trail.ts
10865
+ var REWRITE_TRAIL = createVar();
10866
+
10398
10867
  // src/cache/invalidation-order.ts
10399
10868
  var order = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:tag-invalidation-order")] ??= { seq: 0, tags: /* @__PURE__ */ new Map(), forgotten: 0 };
10400
10869
  var responseStarts = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:response-execution-start")] ??= /* @__PURE__ */ new WeakMap();
10401
10870
 
10402
10871
  // src/server/request-context.ts
10403
10872
  var requestContextStorage = new AsyncLocalStorage5();
10873
+ var RES_SLOT = /* @__PURE__ */ Symbol("rango.requestContext.res");
10874
+ var RENDER_BARRIER_SLOT = /* @__PURE__ */ Symbol(
10875
+ "rango.requestContext.renderBarrier"
10876
+ );
10877
+ var RES_DESCRIPTOR = {
10878
+ ...slotAccessor(RES_SLOT, true),
10879
+ set(_) {
10880
+ throw new Error(
10881
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations."
10882
+ );
10883
+ }
10884
+ };
10885
+ var RENDER_BARRIER_DESCRIPTOR = slotAccessor(
10886
+ RENDER_BARRIER_SLOT,
10887
+ true
10888
+ );
10889
+ var DERIVED_RENDER_BARRIER_DESCRIPTOR = {
10890
+ ...RENDER_BARRIER_DESCRIPTOR,
10891
+ enumerable: false
10892
+ };
10404
10893
 
10405
10894
  // src/route-definition/redirect.ts
10406
10895
  init_redirect_origin();
@@ -10542,7 +11031,10 @@ function serializeTransition(route2) {
10542
11031
  }
10543
11032
  return Object.freeze(config);
10544
11033
  }
10545
- function serializeRoute(route2) {
11034
+ function indicesWhere(items, pred) {
11035
+ return items.flatMap((item, index) => pred(item) ? [index] : []);
11036
+ }
11037
+ function serializeRoute(route2, options) {
10546
11038
  const loaderIds = route2.loaders.map(({ loader: loader2 }, index) => {
10547
11039
  if (typeof loader2.$$id !== "string" || loader2.$$id.length === 0) {
10548
11040
  throw projectionError(
@@ -10552,7 +11044,25 @@ function serializeRoute(route2) {
10552
11044
  }
10553
11045
  return loader2.$$id;
10554
11046
  });
10555
- const awaitedLoaderIndices = route2.loaders.map(({ ssr }, index) => ssr === false ? index : -1).filter((index) => index >= 0);
11047
+ const loaderRevalidate = route2.loaders.map(
11048
+ ({ loader: loader2, revalidate: revalidate2 }) => revalidate2.flatMap((entry) => {
11049
+ if (isHoistedRevalidateRef(entry)) return [entry.$$rangoRevalidate];
11050
+ if (options.hoisted) {
11051
+ throw new Error(
11052
+ `clientUrls() revalidate() on loader "${loader2.$$id}" of route "${route2.pattern}" was not hoisted to the server: call the builder's revalidate() helper with an inline function or an imported predicate.`
11053
+ );
11054
+ }
11055
+ return [];
11056
+ })
11057
+ );
11058
+ const awaitedLoaderIndices = indicesWhere(
11059
+ route2.loaders,
11060
+ ({ ssr }) => ssr === false
11061
+ );
11062
+ const deferrableLoaderIndices = indicesWhere(
11063
+ route2.loaders,
11064
+ ({ prefetch }) => prefetch === false
11065
+ );
10556
11066
  const transition2 = serializeTransition(route2);
10557
11067
  return Object.freeze({
10558
11068
  id: route2.id,
@@ -10562,6 +11072,12 @@ function serializeRoute(route2) {
10562
11072
  loaderIds: Object.freeze(loaderIds),
10563
11073
  hasLoading: route2.loading !== void 0,
10564
11074
  ...awaitedLoaderIndices.length > 0 ? { awaitedLoaderIndices: Object.freeze(awaitedLoaderIndices) } : {},
11075
+ ...deferrableLoaderIndices.length > 0 ? { deferrableLoaderIndices: Object.freeze(deferrableLoaderIndices) } : {},
11076
+ ...loaderRevalidate.some((keys) => keys.length > 0) ? {
11077
+ loaderRevalidate: Object.freeze(
11078
+ loaderRevalidate.map((keys) => Object.freeze(keys))
11079
+ )
11080
+ } : {},
10565
11081
  ...transition2 ? { transition: transition2 } : {},
10566
11082
  ...typeof route2.transition?.when === "function" ? { transitionWhen: true } : {}
10567
11083
  });
@@ -10582,15 +11098,24 @@ function serializeIntercept(record, index) {
10582
11098
  hasLoading: record.loading !== void 0
10583
11099
  });
10584
11100
  }
10585
- function serializeClientUrlPatterns(patterns) {
11101
+ function serializeClientUrlPatterns(patterns, options = {}) {
10586
11102
  return Object.freeze({
10587
11103
  version: 1,
10588
- routes: Object.freeze(patterns.routes.map(serializeRoute)),
11104
+ routes: Object.freeze(
11105
+ patterns.routes.map((route2) => serializeRoute(route2, options))
11106
+ ),
10589
11107
  intercepts: Object.freeze(
10590
11108
  (patterns.intercepts ?? []).map(serializeIntercept)
10591
11109
  )
10592
11110
  });
10593
11111
  }
11112
+ function projectClientUrlsModule(namespace, options = {}) {
11113
+ const definition = namespace.default;
11114
+ if (typeof definition !== "object" && typeof definition !== "function" || definition === null || definition.__brand !== "client-urls") {
11115
+ return void 0;
11116
+ }
11117
+ return serializeClientUrlPatterns(definition, options);
11118
+ }
10594
11119
 
10595
11120
  // src/vite/discovery/client-urls-projection.ts
10596
11121
  init_client_ref_hashing();
@@ -10599,7 +11124,7 @@ var debug10 = createRangoDebugger(NS.discovery);
10599
11124
  function hasDefaultExport(code) {
10600
11125
  let program;
10601
11126
  try {
10602
- program = parseAst7(code, { lang: "tsx" });
11127
+ program = parseAst8(code, { lang: "tsx" });
10603
11128
  } catch {
10604
11129
  return false;
10605
11130
  }
@@ -10615,20 +11140,20 @@ function hasDefaultExport(code) {
10615
11140
  }
10616
11141
  function normalizeSource(state, id) {
10617
11142
  const cleanId = id.split("?", 1)[0];
10618
- const absoluteId = isAbsolute3(cleanId) ? cleanId : resolve8(state.projectRoot, cleanId);
10619
- return normalizePath5(absoluteId);
11143
+ const absoluteId = isAbsolute4(cleanId) ? cleanId : resolve8(state.projectRoot, cleanId);
11144
+ return normalizePath7(absoluteId);
10620
11145
  }
10621
11146
  function referenceId(referenceKey) {
10622
11147
  return `${referenceKey}#default`;
10623
11148
  }
10624
11149
  function recordClientUrlsModule(state, code, id) {
10625
- if (!code.includes("clientUrls(") || !hasUseClientDirective2(code) || !hasDefaultExport(code)) {
11150
+ if (!code.includes("clientUrls(") || !hasUseClientDirective(code) || !hasDefaultExport(code)) {
10626
11151
  return;
10627
11152
  }
10628
11153
  const source = normalizeSource(state, id);
10629
- const projectRoot = normalizePath5(resolve8(state.projectRoot));
10630
- const relativeId = normalizePath5(relative5(projectRoot, source));
10631
- const outsideProject = relativeId === ".." || relativeId.startsWith("../") || isAbsolute3(relativeId);
11154
+ const projectRoot = normalizePath7(resolve8(state.projectRoot));
11155
+ const relativeId = normalizePath7(relative6(projectRoot, source));
11156
+ const outsideProject = relativeId === ".." || relativeId.startsWith("../") || isAbsolute4(relativeId);
10632
11157
  const devReferenceKey = outsideProject ? `/@fs${source}` : `/${relativeId}`;
10633
11158
  const sourceByReferenceId = state.clientUrlSourceByReferenceId ??= /* @__PURE__ */ new Map();
10634
11159
  sourceByReferenceId.set(referenceId(devReferenceKey), source);
@@ -10672,18 +11197,19 @@ async function refreshRecordedClientUrlProjections(state, ssrEnv, serverMod) {
10672
11197
  }
10673
11198
  }
10674
11199
  for (const [source, referenceIds] of referenceIdsBySource) {
10675
- let definition;
11200
+ let module;
10676
11201
  try {
10677
- definition = (await ssrEnv.runner.import(source)).default;
11202
+ module = await ssrEnv.runner.import(source);
10678
11203
  } catch {
10679
11204
  continue;
10680
11205
  }
10681
- if (typeof definition !== "object" && typeof definition !== "function" || definition === null || definition.__brand !== "client-urls") {
11206
+ let projection;
11207
+ try {
11208
+ projection = projectClientUrlsModule(module, { hoisted: true });
11209
+ } catch {
10682
11210
  continue;
10683
11211
  }
10684
- const projection = serializeClientUrlPatterns(
10685
- definition
10686
- );
11212
+ if (!projection) continue;
10687
11213
  debug10?.(
10688
11214
  "pre-entry refresh %s -> [%s]",
10689
11215
  source,
@@ -10738,17 +11264,24 @@ async function discoverClientUrlProjections(state, ssrEnv, serverMod) {
10738
11264
  cause
10739
11265
  );
10740
11266
  }
10741
- const definition = module.default;
10742
- if (typeof definition !== "object" && typeof definition !== "function" || definition === null || definition.__brand !== "client-urls") {
11267
+ let projection;
11268
+ try {
11269
+ projection = projectClientUrlsModule(module, { hoisted: true });
11270
+ } catch (cause) {
11271
+ throw projectionError2(
11272
+ referenceIds[0],
11273
+ source,
11274
+ cause instanceof Error ? cause.message : String(cause),
11275
+ cause
11276
+ );
11277
+ }
11278
+ if (!projection) {
10743
11279
  throw projectionError2(
10744
11280
  referenceIds[0],
10745
11281
  source,
10746
11282
  'the module default export must be created by clientUrls() and have __brand === "client-urls"'
10747
11283
  );
10748
11284
  }
10749
- const projection = serializeClientUrlPatterns(
10750
- definition
10751
- );
10752
11285
  for (const referenceId2 of referenceIds) {
10753
11286
  nextProjections.set(referenceId2, projection);
10754
11287
  }
@@ -10767,13 +11300,51 @@ async function discoverClientUrlProjections(state, ssrEnv, serverMod) {
10767
11300
 
10768
11301
  // src/vite/discovery/discover-routers.ts
10769
11302
  var debug11 = createRangoDebugger(NS.discovery);
11303
+ var SERVER_MODULE_ID = "@rangojs/router/server";
11304
+ var MAX_DISCOVERY_PASSES = 3;
11305
+ var RUNNER_RELOADED = /* @__PURE__ */ Symbol("rango.discovery.runnerReloaded");
11306
+ async function runnerReloadedSince(rscEnv, serverMod) {
11307
+ try {
11308
+ return await rscEnv.runner.import(SERVER_MODULE_ID) !== serverMod;
11309
+ } catch {
11310
+ return false;
11311
+ }
11312
+ }
10770
11313
  async function discoverRouters(state, rscEnv, ssrEnv) {
10771
- if (!state.resolvedEntryPath) return;
10772
- const serverMod = await timed(
10773
- debug11,
10774
- "inner: import @rangojs/router/server",
10775
- () => rscEnv.runner.import("@rangojs/router/server")
10776
- );
11314
+ const entryPath = state.resolvedEntryPath;
11315
+ if (!entryPath) return;
11316
+ for (let pass = 1; ; pass++) {
11317
+ const serverMod = await timed(
11318
+ debug11,
11319
+ "inner: import @rangojs/router/server",
11320
+ () => rscEnv.runner.import(SERVER_MODULE_ID)
11321
+ );
11322
+ let result;
11323
+ try {
11324
+ result = await discoverRoutersPass(
11325
+ state,
11326
+ rscEnv,
11327
+ ssrEnv,
11328
+ entryPath,
11329
+ serverMod
11330
+ );
11331
+ } catch (error) {
11332
+ if (!await runnerReloadedSince(rscEnv, serverMod)) throw error;
11333
+ result = RUNNER_RELOADED;
11334
+ }
11335
+ if (result !== RUNNER_RELOADED) return result;
11336
+ if (pass === MAX_DISCOVERY_PASSES) {
11337
+ throw new Error(
11338
+ `[rango] Route discovery did not settle: the RSC module runner reloaded during ${MAX_DISCOVERY_PASSES} consecutive discovery passes.`
11339
+ );
11340
+ }
11341
+ debug11?.(
11342
+ "inner: module runner reloaded mid-pass, redoing discovery (pass %d)",
11343
+ pass + 1
11344
+ );
11345
+ }
11346
+ }
11347
+ async function discoverRoutersPass(state, rscEnv, ssrEnv, entryPath, serverMod) {
10777
11348
  await timed(
10778
11349
  debug11,
10779
11350
  "inner: refresh recorded client URL projections",
@@ -10782,9 +11353,10 @@ async function discoverRouters(state, rscEnv, ssrEnv) {
10782
11353
  await timed(
10783
11354
  debug11,
10784
11355
  "inner: import entry",
10785
- () => rscEnv.runner.import(state.resolvedEntryPath)
11356
+ () => rscEnv.runner.import(entryPath)
10786
11357
  );
10787
- let registry = serverMod.RouterRegistry;
11358
+ if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
11359
+ const registry = serverMod.RouterRegistry;
10788
11360
  if (!registry || registry.size === 0) {
10789
11361
  const discoveryErrors = [];
10790
11362
  try {
@@ -10798,20 +11370,15 @@ async function discoverRouters(state, rscEnv, ssrEnv) {
10798
11370
  for (const { context, error } of handlerErrors) {
10799
11371
  debug11?.("caught error while resolving %s: %O", context, error);
10800
11372
  }
10801
- const freshServerMod = await rscEnv.runner.import(
10802
- "@rangojs/router/server"
10803
- );
10804
- const freshRegistry = freshServerMod.RouterRegistry;
10805
- if (freshRegistry && freshRegistry.size > 0) {
10806
- Object.assign(serverMod, freshServerMod);
10807
- registry = freshRegistry;
11373
+ if (await runnerReloadedSince(rscEnv, serverMod)) {
11374
+ return RUNNER_RELOADED;
10808
11375
  }
10809
11376
  }
10810
11377
  } catch (error) {
10811
11378
  discoveryErrors.push({ context: "host-router discovery", error });
10812
11379
  }
10813
11380
  if (!registry || registry.size === 0) {
10814
- throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
11381
+ throw new DiscoveryError(entryPath, discoveryErrors);
10815
11382
  }
10816
11383
  }
10817
11384
  await timed(
@@ -10960,6 +11527,7 @@ async function discoverRouters(state, rscEnv, ssrEnv) {
10960
11527
  }
10961
11528
  }
10962
11529
  }
11530
+ if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
10963
11531
  state.mergedRouteManifest = newMergedRouteManifest;
10964
11532
  state.perRouterManifests = newPerRouterManifests;
10965
11533
  state.perRouterManifestDataMap = newPerRouterManifestDataMap;
@@ -10997,7 +11565,7 @@ import { dirname as dirname4, join as join6, resolve as resolve9 } from "node:pa
10997
11565
  init_debug();
10998
11566
 
10999
11567
  // src/vite/discovery/installed-packages.ts
11000
- import { createHash as createHash6 } from "node:crypto";
11568
+ import { createHash as createHash7 } from "node:crypto";
11001
11569
  import {
11002
11570
  existsSync as existsSync8,
11003
11571
  readdirSync as readdirSync3,
@@ -11014,7 +11582,7 @@ function linkTarget(path6) {
11014
11582
  }
11015
11583
  }
11016
11584
  function digestDirectory(dir) {
11017
- const hash = createHash6("sha256");
11585
+ const hash = createHash7("sha256");
11018
11586
  const entered = /* @__PURE__ */ new Set();
11019
11587
  const visit = (current, prefix) => {
11020
11588
  const real = realpathSync(current);
@@ -11727,6 +12295,17 @@ function devDiscoveryBootstrap(state) {
11727
12295
  const epoch = state.devDiscoveryEpoch;
11728
12296
  return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
11729
12297
  }
12298
+ function clientUrlProjectionReplay(state) {
12299
+ const projections = [...state.clientUrlProjectionMap ?? []].sort(
12300
+ ([a], [b]) => compareStrings(a, b)
12301
+ );
12302
+ return [
12303
+ `clearClientUrlProjections();`,
12304
+ ...projections.map(
12305
+ ([referenceId2, projection]) => `setClientUrlProjection(${JSON.stringify(referenceId2)}, ${jsonParseExpression(projection)});`
12306
+ )
12307
+ ];
12308
+ }
11730
12309
  function generateRoutesManifestModule(state) {
11731
12310
  const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
11732
12311
  const hasClientUrlModules = Boolean(state.clientUrlSourceByReferenceId?.size);
@@ -11773,17 +12352,7 @@ function generateRoutesManifestModule(state) {
11773
12352
  `clearAllRouterData();`,
11774
12353
  TRANSITION_WHEN_VALIDATION
11775
12354
  ];
11776
- if (hasClientUrlModules) {
11777
- lines2.push(`clearClientUrlProjections();`);
11778
- const projections = [...state.clientUrlProjectionMap ?? []].sort(
11779
- ([a], [b]) => compareStrings(a, b)
11780
- );
11781
- for (const [referenceId2, projection] of projections) {
11782
- lines2.push(
11783
- `setClientUrlProjection(${JSON.stringify(referenceId2)}, ${jsonParseExpression(projection)});`
11784
- );
11785
- }
11786
- }
12355
+ if (hasClientUrlModules) lines2.push(...clientUrlProjectionReplay(state));
11787
12356
  if (varIdx > 0) {
11788
12357
  lines2.push(
11789
12358
  `function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`
@@ -11825,12 +12394,10 @@ function generateRoutesManifestModule(state) {
11825
12394
  return lines2.join("\n");
11826
12395
  }
11827
12396
  const lines = [
11828
- `import { ${hasClientUrlModules ? "clearClientUrlProjections, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
12397
+ `import { ${hasClientUrlModules ? "clearClientUrlProjections, setClientUrlProjection, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
11829
12398
  TRANSITION_WHEN_VALIDATION
11830
12399
  ];
11831
- if (hasClientUrlModules) {
11832
- lines.push(`clearClientUrlProjections();`);
11833
- }
12400
+ if (hasClientUrlModules) lines.push(...clientUrlProjectionReplay(state));
11834
12401
  if (!state.isBuildMode) {
11835
12402
  const origin = state.devServerOrigin || state.devServer?.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || state.devServer && `http://localhost:${state.devServer.config.server.port || 5173}`;
11836
12403
  if (origin) {
@@ -12169,6 +12736,12 @@ var debugDiscovery = createRangoDebugger(NS.discovery);
12169
12736
  var debugRoutes = createRangoDebugger(NS.routes);
12170
12737
  var debugBuild = createRangoDebugger(NS.build);
12171
12738
  var debugDev = createRangoDebugger(NS.dev);
12739
+ function modulesForFile(graph, file) {
12740
+ return graph.getModulesByFile(normalizePath8(file));
12741
+ }
12742
+ function isFileOutsideApp(root, file, rscGraph) {
12743
+ return isInSkippedScanDir(root, file) && !(rscGraph && modulesForFile(rscGraph, file)?.size);
12744
+ }
12172
12745
  var loaderHookRegistered = false;
12173
12746
  function ensureCloudflareProtocolLoaderRegistered() {
12174
12747
  if (loaderHookRegistered) return;
@@ -12261,6 +12834,10 @@ async function createTempRscServer(state, options = {}) {
12261
12834
  // Inline transition({ when }) literals become client references here
12262
12835
  // too, or discovery would see (and reject) the plain function.
12263
12836
  transitionWhenHoistPlugin(),
12837
+ // clientUrls() revalidate() predicates move to the server here too:
12838
+ // discovery serializes the SSR copy's markers and the RSC realm
12839
+ // registers the predicates the materialized stubs call.
12840
+ ...clientUrlsRevalidateHoistPlugin(),
12264
12841
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
12265
12842
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
12266
12843
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : void 0),
@@ -12697,12 +13274,18 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
12697
13274
  "acquireBuildEnv",
12698
13275
  () => acquireBuildEnv(s, viteCommand, viteMode)
12699
13276
  );
12700
- tempRscEnv = (await timed(
13277
+ const temp = await timed(
12701
13278
  debugDiscovery,
12702
13279
  "getOrCreateTempServer",
12703
13280
  () => getOrCreateTempServer()
12704
- )).env;
12705
- if (tempRscEnv) {
13281
+ );
13282
+ tempRscEnv = temp.env;
13283
+ if (!tempRscEnv) {
13284
+ recordDiscoveryFailure(
13285
+ s,
13286
+ temp.error ?? "temp runner unavailable for discovery"
13287
+ );
13288
+ } else {
12706
13289
  optimizerHashBefore2 = tempRscEnv.depsOptimizer?.metadata?.browserHash;
12707
13290
  await timed(
12708
13291
  debugDiscovery,
@@ -12720,6 +13303,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
12720
13303
  );
12721
13304
  }
12722
13305
  } catch (err) {
13306
+ recordDiscoveryFailure(s, err);
12723
13307
  emitDiscoveryFailure(
12724
13308
  err,
12725
13309
  optimizerHashBefore2,
@@ -12761,6 +13345,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
12761
13345
  () => propagateDiscoveryState(rscEnv)
12762
13346
  );
12763
13347
  } catch (err) {
13348
+ recordDiscoveryFailure(s, err);
12764
13349
  emitDiscoveryFailure(
12765
13350
  err,
12766
13351
  optimizerHashBefore,
@@ -13117,6 +13702,11 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13117
13702
  });
13118
13703
  if (opts?.staticRouteTypesGeneration !== false) {
13119
13704
  const isGeneratedRouteFile = (filePath) => filePath.endsWith(".gen.ts") && (filePath.includes("named-routes.gen.ts") || filePath.includes("urls.gen.ts"));
13705
+ const isOutsideApp = (filePath) => isFileOutsideApp(
13706
+ s.projectRoot,
13707
+ filePath,
13708
+ server.environments?.rsc?.moduleGraph
13709
+ );
13120
13710
  const regenerateGeneratedRouteFiles = () => {
13121
13711
  if (s.perRouterManifests.length > 0) {
13122
13712
  writeRouteTypesFiles(s);
@@ -13148,15 +13738,24 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13148
13738
  return true;
13149
13739
  };
13150
13740
  let routeChangeTimer;
13741
+ const invalidateRoutesManifestModule = (rscEnv) => {
13742
+ for (const virtualId of [
13743
+ VIRTUAL_ROUTES_MANIFEST_ID,
13744
+ `\0${VIRTUAL_ROUTES_MANIFEST_ID}`
13745
+ ]) {
13746
+ const virtualMod = rscEnv.moduleGraph?.getModuleById?.(virtualId);
13747
+ if (virtualMod) rscEnv.moduleGraph.invalidateModule(virtualMod);
13748
+ }
13749
+ };
13151
13750
  const refreshRuntimeDiscovery = async () => {
13152
13751
  const rscEnv = server.environments?.rsc;
13153
13752
  const hasMainRunner = !!rscEnv?.runner;
13154
- if (!hasMainRunner && s.perRouterManifests.length === 0) return;
13155
13753
  let previousRouteShape = routeShapeSignature();
13156
13754
  await gate.runRefreshCycle(async () => {
13157
13755
  const hmrStart = performance.now();
13158
13756
  try {
13159
13757
  if (hasMainRunner) {
13758
+ const recoveringColdStart = s.perRouterManifests.length === 0;
13160
13759
  await timed(
13161
13760
  debugDiscovery,
13162
13761
  "hmr discoverRouters",
@@ -13172,6 +13771,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13172
13771
  "hmr propagateDiscoveryState",
13173
13772
  () => propagateDiscoveryState(rscEnv)
13174
13773
  );
13774
+ if (recoveringColdStart) invalidateRoutesManifestModule(rscEnv);
13175
13775
  } else {
13176
13776
  const tempRscEnv = await timed(
13177
13777
  debugDiscovery,
@@ -13220,10 +13820,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13220
13820
  forceCloudflareWorkerReload(rscEnv, expectedEpoch);
13221
13821
  }
13222
13822
  } catch (err) {
13223
- s.lastDiscoveryError = {
13224
- message: err?.message ?? String(err),
13225
- at: Date.now()
13226
- };
13823
+ recordDiscoveryFailure(s, err);
13227
13824
  const whenError2 = showTransitionWhenError(err);
13228
13825
  if (whenError2) {
13229
13826
  console.error(whenError2.message);
@@ -13317,15 +13914,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13317
13914
  server.environments?.ssr,
13318
13915
  serverMod
13319
13916
  );
13320
- for (const virtualId of [
13321
- VIRTUAL_ROUTES_MANIFEST_ID,
13322
- `\0${VIRTUAL_ROUTES_MANIFEST_ID}`
13323
- ]) {
13324
- const virtualMod = rscEnv.moduleGraph?.getModuleById?.(virtualId);
13325
- if (virtualMod) {
13326
- rscEnv.moduleGraph.invalidateModule(virtualMod);
13327
- }
13328
- }
13917
+ invalidateRoutesManifestModule(rscEnv);
13329
13918
  } catch (err) {
13330
13919
  debugDiscovery?.(
13331
13920
  "watcher: clientUrls projection pre-refresh failed: %s",
@@ -13351,7 +13940,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13351
13940
  "watcher: regenerated gen files (%sms)",
13352
13941
  (performance.now() - regenStart).toFixed(1)
13353
13942
  );
13354
- if (s.perRouterManifests.length > 0) {
13943
+ if (canRefreshRuntimeDiscovery(s)) {
13355
13944
  refreshRuntimeDiscovery().catch((err) => {
13356
13945
  console.warn(
13357
13946
  `[rango] Runtime re-discovery error: ${err.message}`
@@ -13362,6 +13951,13 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13362
13951
  }, 100);
13363
13952
  };
13364
13953
  const handleRouteFileChange = (filePath) => {
13954
+ if (isOutsideApp(filePath)) {
13955
+ debugDiscovery?.(
13956
+ "watcher: skip %s (outside the discovery scan)",
13957
+ filePath
13958
+ );
13959
+ return;
13960
+ }
13365
13961
  if (maybeHandleGeneratedRouteFileMutation(filePath)) return;
13366
13962
  if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx") && !filePath.endsWith(".js") && !filePath.endsWith(".jsx")) {
13367
13963
  if (s.lastDiscoveryError) {
@@ -13436,9 +14032,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13436
14032
  }
13437
14033
  const seen = /* @__PURE__ */ new Set();
13438
14034
  for (const file of routerSourceFiles) {
13439
- const mods = rscGraph.getModulesByFile(
13440
- file.replaceAll("\\", "/")
13441
- );
14035
+ const mods = modulesForFile(rscGraph, file);
13442
14036
  if (!mods) {
13443
14037
  debugDiscovery?.(
13444
14038
  "watcher: clientUrls invalidation found no rsc modules for %s",
@@ -13470,7 +14064,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13470
14064
  }
13471
14065
  s.cachedRouterFiles = void 0;
13472
14066
  }
13473
- if (s.perRouterManifests.length > 0) {
14067
+ if (canRefreshRuntimeDiscovery(s)) {
13474
14068
  gate.noteRouteEvent();
13475
14069
  }
13476
14070
  scheduleRouteRegeneration();
@@ -13488,7 +14082,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13488
14082
  server.watcher.on("add", handleRouteFileChange);
13489
14083
  server.watcher.on("change", handleRouteFileChange);
13490
14084
  server.watcher.on("unlink", (filePath) => {
13491
- if (!isGeneratedRouteFile(filePath)) return;
14085
+ if (!isGeneratedRouteFile(filePath) || isOutsideApp(filePath)) return;
13492
14086
  const hasRunner = !!server.environments?.rsc?.runner;
13493
14087
  if (!hasRunner) return;
13494
14088
  regenerateGeneratedRouteFiles();
@@ -14166,7 +14760,7 @@ If this is a multi-app host router, export a createHostRouter() instance and set
14166
14760
  return;
14167
14761
  try {
14168
14762
  const source = readFileSync12(file, "utf-8");
14169
- if (hasUseClientDirective2(source)) {
14763
+ if (hasUseClientDirective(source)) {
14170
14764
  return [];
14171
14765
  }
14172
14766
  } catch {
@@ -14175,6 +14769,7 @@ If this is a multi-app host router, export a createHostRouter() instance and set
14175
14769
  });
14176
14770
  plugins.push(createLoaderDirectiveGuardPlugin());
14177
14771
  plugins.push(transitionWhenHoistPlugin());
14772
+ plugins.push(...clientUrlsRevalidateHoistPlugin());
14178
14773
  plugins.push(createLoaderScanStubPlugin());
14179
14774
  plugins.push(exposeActionId());
14180
14775
  plugins.push(