@rangojs/router 0.22.1 → 0.23.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 (179) hide show
  1. package/dist/bin/rango.js +8 -3
  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/navigation-transaction.d.ts +21 -7
  7. package/dist/types/browser/partial-update.d.ts +34 -5
  8. package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
  9. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  10. package/dist/types/browser/response-adapter.d.ts +7 -0
  11. package/dist/types/browser/types.d.ts +19 -9
  12. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  13. package/dist/types/build/generate-route-types.d.ts +1 -1
  14. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  15. package/dist/types/build/route-types/scan-filter.d.ts +16 -2
  16. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  17. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  18. package/dist/types/client-urls/client-urls.d.ts +1 -1
  19. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  20. package/dist/types/client-urls/navigation.d.ts +0 -37
  21. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  22. package/dist/types/client-urls/server-projection.d.ts +29 -1
  23. package/dist/types/client-urls/types.d.ts +34 -69
  24. package/dist/types/client.d.ts +0 -1
  25. package/dist/types/errors.d.ts +5 -0
  26. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  27. package/dist/types/handles/pushed-by.d.ts +23 -0
  28. package/dist/types/loader-store.d.ts +8 -0
  29. package/dist/types/prefetch-markers.d.ts +11 -0
  30. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  31. package/dist/types/router/is-action.d.ts +2 -1
  32. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  33. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
  34. package/dist/types/server/context.d.ts +21 -1
  35. package/dist/types/server/handle-store.d.ts +9 -1
  36. package/dist/types/server/request-context.d.ts +14 -1
  37. package/dist/types/server.d.ts +1 -0
  38. package/dist/types/testing/flight.entry.d.ts +1 -1
  39. package/dist/types/testing/index.d.ts +3 -3
  40. package/dist/types/testing/internal/action-id.d.ts +10 -1
  41. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  42. package/dist/types/testing/internal/test-action.d.ts +8 -0
  43. package/dist/types/testing/run-loader.d.ts +2 -1
  44. package/dist/types/testing/run-revalidate.d.ts +74 -0
  45. package/dist/types/testing/run-transition-when.d.ts +2 -3
  46. package/dist/types/testing/serve-shell-request.d.ts +38 -0
  47. package/dist/types/testing/vitest.d.ts +11 -0
  48. package/dist/types/types/boundaries.d.ts +9 -0
  49. package/dist/types/types/handler-context.d.ts +14 -0
  50. package/dist/types/types/loader-types.d.ts +30 -0
  51. package/dist/types/types/segments.d.ts +21 -4
  52. package/dist/types/urls/path-helper-types.d.ts +25 -4
  53. package/dist/types/urls/type-extraction.d.ts +6 -2
  54. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  55. package/dist/types/vite/discovery/state.d.ts +11 -4
  56. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  57. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  58. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  59. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  60. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  61. package/dist/types/vite/router-discovery.d.ts +10 -0
  62. package/dist/vite/index.js +842 -308
  63. package/package.json +1 -1
  64. package/skills/caching/SKILL.md +42 -5
  65. package/skills/catalog.json +2 -2
  66. package/skills/client-urls/SKILL.md +113 -24
  67. package/skills/cloudflare/SKILL.md +14 -9
  68. package/skills/hooks/SKILL.md +11 -3
  69. package/skills/links/SKILL.md +24 -0
  70. package/skills/loader/SKILL.md +192 -1
  71. package/skills/migrate-nextjs/SKILL.md +2 -2
  72. package/skills/migrate-react-router/SKILL.md +10 -7
  73. package/skills/migrate-react-router/data-and-actions.md +1 -1
  74. package/skills/ppr/SKILL.md +13 -0
  75. package/skills/prerender/SKILL.md +22 -8
  76. package/skills/rango/SKILL.md +12 -6
  77. package/skills/route/SKILL.md +139 -0
  78. package/skills/testing/SKILL.md +3 -2
  79. package/skills/testing/client-components.md +1 -1
  80. package/skills/testing/predicates.md +67 -32
  81. package/skills/testing/serve-shell-request.md +88 -18
  82. package/skills/testing/setup.md +2 -2
  83. package/skills/typesafety/route-types.md +2 -2
  84. package/skills/view-transitions/SKILL.md +26 -0
  85. package/src/browser/defer.ts +82 -0
  86. package/src/browser/document-revealed.ts +52 -0
  87. package/src/browser/event-controller.ts +43 -32
  88. package/src/browser/navigation-bridge.ts +64 -14
  89. package/src/browser/navigation-client.ts +48 -36
  90. package/src/browser/navigation-store.ts +13 -0
  91. package/src/browser/navigation-transaction.ts +25 -8
  92. package/src/browser/partial-update.ts +528 -135
  93. package/src/browser/prefetch/fetch.ts +2 -1
  94. package/src/browser/react/NavigationProvider.tsx +47 -28
  95. package/src/browser/react/filter-segment-order.ts +13 -2
  96. package/src/browser/response-adapter.ts +20 -0
  97. package/src/browser/rsc-router.tsx +17 -45
  98. package/src/browser/server-action-bridge.ts +19 -63
  99. package/src/browser/types.ts +19 -9
  100. package/src/browser/validate-redirect-origin.ts +16 -0
  101. package/src/build/generate-route-types.ts +1 -0
  102. package/src/build/route-types/router-processing.ts +3 -6
  103. package/src/build/route-types/scan-filter.ts +37 -10
  104. package/src/cache/cf/cf-cache-store.ts +59 -21
  105. package/src/cache/cf/cf-cache-types.ts +24 -2
  106. package/src/cache/document-cache.ts +22 -1
  107. package/src/client-urls/client-urls.ts +22 -6
  108. package/src/client-urls/hoisted-revalidate.ts +25 -0
  109. package/src/client-urls/navigation.ts +0 -96
  110. package/src/client-urls/revalidate-registry.ts +30 -0
  111. package/src/client-urls/server-projection.ts +151 -65
  112. package/src/client-urls/types.ts +39 -71
  113. package/src/client.tsx +9 -5
  114. package/src/errors.ts +15 -0
  115. package/src/handles/deferred-resolution.ts +28 -5
  116. package/src/handles/pushed-by.ts +122 -0
  117. package/src/loader-store.ts +18 -7
  118. package/src/prefetch-markers.ts +13 -0
  119. package/src/route-content-wrapper.tsx +18 -4
  120. package/src/route-definition/dsl-helpers.ts +27 -4
  121. package/src/route-definition/helpers-types.ts +27 -1
  122. package/src/router/intercept-resolution.ts +6 -4
  123. package/src/router/is-action.ts +2 -1
  124. package/src/router/loader-resolution.ts +83 -4
  125. package/src/router/match-api.ts +36 -1
  126. package/src/router/match-middleware/background-revalidation.ts +11 -1
  127. package/src/router/match-middleware/cache-lookup.ts +29 -6
  128. package/src/router/match-middleware/cache-store.ts +17 -1
  129. package/src/router/match-result.ts +14 -2
  130. package/src/router/navigation-snapshot.ts +2 -1
  131. package/src/router/segment-resolution/fresh.ts +22 -18
  132. package/src/router/segment-resolution/loader-cache.ts +2 -1
  133. package/src/router/segment-resolution/loader-mask.ts +5 -3
  134. package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
  135. package/src/router/segment-resolution/revalidation.ts +169 -60
  136. package/src/rsc/rsc-rendering.ts +50 -7
  137. package/src/rsc/shell-capture.ts +2 -1
  138. package/src/segment-loader-promise.ts +23 -20
  139. package/src/segment-system.tsx +60 -26
  140. package/src/server/context.ts +39 -5
  141. package/src/server/handle-store.ts +104 -8
  142. package/src/server/request-context.ts +21 -1
  143. package/src/server.ts +4 -0
  144. package/src/testing/action-form.ts +2 -11
  145. package/src/testing/e2e/page-helpers.ts +3 -1
  146. package/src/testing/flight.entry.ts +1 -0
  147. package/src/testing/index.ts +6 -3
  148. package/src/testing/internal/action-id.ts +16 -1
  149. package/src/testing/internal/client-urls-projection.ts +70 -0
  150. package/src/testing/internal/test-action.ts +25 -0
  151. package/src/testing/run-loader.ts +2 -1
  152. package/src/testing/run-revalidate.ts +142 -0
  153. package/src/testing/run-transition-when.ts +7 -8
  154. package/src/testing/serve-shell-request.ts +116 -6
  155. package/src/testing/vitest.ts +67 -1
  156. package/src/types/boundaries.ts +12 -0
  157. package/src/types/handler-context.ts +14 -0
  158. package/src/types/loader-types.ts +30 -0
  159. package/src/types/segments.ts +26 -4
  160. package/src/urls/path-helper-types.ts +27 -5
  161. package/src/urls/type-extraction.ts +30 -10
  162. package/src/use-loader.tsx +18 -13
  163. package/src/vite/discovery/client-urls-projection.ts +20 -21
  164. package/src/vite/discovery/discover-routers.ts +94 -26
  165. package/src/vite/discovery/state.ts +20 -4
  166. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  167. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  168. package/src/vite/plugins/hoist/literal.ts +417 -0
  169. package/src/vite/plugins/hoist/scope.ts +370 -0
  170. package/src/vite/plugins/hoist/source.ts +84 -0
  171. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  172. package/src/vite/rango.ts +2 -0
  173. package/src/vite/router-discovery.ts +93 -32
  174. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  175. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  176. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  177. package/src/client-urls/revalidate-chain.ts +0 -83
  178. package/src/client-urls/revalidation-protocol.ts +0 -56
  179. 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 relative3 } 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 = relative3(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 = relative3(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 {
@@ -2433,6 +2443,9 @@ function compareStrings(a, b) {
2433
2443
  // src/vite/discovery/state.ts
2434
2444
  var VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
2435
2445
  var VIRTUAL_LOADER_MANIFEST_ID = "virtual:rsc-router/loader-manifest";
2446
+ function canRefreshRuntimeDiscovery(state, hasMainRunner) {
2447
+ return state.perRouterManifests.length > 0 || hasMainRunner && !!state.lastDiscoveryError;
2448
+ }
2436
2449
  function createDiscoveryState(entryPath, opts) {
2437
2450
  return {
2438
2451
  resolvedEntryPath: entryPath,
@@ -3201,18 +3214,18 @@ async function transformUseCache(code, id, options) {
3201
3214
  return;
3202
3215
  }
3203
3216
  }
3204
- const { hasDirective: hasDirective2, transformWrapExport, transformHoistInlineDirective } = rscTransforms;
3217
+ const { hasDirective: hasDirective4, transformWrapExport, transformHoistInlineDirective } = rscTransforms;
3205
3218
  let ast;
3206
3219
  try {
3207
- const { parseAst: parseAst8 } = await import("vite");
3208
- ast = parseAst8(code, { lang: "tsx" });
3220
+ const { parseAst: parseAst9 } = await import("vite");
3221
+ ast = parseAst9(code, { lang: "tsx" });
3209
3222
  } catch {
3210
3223
  return;
3211
3224
  }
3212
3225
  stripNullDirectiveFields(ast);
3213
3226
  const filePath = normalizePath(path5.relative(options.root, id));
3214
3227
  const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
3215
- if (hasDirective2(ast.body, "use cache")) {
3228
+ if (hasDirective4(ast.body, "use cache")) {
3216
3229
  return transformFileLevelUseCache(
3217
3230
  code,
3218
3231
  ast,
@@ -3221,7 +3234,7 @@ async function transformUseCache(code, id, options) {
3221
3234
  options.isBuild,
3222
3235
  isLayoutOrTemplate,
3223
3236
  transformWrapExport,
3224
- hasDirective2
3237
+ hasDirective4
3225
3238
  );
3226
3239
  }
3227
3240
  const functionResult = transformFunctionLevelUseCache(
@@ -3235,7 +3248,7 @@ async function transformUseCache(code, id, options) {
3235
3248
  warnOnNearMissDirectives(ast, id, options.warn);
3236
3249
  if (functionResult) return functionResult;
3237
3250
  }
3238
- async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLayoutOrTemplate, transformWrapExport, hasDirective2) {
3251
+ async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLayoutOrTemplate, transformWrapExport, hasDirective4) {
3239
3252
  const unconfirmedExports = [];
3240
3253
  const { exportNames, output } = transformWrapExport(code, ast, {
3241
3254
  runtime: (value, name) => {
@@ -3247,7 +3260,7 @@ async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild
3247
3260
  if (name === "default" && isLayoutOrTemplate) return false;
3248
3261
  if (name.startsWith("$$hoist_")) return false;
3249
3262
  if (isHoistedServerReferenceRebind(meta.valueNode)) return false;
3250
- if (functionHasUseServerDirective(meta.valueNode, hasDirective2)) {
3263
+ if (functionHasUseServerDirective(meta.valueNode, hasDirective4)) {
3251
3264
  return false;
3252
3265
  }
3253
3266
  if (meta.isFunction !== true) {
@@ -3264,8 +3277,8 @@ async function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild
3264
3277
  );
3265
3278
  }
3266
3279
  if (exportNames.length === 0) {
3267
- const { default: MagicString5 } = await import("magic-string");
3268
- const s = new MagicString5(code);
3280
+ const { default: MagicString6 } = await import("magic-string");
3281
+ const s = new MagicString6(code);
3269
3282
  const directive2 = findFileLevelDirective(ast);
3270
3283
  if (directive2) {
3271
3284
  s.overwrite(
@@ -3341,13 +3354,13 @@ function isHoistedServerReferenceRebind(valueNode) {
3341
3354
  const first = valueNode.arguments[0];
3342
3355
  return first !== void 0 && first.type === "Identifier" && first.name.startsWith("$$hoist_");
3343
3356
  }
3344
- function functionHasUseServerDirective(valueNode, hasDirective2) {
3357
+ function functionHasUseServerDirective(valueNode, hasDirective4) {
3345
3358
  if (!valueNode || !("body" in valueNode)) return false;
3346
3359
  const { body } = valueNode;
3347
3360
  if (!body || Array.isArray(body) || body.type !== "BlockStatement") {
3348
3361
  return false;
3349
3362
  }
3350
- return hasDirective2(
3363
+ return hasDirective4(
3351
3364
  body.body,
3352
3365
  "use server"
3353
3366
  );
@@ -3390,10 +3403,10 @@ function warnOnNearMissDirectives(ast, fileId, warn) {
3390
3403
  }
3391
3404
 
3392
3405
  // src/vite/plugins/transition-when-hoist.ts
3393
- import { normalizePath as normalizePath2, parseAst as parseAst3 } from "vite";
3406
+ import { normalizePath as normalizePath3, parseAst as parseAst3 } from "vite";
3394
3407
  import MagicString4 from "magic-string";
3395
3408
  import { builtinModules } from "node:module";
3396
- import { readFile } from "node:fs/promises";
3409
+ import { hasDirective as hasDirective2 } from "@vitejs/plugin-rsc/transforms";
3397
3410
 
3398
3411
  // src/route-name.ts
3399
3412
  var AUTO_GENERATED_ROUTE_PREFIX = "$path_";
@@ -3424,10 +3437,7 @@ function findTransitionWhenError(error) {
3424
3437
  return void 0;
3425
3438
  }
3426
3439
 
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_";
3440
+ // src/vite/plugins/hoist/scope.ts
3431
3441
  var Scope = class {
3432
3442
  constructor(parent, isFunction, node) {
3433
3443
  this.parent = parent;
@@ -3733,29 +3743,23 @@ function collectImports(program) {
3733
3743
  }
3734
3744
  return imports;
3735
3745
  }
3736
- function hasUseClientDirective(program) {
3746
+
3747
+ // src/vite/plugins/hoist/literal.ts
3748
+ function directivePrologueEnd(program) {
3749
+ let end = 0;
3737
3750
  for (const statement of program.body) {
3738
3751
  if (statement.type !== "ExpressionStatement" || typeof statement.directive !== "string") {
3739
- return false;
3752
+ break;
3740
3753
  }
3741
- if (statement.directive === "use client") return true;
3754
+ end = statement.end;
3742
3755
  }
3743
- return false;
3756
+ return end;
3744
3757
  }
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}`;
3758
+ function parseLang(file) {
3759
+ if (/\.[cm]?ts$/.test(file)) return "ts";
3760
+ if (/\.[cm]?jsx?$/.test(file)) return "jsx";
3761
+ return "tsx";
3757
3762
  }
3758
- var ROUTER_SOURCE = "@rangojs/router";
3759
3763
  var EXPRESSION_WRAPPERS = /* @__PURE__ */ new Set([
3760
3764
  "TSAsExpression",
3761
3765
  "TSSatisfiesExpression",
@@ -3773,51 +3777,50 @@ function isFunctionLiteral(node) {
3773
3777
  function propertyKeyIs(prop, name) {
3774
3778
  return prop?.type === "Property" && !prop.computed && (prop.key.type === "Identifier" && prop.key.name === name || prop.key.type === "Literal" && prop.key.value === name);
3775
3779
  }
3776
- function findWhenSites(program, scopeOf, moduleScope, imports) {
3777
- const sites = [];
3780
+ function findHelperCalls(program, scopeOf, moduleScope, imports, spec) {
3781
+ const calls = [];
3778
3782
  const helpers = /* @__PURE__ */ new Map();
3779
- const routerExport = (callee, scope) => {
3780
- const routerImport = (id) => {
3783
+ const sourceExport = (callee, scope) => {
3784
+ const sourceImport = (id) => {
3781
3785
  if (id?.type !== "Identifier") return null;
3782
3786
  if (scope.lookup(id.name) !== moduleScope) return null;
3783
3787
  const binding = imports.get(id.name);
3784
- return binding?.source === ROUTER_SOURCE && !binding.typeOnly ? binding : null;
3788
+ return binding && spec.sources.has(binding.source) && !binding.typeOnly ? binding : null;
3785
3789
  };
3786
3790
  if (callee.type === "Identifier") {
3787
- const imported = routerImport(callee)?.imported;
3791
+ const imported = sourceImport(callee)?.imported;
3788
3792
  return imported && imported !== "*" && imported !== "default" ? imported : null;
3789
3793
  }
3790
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && routerImport(callee.object)?.imported === "*") {
3794
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && sourceImport(callee.object)?.imported === "*") {
3791
3795
  return callee.property.name;
3792
3796
  }
3793
3797
  return null;
3794
3798
  };
3795
- const registerUrlsCallback = (callback) => {
3799
+ const registerBuilderCallback = (callback) => {
3796
3800
  const fnScope = scopeOf.get(callback);
3797
3801
  let param = callback.params[0];
3798
3802
  if (param?.type === "AssignmentPattern") param = param.left;
3799
3803
  if (!fnScope || !param) return;
3800
- const bindings = {
3801
- transitions: /* @__PURE__ */ new Set(),
3802
- objects: /* @__PURE__ */ new Set()
3803
- };
3804
+ const bindings = { names: /* @__PURE__ */ new Set(), objects: /* @__PURE__ */ new Set() };
3804
3805
  if (param.type === "Identifier") bindings.objects.add(param.name);
3805
3806
  if (param.type === "ObjectPattern") {
3806
3807
  for (const prop of param.properties) {
3807
- if (!propertyKeyIs(prop, "transition")) continue;
3808
+ if (!propertyKeyIs(prop, spec.helper)) continue;
3808
3809
  const target = prop.value.type === "AssignmentPattern" ? prop.value.left : prop.value;
3809
- if (target.type === "Identifier") bindings.transitions.add(target.name);
3810
+ if (target.type === "Identifier") bindings.names.add(target.name);
3810
3811
  }
3811
3812
  }
3812
3813
  helpers.set(fnScope, bindings);
3813
3814
  };
3814
- const isRouterTransition = (callee, scope) => {
3815
- if (routerExport(callee, scope) === "transition") return true;
3815
+ const isHelper = (callee, scope) => {
3816
+ if (spec.directImport && sourceExport(callee, scope) === spec.helper) {
3817
+ return true;
3818
+ }
3816
3819
  if (callee.type === "Identifier") {
3817
3820
  const decl = scope.lookup(callee.name);
3818
- return !!decl && !!helpers.get(decl)?.transitions.has(callee.name);
3821
+ return !!decl && !!helpers.get(decl)?.names.has(callee.name);
3819
3822
  }
3820
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "transition" && callee.object.type === "Identifier") {
3823
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === spec.helper && callee.object.type === "Identifier") {
3821
3824
  const decl = scope.lookup(callee.object.name);
3822
3825
  return !!decl && !!helpers.get(decl)?.objects.has(callee.object.name);
3823
3826
  }
@@ -3833,20 +3836,10 @@ function findWhenSites(program, scopeOf, moduleScope, imports) {
3833
3836
  const own = scopeOf.get(node) ?? scope;
3834
3837
  if (node.type === "CallExpression") {
3835
3838
  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
- }
3839
+ if (isFunctionLiteral(callback) && sourceExport(node.callee, own) === spec.builder) {
3840
+ registerBuilderCallback(callback);
3849
3841
  }
3842
+ if (isHelper(node.callee, own)) calls.push({ call: node, scope: own });
3850
3843
  }
3851
3844
  for (const key of Object.keys(node)) {
3852
3845
  if (key === "type" || key === "start" || key === "end") continue;
@@ -3854,27 +3847,21 @@ function findWhenSites(program, scopeOf, moduleScope, imports) {
3854
3847
  }
3855
3848
  };
3856
3849
  visit(program.body, moduleScope);
3857
- return sites;
3850
+ return calls;
3858
3851
  }
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) {
3852
+ function findLexicalCapture(literal, references, code, messages) {
3866
3853
  const ownArguments = [];
3867
3854
  let found = null;
3868
3855
  const capture = (node, what) => {
3869
3856
  found = {
3870
3857
  node,
3871
- detail: `${LEXICAL_CAPTURE[what]} Read what you need from \`to.params\`/\`to.state\`, or export the predicate from a "use client" module.`
3858
+ detail: `${messages.lexical[what]} ${messages.captureHint}`
3872
3859
  };
3873
3860
  };
3874
3861
  const loads = (node, expression, why) => {
3875
3862
  found = {
3876
3863
  node,
3877
- detail: `\`${expression}\` ${why} Import client-safe modules statically, or export the predicate from a "use client" module.`
3864
+ detail: `\`${expression}\` ${why} ${messages.loadsHint}`
3878
3865
  };
3879
3866
  };
3880
3867
  const visit = (node, boundary) => {
@@ -3907,7 +3894,7 @@ function findLexicalCapture(literal, references, code) {
3907
3894
  }
3908
3895
  case "MetaProperty":
3909
3896
  if (node.meta.name === "import") {
3910
- loads(node, "import.meta", "is the server module's.");
3897
+ loads(node, "import.meta", messages.importMetaWhy);
3911
3898
  } else if (!boundary) {
3912
3899
  capture(node, "new.target");
3913
3900
  }
@@ -3919,7 +3906,7 @@ function findLexicalCapture(literal, references, code) {
3919
3906
  loads(
3920
3907
  node,
3921
3908
  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."
3909
+ messages.importMetaMemberWhy
3923
3910
  );
3924
3911
  return;
3925
3912
  }
@@ -3928,7 +3915,7 @@ function findLexicalCapture(literal, references, code) {
3928
3915
  loads(
3929
3916
  node,
3930
3917
  `import(${code.slice(node.source.start, node.source.end)})`,
3931
- "loads a module at run time, which the build cannot check is client-safe."
3918
+ messages.dynamicImportWhy
3932
3919
  );
3933
3920
  return;
3934
3921
  }
@@ -3953,32 +3940,150 @@ function findLexicalCapture(literal, references, code) {
3953
3940
  }
3954
3941
  return null;
3955
3942
  }
3943
+ function collectFreeReferences(literal, references, moduleScope, imports) {
3944
+ const used = /* @__PURE__ */ new Map();
3945
+ const bindings = [];
3946
+ const globals = [];
3947
+ for (const ref of references) {
3948
+ if (ref.node.start < literal.start || ref.node.end > literal.end) continue;
3949
+ const decl = ref.scope.lookup(ref.node.name);
3950
+ if (!decl) {
3951
+ if (!ref.typeOnly) globals.push(ref);
3952
+ continue;
3953
+ }
3954
+ if (decl.node.start >= literal.start && decl.node.end <= literal.end && decl !== moduleScope) {
3955
+ continue;
3956
+ }
3957
+ if (decl === moduleScope && imports.has(ref.node.name)) {
3958
+ const valueUse = !ref.typeOnly && !imports.get(ref.node.name).typeOnly;
3959
+ used.set(ref.node.name, (used.get(ref.node.name) ?? false) || valueUse);
3960
+ continue;
3961
+ }
3962
+ if (ref.typeOnly) continue;
3963
+ bindings.push(ref);
3964
+ }
3965
+ return { imports: used, bindings, globals };
3966
+ }
3967
+ async function emitImports(used, imports, checkValueImport) {
3968
+ const bySource = /* @__PURE__ */ new Map();
3969
+ for (const [local, valueUse] of used) {
3970
+ const binding = imports.get(local);
3971
+ if (valueUse) await checkValueImport(local, binding);
3972
+ const lines = bySource.get(binding.source) ?? [];
3973
+ const typePrefix = valueUse ? "" : "type ";
3974
+ if (binding.imported === "*") {
3975
+ lines.push(
3976
+ `import ${typePrefix}* as ${local} from ${JSON.stringify(binding.source)};`
3977
+ );
3978
+ } else if (binding.imported === "default") {
3979
+ lines.push(
3980
+ `import ${typePrefix}${local} from ${JSON.stringify(binding.source)};`
3981
+ );
3982
+ } else {
3983
+ const spec = binding.imported === local ? local : `${JSON.stringify(binding.imported)} as ${local}`;
3984
+ lines.push(
3985
+ `import ${typePrefix}{ ${spec} } from ${JSON.stringify(binding.source)};`
3986
+ );
3987
+ }
3988
+ bySource.set(binding.source, lines);
3989
+ }
3990
+ return [...bySource.values()].flat();
3991
+ }
3992
+
3993
+ // src/vite/plugins/hoist/source.ts
3994
+ import { readFile } from "node:fs/promises";
3995
+ import { normalizePath as normalizePath2 } from "vite";
3996
+ var SOURCE_FILE_RE = /\.[cm]?[jt]sx?$/;
3997
+ async function readSource(file) {
3998
+ try {
3999
+ return await readFile(file, "utf-8");
4000
+ } catch {
4001
+ return null;
4002
+ }
4003
+ }
4004
+ async function resolveSourceTarget(ctx, source, importer, options) {
4005
+ const resolved2 = await ctx.resolve(source, importer);
4006
+ if (!resolved2 || resolved2.external) return null;
4007
+ const target = resolved2.id.split("?", 1)[0];
4008
+ if (target.startsWith("\0")) return null;
4009
+ if (options.skipNodeModules && target.includes("/node_modules/")) {
4010
+ return null;
4011
+ }
4012
+ return target;
4013
+ }
4014
+ async function recordEmitted(emitted, file, value) {
4015
+ emitted.set(file, { disk: await readSource(file), value });
4016
+ }
4017
+ async function loadEmittedOrDisk(emitted, hoistedFile, rehoist) {
4018
+ const disk = await readSource(hoistedFile);
4019
+ const entry = emitted.get(normalizePath2(hoistedFile));
4020
+ if (entry && (disk === null || entry.disk === disk)) return entry.value;
4021
+ return disk === null ? void 0 : rehoist(disk);
4022
+ }
4023
+
4024
+ // src/vite/plugins/transition-when-hoist.ts
4025
+ var TRANSITION_WHEN_QUERY = "rango-when";
4026
+ var EXPORT_NAME = "__rango_when";
4027
+ var LOCAL_PREFIX = "__rango_when_";
4028
+ var ROUTER_SOURCES = /* @__PURE__ */ new Set(["@rangojs/router"]);
4029
+ function findWhenSites(program, scopeOf, moduleScope, imports) {
4030
+ const sites = [];
4031
+ for (const { call, scope } of findHelperCalls(
4032
+ program,
4033
+ scopeOf,
4034
+ moduleScope,
4035
+ imports,
4036
+ {
4037
+ sources: ROUTER_SOURCES,
4038
+ builder: "urls",
4039
+ helper: "transition",
4040
+ directImport: true
4041
+ }
4042
+ )) {
4043
+ if (call.arguments[0]?.type !== "ObjectExpression") continue;
4044
+ for (const prop of call.arguments[0].properties) {
4045
+ if (propertyKeyIs(prop, "when")) {
4046
+ sites.push({ prop, value: unwrapExpression(prop.value), scope });
4047
+ }
4048
+ }
4049
+ }
4050
+ return sites;
4051
+ }
4052
+ var WHEN_CAPTURE_MESSAGES = {
4053
+ lexical: {
4054
+ this: "`this` in an arrow is the enclosing server function's `this`.",
4055
+ arguments: "`arguments` in an arrow is the enclosing server function's `arguments`.",
4056
+ super: "`super` refers to the enclosing server object.",
4057
+ "new.target": "`new.target` in an arrow is the enclosing server function's."
4058
+ },
4059
+ captureHint: 'Read what you need from `to.params`/`to.state`, or export the predicate from a "use client" module.',
4060
+ loadsHint: 'Import client-safe modules statically, or export the predicate from a "use client" module.',
4061
+ importMetaWhy: "is the server module's.",
4062
+ importMetaMemberWhy: "is resolved against the server module; only `import.meta.env` is available to an inline predicate.",
4063
+ dynamicImportWhy: "loads a module at run time, which the build cannot check is client-safe."
4064
+ };
3956
4065
  var NODE_BUILTINS = new Set(builtinModules);
3957
4066
  function isServerOnlySpecifier(source) {
3958
4067
  return source === "server-only" || source.startsWith("node:") || source.startsWith("cloudflare:") || NODE_BUILTINS.has(source);
3959
4068
  }
3960
4069
  var ROUTER_IMPORT_RE = /["']@rangojs\/router["']/;
3961
4070
  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
4071
  function whenError(file, code, node, detail) {
4072
+ const { line, column } = offsetToLineColumn(code, node.start);
3968
4073
  return new TransitionWhenError(
3969
- `[rango] transition({ when }) at ${file}:${lineColumn(code, node.start)}: \`when\` runs in the browser; ${detail}`
4074
+ `[rango] transition({ when }) at ${file}:${line}:${column}: \`when\` runs in the browser; ${detail}`
3970
4075
  );
3971
4076
  }
3972
4077
  async function hoistTransitionWhens(code, id, deps = {}) {
3973
4078
  if (!ROUTER_IMPORT_RE.test(code) || !WHEN_RE.test(code)) return null;
3974
- const file = normalizePath2(id.split("?", 1)[0]);
4079
+ const file = normalizePath3(id.split("?", 1)[0]);
3975
4080
  let program;
3976
4081
  try {
3977
4082
  program = parseAst3(code, { lang: parseLang(file) });
3978
4083
  } catch {
3979
4084
  return null;
3980
4085
  }
3981
- if (hasUseClientDirective(program)) return null;
4086
+ if (hasDirective2(program.body, "use client")) return null;
3982
4087
  const { moduleScope, references, scopeOf } = analyzeScopes(program);
3983
4088
  const imports = collectImports(program);
3984
4089
  const sites = findWhenSites(program, scopeOf, moduleScope, imports);
@@ -4009,66 +4114,43 @@ async function hoistTransitionWhens(code, id, deps = {}) {
4009
4114
  continue;
4010
4115
  }
4011
4116
  if (!isFunctionLiteral(value)) continue;
4012
- const lexical = findLexicalCapture(value, references, code);
4117
+ const lexical = findLexicalCapture(
4118
+ value,
4119
+ references,
4120
+ code,
4121
+ WHEN_CAPTURE_MESSAGES
4122
+ );
4013
4123
  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;
4124
+ const free = collectFreeReferences(value, references, moduleScope, imports);
4125
+ const binding = free.bindings[0];
4126
+ if (binding) {
4028
4127
  throw whenError(
4029
4128
  file,
4030
4129
  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.`
4130
+ binding.node,
4131
+ `\`${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
4132
  );
4034
4133
  }
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)) {
4134
+ const importLines = await emitImports(
4135
+ free.imports,
4136
+ imports,
4137
+ async (local2, imported) => {
4138
+ if (isServerOnlySpecifier(imported.source) || await deps.isServerOnlyImport?.(imported.source)) {
4040
4139
  throw whenError(
4041
4140
  file,
4042
4141
  code,
4043
4142
  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.`
4143
+ `\`${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
4144
  );
4046
4145
  }
4047
4146
  }
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
- }
4147
+ );
4066
4148
  const source = prop.method ? `${value.async ? "async " : ""}function${value.generator ? "*" : ""} ` + code.slice(value.start, value.end) : code.slice(value.start, value.end);
4067
4149
  const index = modules.length;
4068
4150
  modules.push(
4069
4151
  [
4070
4152
  `"use client";`,
4071
- ...[...bySource.values()].flat(),
4153
+ ...importLines,
4072
4154
  `export const ${EXPORT_NAME} = ${source};`,
4073
4155
  ""
4074
4156
  ].join("\n")
@@ -4086,13 +4168,7 @@ async function hoistTransitionWhens(code, id, deps = {}) {
4086
4168
  );
4087
4169
  }
4088
4170
  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
- }
4171
+ const insertAt = directivePrologueEnd(program);
4096
4172
  s.appendLeft(insertAt, `${insertAt ? "\n" : ""}${header.join("\n")}
4097
4173
  `);
4098
4174
  return {
@@ -4110,13 +4186,6 @@ function parseHoistedWhenId(id) {
4110
4186
  return { file: id.slice(0, q), index: Number(raw) };
4111
4187
  }
4112
4188
  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
4189
  function transitionWhenHoistPlugin() {
4121
4190
  const emitted = /* @__PURE__ */ new Map();
4122
4191
  return {
@@ -4125,10 +4194,11 @@ function transitionWhenHoistPlugin() {
4125
4194
  async load(id) {
4126
4195
  const hoisted = parseHoistedWhenId(id);
4127
4196
  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;
4197
+ const modules = await loadEmittedOrDisk(
4198
+ emitted,
4199
+ hoisted.file,
4200
+ async (disk) => (await hoistTransitionWhens(disk, hoisted.file))?.modules
4201
+ );
4132
4202
  const module = modules?.[hoisted.index];
4133
4203
  if (module === void 0) {
4134
4204
  throw new TransitionWhenError(
@@ -4140,35 +4210,397 @@ function transitionWhenHoistPlugin() {
4140
4210
  async transform(code, id) {
4141
4211
  if (this.environment?.name !== "rsc") return;
4142
4212
  if (id.includes("/node_modules/") || parseHoistedWhenId(id)) return;
4143
- const file = normalizePath2(id.split("?", 1)[0]);
4144
- if (!/\.[cm]?[jt]sx?$/.test(file)) return;
4213
+ const file = normalizePath3(id.split("?", 1)[0]);
4214
+ if (!SOURCE_FILE_RE.test(file)) return;
4145
4215
  emitted.delete(file);
4146
4216
  const result = await hoistTransitionWhens(code, id, {
4147
4217
  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
- }
4218
+ const target = await resolveSourceTarget(this, source, id, {
4219
+ skipNodeModules: true
4220
+ });
4221
+ if (target === null) return false;
4154
4222
  const text = await readSource(target);
4155
4223
  return text !== null && SERVER_ONLY_IMPORT_RE.test(text);
4156
4224
  }
4157
4225
  });
4158
4226
  if (!result) return;
4159
- emitted.set(file, {
4160
- disk: await readSource(file),
4161
- modules: result.modules
4162
- });
4227
+ await recordEmitted(emitted, file, result.modules);
4163
4228
  return { code: result.code, map: result.map };
4164
4229
  }
4165
4230
  };
4166
4231
  }
4167
4232
 
4233
+ // src/vite/plugins/client-urls-revalidate-hoist.ts
4234
+ import { normalizePath as normalizePath4, parseAst as parseAst5 } from "vite";
4235
+ import MagicString5 from "magic-string";
4236
+ import { createHash as createHash2 } from "node:crypto";
4237
+ import { relative } from "node:path";
4238
+ import { hasDirective as hasDirective3 } from "@vitejs/plugin-rsc/transforms";
4239
+
4240
+ // src/client-urls/hoisted-revalidate.ts
4241
+ var HOISTED_REVALIDATE_KEY = "$$rangoRevalidate";
4242
+ function isHoistedRevalidateRef(value) {
4243
+ return typeof value === "object" && value !== null && typeof value[HOISTED_REVALIDATE_KEY] === "string";
4244
+ }
4245
+
4246
+ // src/vite/utils/directive-prologue.ts
4247
+ import { parseAst as parseAst4 } from "vite";
4248
+ function hasUseClientDirective(source) {
4249
+ let program;
4250
+ try {
4251
+ program = parseAst4(source, { lang: "tsx" });
4252
+ } catch {
4253
+ return false;
4254
+ }
4255
+ for (const node of program.body ?? []) {
4256
+ if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
4257
+ if (node.expression.value === "use client") return true;
4258
+ continue;
4259
+ }
4260
+ break;
4261
+ }
4262
+ return false;
4263
+ }
4264
+
4265
+ // src/vite/plugins/client-urls-revalidate-hoist.ts
4266
+ var CLIENT_URLS_REVALIDATE_QUERY = "rango-revalidate";
4267
+ var REGISTER = "__rango_registerClientUrlRevalidators";
4268
+ var CLIENT_SOURCES = /* @__PURE__ */ new Set(["@rangojs/router/client"]);
4269
+ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
4270
+ "window",
4271
+ "document",
4272
+ "navigator",
4273
+ "localStorage",
4274
+ "sessionStorage",
4275
+ "location",
4276
+ "history"
4277
+ ]);
4278
+ var NOT_A_FUNCTION = /* @__PURE__ */ new Set([
4279
+ "ObjectExpression",
4280
+ "ArrayExpression",
4281
+ "Literal",
4282
+ "TemplateLiteral",
4283
+ "ClassExpression"
4284
+ ]);
4285
+ var CAPTURE_MESSAGES = {
4286
+ lexical: {
4287
+ this: "`this` in an arrow is the enclosing function's `this`.",
4288
+ arguments: "`arguments` in an arrow is the enclosing function's.",
4289
+ super: "`super` refers to the enclosing object.",
4290
+ "new.target": "`new.target` in an arrow is the enclosing function's."
4291
+ },
4292
+ captureHint: "Read what you need from the predicate's argument.",
4293
+ loadsHint: "Import server-safe modules statically.",
4294
+ importMetaWhy: `is the "use client" module's.`,
4295
+ importMetaMemberWhy: 'is resolved against the "use client" module; only `import.meta.env` is available to a hoisted predicate.',
4296
+ dynamicImportWhy: "loads a module at run time, which the build cannot check is server-safe."
4297
+ };
4298
+ var CLIENT_URLS_RE = /\bclientUrls\b/;
4299
+ var CLIENT_IMPORT_RE = /["']@rangojs\/router\/client["']/;
4300
+ var REVALIDATE_RE = /\brevalidate\b/;
4301
+ function mayHoist(code) {
4302
+ return code.includes("use client") && CLIENT_URLS_RE.test(code) && CLIENT_IMPORT_RE.test(code) && REVALIDATE_RE.test(code);
4303
+ }
4304
+ function revalidateError(file, code, node, detail) {
4305
+ const { line, column } = offsetToLineColumn(code, node.start);
4306
+ return new Error(
4307
+ `[rango] clientUrls() revalidate() at ${file}:${line}:${column}: revalidate() runs on the server; ${detail}`
4308
+ );
4309
+ }
4310
+ var FINGERPRINT_SKIP = /* @__PURE__ */ new Set([
4311
+ "type",
4312
+ "start",
4313
+ "end",
4314
+ "loc",
4315
+ "range",
4316
+ "raw",
4317
+ "extra",
4318
+ "shorthand",
4319
+ // TS-only flags (`let x!: T`), lost with the types.
4320
+ "definite",
4321
+ ...TYPE_KEYS
4322
+ ]);
4323
+ function fingerprint(node) {
4324
+ const out = [];
4325
+ const visit = (value) => {
4326
+ if (value === null || value === void 0) return;
4327
+ if (Array.isArray(value)) {
4328
+ out.push("[");
4329
+ for (const item of value) visit(item);
4330
+ out.push("]");
4331
+ return;
4332
+ }
4333
+ if (typeof value !== "object") {
4334
+ out.push(JSON.stringify(typeof value === "bigint" ? `${value}n` : value));
4335
+ return;
4336
+ }
4337
+ let entry = value;
4338
+ if (typeof entry.type === "string") {
4339
+ entry = unwrapExpression(entry);
4340
+ if (entry.type.startsWith("TS")) {
4341
+ visit(entry.expression);
4342
+ return;
4343
+ }
4344
+ out.push(entry.type, "(");
4345
+ if (entry.type === "TemplateElement") {
4346
+ out.push(JSON.stringify(entry.value.raw));
4347
+ }
4348
+ }
4349
+ for (const key of Object.keys(entry)) {
4350
+ if (FINGERPRINT_SKIP.has(key)) continue;
4351
+ if (key === "optional" && entry.type === "Identifier") continue;
4352
+ out.push(key);
4353
+ visit(entry[key]);
4354
+ }
4355
+ if (typeof entry.type === "string") out.push(")");
4356
+ };
4357
+ visit(node);
4358
+ return out.join(" ");
4359
+ }
4360
+ function keyOf(relativeFile, predicate) {
4361
+ return createHash2("sha256").update(relativeFile).update("\0").update(fingerprint(predicate)).digest("hex").slice(0, 12);
4362
+ }
4363
+ async function hoistClientUrlsRevalidates(code, id, deps) {
4364
+ if (!mayHoist(code)) return null;
4365
+ const file = normalizePath4(id.split("?", 1)[0]);
4366
+ let program;
4367
+ try {
4368
+ program = parseAst5(code, { lang: parseLang(file) });
4369
+ } catch {
4370
+ return null;
4371
+ }
4372
+ if (!hasDirective3(program.body, "use client")) return null;
4373
+ const { moduleScope, references, scopeOf } = analyzeScopes(program);
4374
+ const imports = collectImports(program);
4375
+ const calls = findHelperCalls(program, scopeOf, moduleScope, imports, {
4376
+ sources: CLIENT_SOURCES,
4377
+ builder: "clientUrls",
4378
+ helper: "revalidate",
4379
+ directImport: false
4380
+ });
4381
+ if (calls.length === 0) return null;
4382
+ const relativeFile = normalizePath4(relative(deps.root, file));
4383
+ const s = new MagicString5(code);
4384
+ const used = /* @__PURE__ */ new Map();
4385
+ const entries = /* @__PURE__ */ new Map();
4386
+ for (const { call } of calls) {
4387
+ const [arg, extra] = call.arguments;
4388
+ if (!arg || extra || arg.type === "SpreadElement") {
4389
+ throw revalidateError(
4390
+ file,
4391
+ code,
4392
+ call,
4393
+ "`revalidate()` takes exactly one predicate."
4394
+ );
4395
+ }
4396
+ const value = unwrapExpression(arg);
4397
+ if (NOT_A_FUNCTION.has(value.type)) {
4398
+ throw revalidateError(
4399
+ file,
4400
+ code,
4401
+ value,
4402
+ "`revalidate()` expects a predicate function."
4403
+ );
4404
+ }
4405
+ if (isFunctionLiteral(value) && (value.async || value.generator)) {
4406
+ throw revalidateError(
4407
+ file,
4408
+ code,
4409
+ value,
4410
+ `a${value.async ? "n `async`" : " generator"} function returns ${value.async ? "a Promise" : "an iterator"}, which the router ignores: return a boolean, \`{ defaultShouldRevalidate }\`, or nothing.`
4411
+ );
4412
+ }
4413
+ const lexical = findLexicalCapture(
4414
+ value,
4415
+ references,
4416
+ code,
4417
+ CAPTURE_MESSAGES
4418
+ );
4419
+ if (lexical)
4420
+ throw revalidateError(file, code, lexical.node, lexical.detail);
4421
+ const free = collectFreeReferences(value, references, moduleScope, imports);
4422
+ const binding = free.bindings[0];
4423
+ if (binding) {
4424
+ const name = binding.node.name;
4425
+ throw revalidateError(
4426
+ file,
4427
+ code,
4428
+ binding.node,
4429
+ 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\`, ...).`
4430
+ );
4431
+ }
4432
+ const browserGlobal = free.globals.find(
4433
+ (ref) => BROWSER_GLOBALS.has(ref.node.name)
4434
+ );
4435
+ if (browserGlobal) {
4436
+ throw revalidateError(
4437
+ file,
4438
+ code,
4439
+ browserGlobal.node,
4440
+ `\`${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.`
4441
+ );
4442
+ }
4443
+ const unchecked = [...free.imports].filter(
4444
+ ([local, valueUse]) => valueUse && !used.get(local)
4445
+ );
4446
+ const clientOnly = await Promise.all(
4447
+ unchecked.map(([local]) => isClientOnly(imports.get(local), deps))
4448
+ );
4449
+ const offender = unchecked.find((_, i) => clientOnly[i]);
4450
+ if (offender) {
4451
+ const [local] = offender;
4452
+ throw revalidateError(
4453
+ file,
4454
+ code,
4455
+ value,
4456
+ `\`${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".`
4457
+ );
4458
+ }
4459
+ for (const [local, valueUse] of free.imports) {
4460
+ used.set(local, (used.get(local) ?? false) || valueUse);
4461
+ }
4462
+ const source = code.slice(value.start, value.end);
4463
+ const key = keyOf(relativeFile, value);
4464
+ entries.set(key, source);
4465
+ s.overwrite(
4466
+ arg.start,
4467
+ arg.end,
4468
+ `{ ${HOISTED_REVALIDATE_KEY}: ${JSON.stringify(key)} }`
4469
+ );
4470
+ }
4471
+ const importLines = await emitImports(used, imports, () => {
4472
+ });
4473
+ const module = [
4474
+ `import { registerClientUrlRevalidators as ${REGISTER} } from "@rangojs/router/server";`,
4475
+ ...importLines,
4476
+ `${REGISTER}({`,
4477
+ ...[...entries].map(
4478
+ ([key, source]) => ` ${JSON.stringify(key)}: ${source},`
4479
+ ),
4480
+ `});`,
4481
+ ""
4482
+ ].join("\n");
4483
+ return {
4484
+ code: s.toString(),
4485
+ map: s.generateMap({ hires: true, source: file }),
4486
+ module,
4487
+ keys: [...entries.keys()]
4488
+ };
4489
+ }
4490
+ async function isClientOnly(binding, deps) {
4491
+ return binding.source === "client-only" || Boolean(await deps.isClientModule?.(binding.source));
4492
+ }
4493
+ function parseHoistedRevalidateId(id) {
4494
+ const q = id.indexOf("?");
4495
+ if (q === -1) return null;
4496
+ const params = new URLSearchParams(id.slice(q + 1));
4497
+ return params.has(CLIENT_URLS_REVALIDATE_QUERY) ? id.slice(0, q) : null;
4498
+ }
4499
+ var HOISTED_ID_RE = new RegExp(
4500
+ `[?&]${CLIENT_URLS_REVALIDATE_QUERY}(?:[=&]|$)`
4501
+ );
4502
+ var SOURCE_ID_RE = /\.[cm]?[jt]sx?(?:\?|$)/;
4503
+ var NODE_MODULES_RE = /\/node_modules\//;
4504
+ function clientUrlsRevalidateHoistPlugin() {
4505
+ const emitted = /* @__PURE__ */ new Map();
4506
+ const clientModules = /* @__PURE__ */ new Map();
4507
+ let root = process.cwd();
4508
+ const isClientFile = (file) => {
4509
+ let known = clientModules.get(file);
4510
+ if (!known) {
4511
+ known = readSource(file).then(
4512
+ (text) => text !== null && text.includes("use client") && hasUseClientDirective(text)
4513
+ );
4514
+ clientModules.set(file, known);
4515
+ }
4516
+ return known;
4517
+ };
4518
+ return [
4519
+ {
4520
+ name: "@rangojs/router:client-urls-revalidate-hoist",
4521
+ enforce: "pre",
4522
+ configResolved(config) {
4523
+ root = config.root;
4524
+ },
4525
+ buildStart() {
4526
+ clientModules.clear();
4527
+ },
4528
+ watchChange(id) {
4529
+ const file = normalizePath4(id);
4530
+ clientModules.delete(file);
4531
+ emitted.delete(file);
4532
+ },
4533
+ load: {
4534
+ filter: { id: HOISTED_ID_RE },
4535
+ async handler(id) {
4536
+ const hoistedFile = parseHoistedRevalidateId(id);
4537
+ if (hoistedFile === null) return;
4538
+ const module = await loadEmittedOrDisk(
4539
+ emitted,
4540
+ hoistedFile,
4541
+ async (disk) => (await hoistClientUrlsRevalidates(disk, hoistedFile, { root }))?.module
4542
+ );
4543
+ return module ?? "export {};\n";
4544
+ }
4545
+ },
4546
+ transform: {
4547
+ filter: {
4548
+ id: {
4549
+ include: SOURCE_ID_RE,
4550
+ exclude: [NODE_MODULES_RE, HOISTED_ID_RE]
4551
+ },
4552
+ code: CLIENT_URLS_RE
4553
+ },
4554
+ async handler(code, id) {
4555
+ if (id.includes("/node_modules/") || parseHoistedRevalidateId(id)) {
4556
+ return;
4557
+ }
4558
+ const file = normalizePath4(id.split("?", 1)[0]);
4559
+ if (!SOURCE_FILE_RE.test(file)) return;
4560
+ const rsc = this.environment?.name === "rsc";
4561
+ if (rsc) emitted.delete(file);
4562
+ const result = await hoistClientUrlsRevalidates(code, id, {
4563
+ root,
4564
+ isClientModule: rsc ? async (source) => {
4565
+ const target = await resolveSourceTarget(this, source, id, {
4566
+ skipNodeModules: false
4567
+ });
4568
+ return target !== null && await isClientFile(target);
4569
+ } : void 0
4570
+ });
4571
+ if (!result) return;
4572
+ if (rsc) await recordEmitted(emitted, file, result.module);
4573
+ return { code: result.code, map: result.map };
4574
+ }
4575
+ }
4576
+ },
4577
+ {
4578
+ name: "@rangojs/router:client-urls-revalidate-register",
4579
+ enforce: "post",
4580
+ applyToEnvironment: (env) => env.name === "rsc",
4581
+ transform: {
4582
+ filter: { code: "registerClientReference" },
4583
+ handler(code, id) {
4584
+ if (parseHoistedRevalidateId(id)) return;
4585
+ const file = normalizePath4(id.split("?", 1)[0]);
4586
+ if (!emitted.has(file)) return;
4587
+ const s = new MagicString5(code);
4588
+ s.append(
4589
+ `
4590
+ import ${JSON.stringify(`${file}?${CLIENT_URLS_REVALIDATE_QUERY}`)};
4591
+ `
4592
+ );
4593
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
4594
+ }
4595
+ }
4596
+ }
4597
+ ];
4598
+ }
4599
+
4168
4600
  // src/vite/plugins/client-ref-dedup.ts
4169
4601
  init_debug();
4170
4602
  import { existsSync, readFileSync } from "node:fs";
4171
- import { isAbsolute, join, normalize, relative } from "node:path";
4603
+ import { isAbsolute, join, normalize, relative as relative2 } from "node:path";
4172
4604
  var debug4 = createRangoDebugger(NS.transform);
4173
4605
  var CLIENT_IN_SERVER_PROXY_PREFIX = "virtual:vite-rsc/client-in-server-package-proxy/";
4174
4606
  var DEDUP_PREFIX = "\0rango:dedup/";
@@ -4254,7 +4686,7 @@ function getPublicSpecifierCandidates(metadata, packageSpecifier, source) {
4254
4686
  return [];
4255
4687
  }
4256
4688
  const candidates = /* @__PURE__ */ new Set();
4257
- const sourceRelative = `./${relative(metadata.root, source).replaceAll("\\", "/")}`;
4689
+ const sourceRelative = `./${relative2(metadata.root, source).replaceAll("\\", "/")}`;
4258
4690
  if (sourceRelative.startsWith("./../") || isAbsolute(sourceRelative.slice(2))) {
4259
4691
  return [];
4260
4692
  }
@@ -4411,12 +4843,12 @@ function clientRefDedup() {
4411
4843
  }
4412
4844
 
4413
4845
  // src/vite/plugins/stable-rsc-output.ts
4414
- import { parseAst as parseAst4 } from "vite";
4846
+ import { parseAst as parseAst6 } from "vite";
4415
4847
 
4416
4848
  // src/vite/discovery/build-versions.ts
4417
- import { createHash as createHash3 } from "node:crypto";
4849
+ import { createHash as createHash4 } from "node:crypto";
4418
4850
  import { isBuiltin } from "node:module";
4419
- import { posix, relative as relative3 } from "node:path";
4851
+ import { posix, relative as relative4 } from "node:path";
4420
4852
 
4421
4853
  // src/router-versions.ts
4422
4854
  var DEFAULT_ROUTER_VERSIONS_KEY = "*";
@@ -4990,7 +5422,7 @@ function resolveRouterMembers(server, client, routers) {
4990
5422
  return members;
4991
5423
  }
4992
5424
  function sha256(value) {
4993
- return createHash3("sha256").update(value).digest("hex");
5425
+ return createHash4("sha256").update(value).digest("hex");
4994
5426
  }
4995
5427
  var baseName = posix.basename;
4996
5428
  function isAbsolutePath(path6) {
@@ -5011,7 +5443,7 @@ function portableModuleId(id, projectRoot) {
5011
5443
  return path6.slice(nodeModules + 1).replace(PNPM_PEER_SUFFIX, "$1");
5012
5444
  }
5013
5445
  if (isAbsolutePath(path6)) {
5014
- return relative3(projectRoot, path6).replaceAll("\\", "/");
5446
+ return relative4(projectRoot, path6).replaceAll("\\", "/");
5015
5447
  }
5016
5448
  return withoutWorkingDirectory(path6).replace(
5017
5449
  /([?&][^=&]+=)([^&]+)/g,
@@ -5372,7 +5804,7 @@ function literalProperties(object) {
5372
5804
  function defaultExportProperties(code) {
5373
5805
  let program;
5374
5806
  try {
5375
- program = parseAst4(code);
5807
+ program = parseAst6(code);
5376
5808
  } catch {
5377
5809
  return void 0;
5378
5810
  }
@@ -5428,7 +5860,7 @@ import { resolve } from "node:path";
5428
5860
  // package.json
5429
5861
  var package_default = {
5430
5862
  name: "@rangojs/router",
5431
- version: "0.22.1",
5863
+ version: "0.23.0",
5432
5864
  description: "Django-inspired RSC router with composable URL patterns",
5433
5865
  keywords: [
5434
5866
  "react",
@@ -5888,7 +6320,7 @@ declare global {
5888
6320
  }
5889
6321
 
5890
6322
  // src/build/route-types/scan-filter.ts
5891
- import { join as join2, relative as relative4 } from "node:path";
6323
+ import { isAbsolute as isAbsolute2, join as join2, relative as relative5 } from "node:path";
5892
6324
  import { readdirSync } from "node:fs";
5893
6325
  import picomatch from "picomatch";
5894
6326
  var DEFAULT_EXCLUDE_PATTERNS = [
@@ -5908,12 +6340,22 @@ function createScanFilter(root, opts) {
5908
6340
  const includeMatcher = hasInclude ? picomatch(include) : null;
5909
6341
  const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
5910
6342
  return (absolutePath) => {
5911
- const rel = relative4(root, absolutePath);
6343
+ const rel = relative5(root, absolutePath);
5912
6344
  if (excludeMatcher && excludeMatcher(rel)) return false;
5913
6345
  if (includeMatcher) return includeMatcher(rel);
5914
6346
  return true;
5915
6347
  };
5916
6348
  }
6349
+ function isUniversallySkippedScanDir(name) {
6350
+ return name === "node_modules" || name === "dist" || name === "coverage" || name.startsWith(".");
6351
+ }
6352
+ function isInSkippedScanDir(root, absolutePath) {
6353
+ const rel = relative5(root, absolutePath);
6354
+ if (isAbsolute2(rel)) return false;
6355
+ const dirs = rel.split(/[\\/]/).slice(0, -1);
6356
+ if (dirs[0] === "..") return false;
6357
+ return dirs.some(isUniversallySkippedScanDir);
6358
+ }
5917
6359
  function findTsFiles(dir, filter) {
5918
6360
  const results = [];
5919
6361
  let entries;
@@ -5928,8 +6370,9 @@ function findTsFiles(dir, filter) {
5928
6370
  for (const entry of entries) {
5929
6371
  const fullPath = join2(dir, entry.name);
5930
6372
  if (entry.isDirectory()) {
5931
- if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist" || entry.name === "build" || entry.name === "coverage")
6373
+ if (isUniversallySkippedScanDir(entry.name) || entry.name === "build") {
5932
6374
  continue;
6375
+ }
5933
6376
  results.push(...findTsFiles(fullPath, filter));
5934
6377
  } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
5935
6378
  if (filter && !filter(fullPath)) continue;
@@ -6513,7 +6956,7 @@ function isRoutableSourceFile(name) {
6513
6956
  return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
6514
6957
  }
6515
6958
  function isExcludedScanDir(name) {
6516
- return name === "node_modules" || name === "dist" || name === "coverage" || name === "__tests__" || name === "__mocks__" || name.startsWith(".");
6959
+ return isUniversallySkippedScanDir(name) || name === "__tests__" || name === "__mocks__";
6517
6960
  }
6518
6961
  function findCallSiteFilesRecursive(dir, pattern, patternG, stopAtMatchDir, filter, results) {
6519
6962
  let entries;
@@ -6896,28 +7339,7 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
6896
7339
  }
6897
7340
 
6898
7341
  // 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
7342
+ import { parseAst as parseAst7 } from "vite";
6921
7343
  function isCodeModule(id) {
6922
7344
  return /\.(tsx?|jsx?)($|\?)/.test(id);
6923
7345
  }
@@ -6925,10 +7347,10 @@ function normalizeModuleId(id) {
6925
7347
  return id.split("?", 1)[0];
6926
7348
  }
6927
7349
  function getClientModuleSignature(source) {
6928
- if (!hasUseClientDirective2(source)) return void 0;
7350
+ if (!hasUseClientDirective(source)) return void 0;
6929
7351
  let program;
6930
7352
  try {
6931
- program = parseAst6(source, { lang: "tsx" });
7353
+ program = parseAst7(source, { lang: "tsx" });
6932
7354
  } catch {
6933
7355
  return void 0;
6934
7356
  }
@@ -7103,7 +7525,7 @@ function isViteDepCachePath(filePath, cacheDir) {
7103
7525
 
7104
7526
  // src/vite/utils/shared-utils.ts
7105
7527
  import * as Vite from "vite";
7106
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
7528
+ import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
7107
7529
 
7108
7530
  // src/vite/plugins/performance-tracks.ts
7109
7531
  init_debug();
@@ -7190,7 +7612,7 @@ var sharedRolldownOptions = {
7190
7612
  function normalizeHostRouterEntry(rawInput, root, exists) {
7191
7613
  const raw = rawInput.replaceAll("\\", "/");
7192
7614
  const isRelative = raw.startsWith("./") || raw.startsWith("../");
7193
- const isRooted = raw.startsWith("/") || isAbsolute2(rawInput);
7615
+ const isRooted = raw.startsWith("/") || isAbsolute3(rawInput);
7194
7616
  if (isRooted) return raw;
7195
7617
  if (isRelative) {
7196
7618
  if (!exists(resolve4(root, raw))) {
@@ -7680,7 +8102,7 @@ function createCjsToEsmPlugin() {
7680
8102
  }
7681
8103
 
7682
8104
  // src/vite/router-discovery.ts
7683
- import { createServer as createViteServer } from "vite";
8105
+ import { createServer as createViteServer, normalizePath as normalizePath8 } from "vite";
7684
8106
  import { dirname as dirname7, join as join10, resolve as resolve13 } from "node:path";
7685
8107
  import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
7686
8108
  import { createRequire as createRequire3, register } from "node:module";
@@ -8073,10 +8495,10 @@ function evictHandlerCode(code, exports, fnName, brand) {
8073
8495
  }
8074
8496
 
8075
8497
  // src/vite/discovery/self-gen-tracking.ts
8076
- import { createHash as createHash4 } from "node:crypto";
8498
+ import { createHash as createHash5 } from "node:crypto";
8077
8499
  import { readFileSync as readFileSync4 } from "node:fs";
8078
8500
  function markSelfGenWrite(state, filePath, content) {
8079
- const hash = createHash4("sha256").update(content).digest("hex");
8501
+ const hash = createHash5("sha256").update(content).digest("hex");
8080
8502
  state.selfWrittenGenFiles.set(filePath, { at: Date.now(), hash });
8081
8503
  }
8082
8504
  function consumeSelfGenWrite(state, filePath) {
@@ -8094,7 +8516,7 @@ function checkSelfGenWrite(state, filePath, consume) {
8094
8516
  }
8095
8517
  try {
8096
8518
  const current = readFileSync4(filePath, "utf-8");
8097
- const currentHash = createHash4("sha256").update(current).digest("hex");
8519
+ const currentHash = createHash5("sha256").update(current).digest("hex");
8098
8520
  if (currentHash === info.hash) {
8099
8521
  if (consume) state.selfWrittenGenFiles.delete(filePath);
8100
8522
  return true;
@@ -8595,7 +9017,7 @@ function contextSet(variables, keyOrVar, value, options) {
8595
9017
  }
8596
9018
 
8597
9019
  // src/vite/utils/prerender-utils.ts
8598
- import { createHash as createHash5 } from "node:crypto";
9020
+ import { createHash as createHash6 } from "node:crypto";
8599
9021
  import {
8600
9022
  copyFileSync,
8601
9023
  existsSync as existsSync6,
@@ -8768,7 +9190,7 @@ function resetStagedBuildAssets(projectRoot) {
8768
9190
  }
8769
9191
  function writeBuildAssetModule(dir, prefix, exportValue) {
8770
9192
  mkdirSync(dir, { recursive: true });
8771
- const contentHash = createHash5("sha256").update(exportValue).digest("hex").slice(0, 8);
9193
+ const contentHash = createHash6("sha256").update(exportValue).digest("hex").slice(0, 8);
8772
9194
  const fileName = `${prefix}-${contentHash}.js`;
8773
9195
  const filePath = resolve7(dir, fileName);
8774
9196
  if (!existsSync6(filePath)) {
@@ -9303,8 +9725,8 @@ init_client_ref_hashing();
9303
9725
 
9304
9726
  // src/vite/discovery/client-urls-projection.ts
9305
9727
  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";
9728
+ import { isAbsolute as isAbsolute4, relative as relative6, resolve as resolve8 } from "node:path";
9729
+ import { normalizePath as normalizePath7, parseAst as parseAst8 } from "vite";
9308
9730
 
9309
9731
  // src/client-urls/server-projection.ts
9310
9732
  import * as React5 from "react";
@@ -9614,7 +10036,7 @@ import { createContext as createContext4 } from "react";
9614
10036
  var OutletContext = createContext4(null);
9615
10037
 
9616
10038
  // src/route-content-wrapper.tsx
9617
- import { Component, Suspense, use, useMemo as useMemo3 } from "react";
10039
+ import { Component, Suspense, use as use2, useMemo as useMemo3 } from "react";
9618
10040
 
9619
10041
  // src/outlet-provider.tsx
9620
10042
  import {
@@ -9666,6 +10088,15 @@ function getAppVersion() {
9666
10088
  init_validate_redirect_origin();
9667
10089
  init_redirect_origin();
9668
10090
 
10091
+ // src/browser/defer.ts
10092
+ import { use } from "react";
10093
+
10094
+ // src/thenable-status.ts
10095
+ var SETTLED_THENABLE = Object.assign(
10096
+ Promise.resolve(void 0),
10097
+ { status: "fulfilled", value: void 0, _debugInfo: [] }
10098
+ );
10099
+
9669
10100
  // src/route-content-wrapper.tsx
9670
10101
  import { jsx as jsx4 } from "react/jsx-runtime";
9671
10102
  import { createElement as createElement3 } from "react";
@@ -9674,7 +10105,7 @@ import { createElement as createElement3 } from "react";
9674
10105
  import {
9675
10106
  isValidElement,
9676
10107
  startTransition as startTransition2,
9677
- use as use2,
10108
+ use as use3,
9678
10109
  useCallback,
9679
10110
  useContext as useContext7,
9680
10111
  useEffect as useEffect5,
@@ -9684,12 +10115,6 @@ import {
9684
10115
  useState as useState3
9685
10116
  } from "react";
9686
10117
 
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
10118
  // src/loader-store.ts
9694
10119
  var EMPTY_SNAPSHOT = Object.freeze({
9695
10120
  value: void 0,
@@ -9751,7 +10176,7 @@ import {
9751
10176
  useCallback as useCallback4,
9752
10177
  useMemo as useMemo7,
9753
10178
  useRef as useRef11,
9754
- use as use3,
10179
+ use as use4,
9755
10180
  startTransition as startTransition5
9756
10181
  } from "react";
9757
10182
 
@@ -10395,6 +10820,9 @@ var THEME_READ = {
10395
10820
  }
10396
10821
  };
10397
10822
 
10823
+ // src/router/segment-resolution/prefetch-deferral.ts
10824
+ init_prefetch_markers();
10825
+
10398
10826
  // src/cache/invalidation-order.ts
10399
10827
  var order = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:tag-invalidation-order")] ??= { seq: 0, tags: /* @__PURE__ */ new Map(), forgotten: 0 };
10400
10828
  var responseStarts = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:response-execution-start")] ??= /* @__PURE__ */ new WeakMap();
@@ -10542,7 +10970,10 @@ function serializeTransition(route2) {
10542
10970
  }
10543
10971
  return Object.freeze(config);
10544
10972
  }
10545
- function serializeRoute(route2) {
10973
+ function indicesWhere(items, pred) {
10974
+ return items.flatMap((item, index) => pred(item) ? [index] : []);
10975
+ }
10976
+ function serializeRoute(route2, options) {
10546
10977
  const loaderIds = route2.loaders.map(({ loader: loader2 }, index) => {
10547
10978
  if (typeof loader2.$$id !== "string" || loader2.$$id.length === 0) {
10548
10979
  throw projectionError(
@@ -10552,7 +10983,25 @@ function serializeRoute(route2) {
10552
10983
  }
10553
10984
  return loader2.$$id;
10554
10985
  });
10555
- const awaitedLoaderIndices = route2.loaders.map(({ ssr }, index) => ssr === false ? index : -1).filter((index) => index >= 0);
10986
+ const loaderRevalidate = route2.loaders.map(
10987
+ ({ loader: loader2, revalidate: revalidate2 }) => revalidate2.flatMap((entry) => {
10988
+ if (isHoistedRevalidateRef(entry)) return [entry.$$rangoRevalidate];
10989
+ if (options.hoisted) {
10990
+ throw new Error(
10991
+ `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.`
10992
+ );
10993
+ }
10994
+ return [];
10995
+ })
10996
+ );
10997
+ const awaitedLoaderIndices = indicesWhere(
10998
+ route2.loaders,
10999
+ ({ ssr }) => ssr === false
11000
+ );
11001
+ const deferrableLoaderIndices = indicesWhere(
11002
+ route2.loaders,
11003
+ ({ prefetch }) => prefetch === false
11004
+ );
10556
11005
  const transition2 = serializeTransition(route2);
10557
11006
  return Object.freeze({
10558
11007
  id: route2.id,
@@ -10562,6 +11011,12 @@ function serializeRoute(route2) {
10562
11011
  loaderIds: Object.freeze(loaderIds),
10563
11012
  hasLoading: route2.loading !== void 0,
10564
11013
  ...awaitedLoaderIndices.length > 0 ? { awaitedLoaderIndices: Object.freeze(awaitedLoaderIndices) } : {},
11014
+ ...deferrableLoaderIndices.length > 0 ? { deferrableLoaderIndices: Object.freeze(deferrableLoaderIndices) } : {},
11015
+ ...loaderRevalidate.some((keys) => keys.length > 0) ? {
11016
+ loaderRevalidate: Object.freeze(
11017
+ loaderRevalidate.map((keys) => Object.freeze(keys))
11018
+ )
11019
+ } : {},
10565
11020
  ...transition2 ? { transition: transition2 } : {},
10566
11021
  ...typeof route2.transition?.when === "function" ? { transitionWhen: true } : {}
10567
11022
  });
@@ -10582,15 +11037,24 @@ function serializeIntercept(record, index) {
10582
11037
  hasLoading: record.loading !== void 0
10583
11038
  });
10584
11039
  }
10585
- function serializeClientUrlPatterns(patterns) {
11040
+ function serializeClientUrlPatterns(patterns, options = {}) {
10586
11041
  return Object.freeze({
10587
11042
  version: 1,
10588
- routes: Object.freeze(patterns.routes.map(serializeRoute)),
11043
+ routes: Object.freeze(
11044
+ patterns.routes.map((route2) => serializeRoute(route2, options))
11045
+ ),
10589
11046
  intercepts: Object.freeze(
10590
11047
  (patterns.intercepts ?? []).map(serializeIntercept)
10591
11048
  )
10592
11049
  });
10593
11050
  }
11051
+ function projectClientUrlsModule(namespace, options = {}) {
11052
+ const definition = namespace.default;
11053
+ if (typeof definition !== "object" && typeof definition !== "function" || definition === null || definition.__brand !== "client-urls") {
11054
+ return void 0;
11055
+ }
11056
+ return serializeClientUrlPatterns(definition, options);
11057
+ }
10594
11058
 
10595
11059
  // src/vite/discovery/client-urls-projection.ts
10596
11060
  init_client_ref_hashing();
@@ -10599,7 +11063,7 @@ var debug10 = createRangoDebugger(NS.discovery);
10599
11063
  function hasDefaultExport(code) {
10600
11064
  let program;
10601
11065
  try {
10602
- program = parseAst7(code, { lang: "tsx" });
11066
+ program = parseAst8(code, { lang: "tsx" });
10603
11067
  } catch {
10604
11068
  return false;
10605
11069
  }
@@ -10615,20 +11079,20 @@ function hasDefaultExport(code) {
10615
11079
  }
10616
11080
  function normalizeSource(state, id) {
10617
11081
  const cleanId = id.split("?", 1)[0];
10618
- const absoluteId = isAbsolute3(cleanId) ? cleanId : resolve8(state.projectRoot, cleanId);
10619
- return normalizePath5(absoluteId);
11082
+ const absoluteId = isAbsolute4(cleanId) ? cleanId : resolve8(state.projectRoot, cleanId);
11083
+ return normalizePath7(absoluteId);
10620
11084
  }
10621
11085
  function referenceId(referenceKey) {
10622
11086
  return `${referenceKey}#default`;
10623
11087
  }
10624
11088
  function recordClientUrlsModule(state, code, id) {
10625
- if (!code.includes("clientUrls(") || !hasUseClientDirective2(code) || !hasDefaultExport(code)) {
11089
+ if (!code.includes("clientUrls(") || !hasUseClientDirective(code) || !hasDefaultExport(code)) {
10626
11090
  return;
10627
11091
  }
10628
11092
  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);
11093
+ const projectRoot = normalizePath7(resolve8(state.projectRoot));
11094
+ const relativeId = normalizePath7(relative6(projectRoot, source));
11095
+ const outsideProject = relativeId === ".." || relativeId.startsWith("../") || isAbsolute4(relativeId);
10632
11096
  const devReferenceKey = outsideProject ? `/@fs${source}` : `/${relativeId}`;
10633
11097
  const sourceByReferenceId = state.clientUrlSourceByReferenceId ??= /* @__PURE__ */ new Map();
10634
11098
  sourceByReferenceId.set(referenceId(devReferenceKey), source);
@@ -10672,18 +11136,19 @@ async function refreshRecordedClientUrlProjections(state, ssrEnv, serverMod) {
10672
11136
  }
10673
11137
  }
10674
11138
  for (const [source, referenceIds] of referenceIdsBySource) {
10675
- let definition;
11139
+ let module;
10676
11140
  try {
10677
- definition = (await ssrEnv.runner.import(source)).default;
11141
+ module = await ssrEnv.runner.import(source);
10678
11142
  } catch {
10679
11143
  continue;
10680
11144
  }
10681
- if (typeof definition !== "object" && typeof definition !== "function" || definition === null || definition.__brand !== "client-urls") {
11145
+ let projection;
11146
+ try {
11147
+ projection = projectClientUrlsModule(module, { hoisted: true });
11148
+ } catch {
10682
11149
  continue;
10683
11150
  }
10684
- const projection = serializeClientUrlPatterns(
10685
- definition
10686
- );
11151
+ if (!projection) continue;
10687
11152
  debug10?.(
10688
11153
  "pre-entry refresh %s -> [%s]",
10689
11154
  source,
@@ -10738,17 +11203,24 @@ async function discoverClientUrlProjections(state, ssrEnv, serverMod) {
10738
11203
  cause
10739
11204
  );
10740
11205
  }
10741
- const definition = module.default;
10742
- 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 (cause) {
11210
+ throw projectionError2(
11211
+ referenceIds[0],
11212
+ source,
11213
+ cause instanceof Error ? cause.message : String(cause),
11214
+ cause
11215
+ );
11216
+ }
11217
+ if (!projection) {
10743
11218
  throw projectionError2(
10744
11219
  referenceIds[0],
10745
11220
  source,
10746
11221
  'the module default export must be created by clientUrls() and have __brand === "client-urls"'
10747
11222
  );
10748
11223
  }
10749
- const projection = serializeClientUrlPatterns(
10750
- definition
10751
- );
10752
11224
  for (const referenceId2 of referenceIds) {
10753
11225
  nextProjections.set(referenceId2, projection);
10754
11226
  }
@@ -10767,13 +11239,51 @@ async function discoverClientUrlProjections(state, ssrEnv, serverMod) {
10767
11239
 
10768
11240
  // src/vite/discovery/discover-routers.ts
10769
11241
  var debug11 = createRangoDebugger(NS.discovery);
11242
+ var SERVER_MODULE_ID = "@rangojs/router/server";
11243
+ var MAX_DISCOVERY_PASSES = 3;
11244
+ var RUNNER_RELOADED = /* @__PURE__ */ Symbol("rango.discovery.runnerReloaded");
11245
+ async function runnerReloadedSince(rscEnv, serverMod) {
11246
+ try {
11247
+ return await rscEnv.runner.import(SERVER_MODULE_ID) !== serverMod;
11248
+ } catch {
11249
+ return false;
11250
+ }
11251
+ }
10770
11252
  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
- );
11253
+ const entryPath = state.resolvedEntryPath;
11254
+ if (!entryPath) return;
11255
+ for (let pass = 1; ; pass++) {
11256
+ const serverMod = await timed(
11257
+ debug11,
11258
+ "inner: import @rangojs/router/server",
11259
+ () => rscEnv.runner.import(SERVER_MODULE_ID)
11260
+ );
11261
+ let result;
11262
+ try {
11263
+ result = await discoverRoutersPass(
11264
+ state,
11265
+ rscEnv,
11266
+ ssrEnv,
11267
+ entryPath,
11268
+ serverMod
11269
+ );
11270
+ } catch (error) {
11271
+ if (!await runnerReloadedSince(rscEnv, serverMod)) throw error;
11272
+ result = RUNNER_RELOADED;
11273
+ }
11274
+ if (result !== RUNNER_RELOADED) return result;
11275
+ if (pass === MAX_DISCOVERY_PASSES) {
11276
+ throw new Error(
11277
+ `[rango] Route discovery did not settle: the RSC module runner reloaded during ${MAX_DISCOVERY_PASSES} consecutive discovery passes.`
11278
+ );
11279
+ }
11280
+ debug11?.(
11281
+ "inner: module runner reloaded mid-pass, redoing discovery (pass %d)",
11282
+ pass + 1
11283
+ );
11284
+ }
11285
+ }
11286
+ async function discoverRoutersPass(state, rscEnv, ssrEnv, entryPath, serverMod) {
10777
11287
  await timed(
10778
11288
  debug11,
10779
11289
  "inner: refresh recorded client URL projections",
@@ -10782,9 +11292,10 @@ async function discoverRouters(state, rscEnv, ssrEnv) {
10782
11292
  await timed(
10783
11293
  debug11,
10784
11294
  "inner: import entry",
10785
- () => rscEnv.runner.import(state.resolvedEntryPath)
11295
+ () => rscEnv.runner.import(entryPath)
10786
11296
  );
10787
- let registry = serverMod.RouterRegistry;
11297
+ if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
11298
+ const registry = serverMod.RouterRegistry;
10788
11299
  if (!registry || registry.size === 0) {
10789
11300
  const discoveryErrors = [];
10790
11301
  try {
@@ -10798,20 +11309,15 @@ async function discoverRouters(state, rscEnv, ssrEnv) {
10798
11309
  for (const { context, error } of handlerErrors) {
10799
11310
  debug11?.("caught error while resolving %s: %O", context, error);
10800
11311
  }
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;
11312
+ if (await runnerReloadedSince(rscEnv, serverMod)) {
11313
+ return RUNNER_RELOADED;
10808
11314
  }
10809
11315
  }
10810
11316
  } catch (error) {
10811
11317
  discoveryErrors.push({ context: "host-router discovery", error });
10812
11318
  }
10813
11319
  if (!registry || registry.size === 0) {
10814
- throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
11320
+ throw new DiscoveryError(entryPath, discoveryErrors);
10815
11321
  }
10816
11322
  }
10817
11323
  await timed(
@@ -10960,6 +11466,7 @@ async function discoverRouters(state, rscEnv, ssrEnv) {
10960
11466
  }
10961
11467
  }
10962
11468
  }
11469
+ if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
10963
11470
  state.mergedRouteManifest = newMergedRouteManifest;
10964
11471
  state.perRouterManifests = newPerRouterManifests;
10965
11472
  state.perRouterManifestDataMap = newPerRouterManifestDataMap;
@@ -10997,7 +11504,7 @@ import { dirname as dirname4, join as join6, resolve as resolve9 } from "node:pa
10997
11504
  init_debug();
10998
11505
 
10999
11506
  // src/vite/discovery/installed-packages.ts
11000
- import { createHash as createHash6 } from "node:crypto";
11507
+ import { createHash as createHash7 } from "node:crypto";
11001
11508
  import {
11002
11509
  existsSync as existsSync8,
11003
11510
  readdirSync as readdirSync3,
@@ -11014,7 +11521,7 @@ function linkTarget(path6) {
11014
11521
  }
11015
11522
  }
11016
11523
  function digestDirectory(dir) {
11017
- const hash = createHash6("sha256");
11524
+ const hash = createHash7("sha256");
11018
11525
  const entered = /* @__PURE__ */ new Set();
11019
11526
  const visit = (current, prefix) => {
11020
11527
  const real = realpathSync(current);
@@ -11727,6 +12234,17 @@ function devDiscoveryBootstrap(state) {
11727
12234
  const epoch = state.devDiscoveryEpoch;
11728
12235
  return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
11729
12236
  }
12237
+ function clientUrlProjectionReplay(state) {
12238
+ const projections = [...state.clientUrlProjectionMap ?? []].sort(
12239
+ ([a], [b]) => compareStrings(a, b)
12240
+ );
12241
+ return [
12242
+ `clearClientUrlProjections();`,
12243
+ ...projections.map(
12244
+ ([referenceId2, projection]) => `setClientUrlProjection(${JSON.stringify(referenceId2)}, ${jsonParseExpression(projection)});`
12245
+ )
12246
+ ];
12247
+ }
11730
12248
  function generateRoutesManifestModule(state) {
11731
12249
  const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
11732
12250
  const hasClientUrlModules = Boolean(state.clientUrlSourceByReferenceId?.size);
@@ -11773,17 +12291,7 @@ function generateRoutesManifestModule(state) {
11773
12291
  `clearAllRouterData();`,
11774
12292
  TRANSITION_WHEN_VALIDATION
11775
12293
  ];
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
- }
12294
+ if (hasClientUrlModules) lines2.push(...clientUrlProjectionReplay(state));
11787
12295
  if (varIdx > 0) {
11788
12296
  lines2.push(
11789
12297
  `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 +12333,10 @@ function generateRoutesManifestModule(state) {
11825
12333
  return lines2.join("\n");
11826
12334
  }
11827
12335
  const lines = [
11828
- `import { ${hasClientUrlModules ? "clearClientUrlProjections, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
12336
+ `import { ${hasClientUrlModules ? "clearClientUrlProjections, setClientUrlProjection, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
11829
12337
  TRANSITION_WHEN_VALIDATION
11830
12338
  ];
11831
- if (hasClientUrlModules) {
11832
- lines.push(`clearClientUrlProjections();`);
11833
- }
12339
+ if (hasClientUrlModules) lines.push(...clientUrlProjectionReplay(state));
11834
12340
  if (!state.isBuildMode) {
11835
12341
  const origin = state.devServerOrigin || state.devServer?.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || state.devServer && `http://localhost:${state.devServer.config.server.port || 5173}`;
11836
12342
  if (origin) {
@@ -12169,6 +12675,12 @@ var debugDiscovery = createRangoDebugger(NS.discovery);
12169
12675
  var debugRoutes = createRangoDebugger(NS.routes);
12170
12676
  var debugBuild = createRangoDebugger(NS.build);
12171
12677
  var debugDev = createRangoDebugger(NS.dev);
12678
+ function modulesForFile(graph, file) {
12679
+ return graph.getModulesByFile(normalizePath8(file));
12680
+ }
12681
+ function isFileOutsideApp(root, file, rscGraph) {
12682
+ return isInSkippedScanDir(root, file) && !(rscGraph && modulesForFile(rscGraph, file)?.size);
12683
+ }
12172
12684
  var loaderHookRegistered = false;
12173
12685
  function ensureCloudflareProtocolLoaderRegistered() {
12174
12686
  if (loaderHookRegistered) return;
@@ -12261,6 +12773,10 @@ async function createTempRscServer(state, options = {}) {
12261
12773
  // Inline transition({ when }) literals become client references here
12262
12774
  // too, or discovery would see (and reject) the plain function.
12263
12775
  transitionWhenHoistPlugin(),
12776
+ // clientUrls() revalidate() predicates move to the server here too:
12777
+ // discovery serializes the SSR copy's markers and the RSC realm
12778
+ // registers the predicates the materialized stubs call.
12779
+ ...clientUrlsRevalidateHoistPlugin(),
12264
12780
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
12265
12781
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
12266
12782
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : void 0),
@@ -12761,6 +13277,10 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
12761
13277
  () => propagateDiscoveryState(rscEnv)
12762
13278
  );
12763
13279
  } catch (err) {
13280
+ s.lastDiscoveryError = {
13281
+ message: err?.message ?? String(err),
13282
+ at: Date.now()
13283
+ };
12764
13284
  emitDiscoveryFailure(
12765
13285
  err,
12766
13286
  optimizerHashBefore,
@@ -13117,6 +13637,11 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13117
13637
  });
13118
13638
  if (opts?.staticRouteTypesGeneration !== false) {
13119
13639
  const isGeneratedRouteFile = (filePath) => filePath.endsWith(".gen.ts") && (filePath.includes("named-routes.gen.ts") || filePath.includes("urls.gen.ts"));
13640
+ const isOutsideApp = (filePath) => isFileOutsideApp(
13641
+ s.projectRoot,
13642
+ filePath,
13643
+ server.environments?.rsc?.moduleGraph
13644
+ );
13120
13645
  const regenerateGeneratedRouteFiles = () => {
13121
13646
  if (s.perRouterManifests.length > 0) {
13122
13647
  writeRouteTypesFiles(s);
@@ -13148,6 +13673,15 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13148
13673
  return true;
13149
13674
  };
13150
13675
  let routeChangeTimer;
13676
+ const invalidateRoutesManifestModule = (rscEnv) => {
13677
+ for (const virtualId of [
13678
+ VIRTUAL_ROUTES_MANIFEST_ID,
13679
+ `\0${VIRTUAL_ROUTES_MANIFEST_ID}`
13680
+ ]) {
13681
+ const virtualMod = rscEnv.moduleGraph?.getModuleById?.(virtualId);
13682
+ if (virtualMod) rscEnv.moduleGraph.invalidateModule(virtualMod);
13683
+ }
13684
+ };
13151
13685
  const refreshRuntimeDiscovery = async () => {
13152
13686
  const rscEnv = server.environments?.rsc;
13153
13687
  const hasMainRunner = !!rscEnv?.runner;
@@ -13157,6 +13691,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13157
13691
  const hmrStart = performance.now();
13158
13692
  try {
13159
13693
  if (hasMainRunner) {
13694
+ const recoveringColdStart = s.perRouterManifests.length === 0;
13160
13695
  await timed(
13161
13696
  debugDiscovery,
13162
13697
  "hmr discoverRouters",
@@ -13172,6 +13707,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13172
13707
  "hmr propagateDiscoveryState",
13173
13708
  () => propagateDiscoveryState(rscEnv)
13174
13709
  );
13710
+ if (recoveringColdStart) invalidateRoutesManifestModule(rscEnv);
13175
13711
  } else {
13176
13712
  const tempRscEnv = await timed(
13177
13713
  debugDiscovery,
@@ -13317,15 +13853,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13317
13853
  server.environments?.ssr,
13318
13854
  serverMod
13319
13855
  );
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
- }
13856
+ invalidateRoutesManifestModule(rscEnv);
13329
13857
  } catch (err) {
13330
13858
  debugDiscovery?.(
13331
13859
  "watcher: clientUrls projection pre-refresh failed: %s",
@@ -13351,7 +13879,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13351
13879
  "watcher: regenerated gen files (%sms)",
13352
13880
  (performance.now() - regenStart).toFixed(1)
13353
13881
  );
13354
- if (s.perRouterManifests.length > 0) {
13882
+ if (canRefreshRuntimeDiscovery(s, !!rscEnv?.runner)) {
13355
13883
  refreshRuntimeDiscovery().catch((err) => {
13356
13884
  console.warn(
13357
13885
  `[rango] Runtime re-discovery error: ${err.message}`
@@ -13362,6 +13890,13 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13362
13890
  }, 100);
13363
13891
  };
13364
13892
  const handleRouteFileChange = (filePath) => {
13893
+ if (isOutsideApp(filePath)) {
13894
+ debugDiscovery?.(
13895
+ "watcher: skip %s (outside the discovery scan)",
13896
+ filePath
13897
+ );
13898
+ return;
13899
+ }
13365
13900
  if (maybeHandleGeneratedRouteFileMutation(filePath)) return;
13366
13901
  if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx") && !filePath.endsWith(".js") && !filePath.endsWith(".jsx")) {
13367
13902
  if (s.lastDiscoveryError) {
@@ -13436,9 +13971,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13436
13971
  }
13437
13972
  const seen = /* @__PURE__ */ new Set();
13438
13973
  for (const file of routerSourceFiles) {
13439
- const mods = rscGraph.getModulesByFile(
13440
- file.replaceAll("\\", "/")
13441
- );
13974
+ const mods = modulesForFile(rscGraph, file);
13442
13975
  if (!mods) {
13443
13976
  debugDiscovery?.(
13444
13977
  "watcher: clientUrls invalidation found no rsc modules for %s",
@@ -13470,7 +14003,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13470
14003
  }
13471
14004
  s.cachedRouterFiles = void 0;
13472
14005
  }
13473
- if (s.perRouterManifests.length > 0) {
14006
+ if (canRefreshRuntimeDiscovery(s, !!mainRscEnv?.runner)) {
13474
14007
  gate.noteRouteEvent();
13475
14008
  }
13476
14009
  scheduleRouteRegeneration();
@@ -13488,7 +14021,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
13488
14021
  server.watcher.on("add", handleRouteFileChange);
13489
14022
  server.watcher.on("change", handleRouteFileChange);
13490
14023
  server.watcher.on("unlink", (filePath) => {
13491
- if (!isGeneratedRouteFile(filePath)) return;
14024
+ if (!isGeneratedRouteFile(filePath) || isOutsideApp(filePath)) return;
13492
14025
  const hasRunner = !!server.environments?.rsc?.runner;
13493
14026
  if (!hasRunner) return;
13494
14027
  regenerateGeneratedRouteFiles();
@@ -14166,7 +14699,7 @@ If this is a multi-app host router, export a createHostRouter() instance and set
14166
14699
  return;
14167
14700
  try {
14168
14701
  const source = readFileSync12(file, "utf-8");
14169
- if (hasUseClientDirective2(source)) {
14702
+ if (hasUseClientDirective(source)) {
14170
14703
  return [];
14171
14704
  }
14172
14705
  } catch {
@@ -14175,6 +14708,7 @@ If this is a multi-app host router, export a createHostRouter() instance and set
14175
14708
  });
14176
14709
  plugins.push(createLoaderDirectiveGuardPlugin());
14177
14710
  plugins.push(transitionWhenHoistPlugin());
14711
+ plugins.push(...clientUrlsRevalidateHoistPlugin());
14178
14712
  plugins.push(createLoaderScanStubPlugin());
14179
14713
  plugins.push(exposeActionId());
14180
14714
  plugins.push(