@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
package/src/vite/rango.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from "./plugins/expose-internal-ids.js";
15
15
  import { useCacheTransform } from "./plugins/use-cache-transform.js";
16
16
  import { transitionWhenHoistPlugin } from "./plugins/transition-when-hoist.js";
17
+ import { clientUrlsRevalidateHoistPlugin } from "./plugins/client-urls-revalidate-hoist.js";
17
18
  import { clientRefDedup } from "./plugins/client-ref-dedup.js";
18
19
  import { stableRscOutput } from "./plugins/stable-rsc-output.js";
19
20
  import { VIRTUAL_IDS } from "./plugins/virtual-entries.js";
@@ -592,6 +593,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
592
593
 
593
594
  plugins.push(createLoaderDirectiveGuardPlugin());
594
595
  plugins.push(transitionWhenHoistPlugin());
596
+ plugins.push(...clientUrlsRevalidateHoistPlugin());
595
597
  plugins.push(createLoaderScanStubPlugin());
596
598
  plugins.push(exposeActionId());
597
599
  plugins.push(
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Plugin, ViteDevServer } from "vite";
10
- import { createServer as createViteServer } from "vite";
10
+ import { createServer as createViteServer, normalizePath } from "vite";
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { createRequire, register } from "node:module";
@@ -17,6 +17,7 @@ import {
17
17
  findNestedRouterConflict,
18
18
  findRouterFiles,
19
19
  createScanFilter,
20
+ isInSkippedScanDir,
20
21
  } from "../build/generate-route-types.js";
21
22
  import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
22
23
  import {
@@ -50,6 +51,7 @@ import { hashServerRefs } from "./plugins/server-ref-hashing.js";
50
51
  import { defineEncryptionKeyExpr } from "./encryption-key.js";
51
52
  import { extractHandlerExportsFromChunk } from "./utils/bundle-analysis.js";
52
53
  import {
54
+ canRefreshRuntimeDiscovery,
53
55
  createDiscoveryState,
54
56
  VIRTUAL_ROUTES_MANIFEST_ID,
55
57
  type DiscoveryState,
@@ -75,6 +77,7 @@ import {
75
77
  import { describeDiscoveryFailure } from "./discovery/discovery-errors.js";
76
78
  import { findTransitionWhenError } from "../transition-when-ref.js";
77
79
  import { transitionWhenHoistPlugin } from "./plugins/transition-when-hoist.js";
80
+ import { clientUrlsRevalidateHoistPlugin } from "./plugins/client-urls-revalidate-hoist.js";
78
81
  import {
79
82
  createDevPrerenderCache,
80
83
  devPrerenderCacheKey,
@@ -106,6 +109,35 @@ const debugDev = createRangoDebugger(NS.dev);
106
109
 
107
110
  export { VIRTUAL_ROUTES_MANIFEST_ID };
108
111
 
112
+ /** The slice of a Vite environment module graph the route-file watcher reads. */
113
+ interface ModulesByFileGraph {
114
+ getModulesByFile(file: string): ReadonlySet<unknown> | undefined;
115
+ }
116
+
117
+ /** Modules a Vite graph holds for `file`; Vite keys its file index by normalized path. */
118
+ function modulesForFile(
119
+ graph: ModulesByFileGraph,
120
+ file: string,
121
+ ): ReadonlySet<unknown> | undefined {
122
+ return graph.getModulesByFile(normalizePath(file));
123
+ }
124
+
125
+ /**
126
+ * Whether the dev route-file watcher ignores `file`: it sits in a directory
127
+ * every discovery scan skips (isInSkippedScanDir) and the RSC graph has not
128
+ * loaded it. See docs/manifests.md, "A reload no file of the app asked for".
129
+ */
130
+ export function isFileOutsideApp(
131
+ root: string,
132
+ file: string,
133
+ rscGraph: ModulesByFileGraph | undefined,
134
+ ): boolean {
135
+ return (
136
+ isInSkippedScanDir(root, file) &&
137
+ !(rscGraph && modulesForFile(rscGraph, file)?.size)
138
+ );
139
+ }
140
+
109
141
  // ============================================================================
110
142
  // Node ESM Loader Hook Registration
111
143
  // ============================================================================
@@ -299,6 +331,10 @@ export async function createTempRscServer(
299
331
  // Inline transition({ when }) literals become client references here
300
332
  // too, or discovery would see (and reject) the plain function.
301
333
  transitionWhenHoistPlugin(),
334
+ // clientUrls() revalidate() predicates move to the server here too:
335
+ // discovery serializes the SSR copy's markers and the RSC realm
336
+ // registers the predicates the materialized stubs call.
337
+ ...clientUrlsRevalidateHoistPlugin(),
302
338
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
303
339
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
304
340
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
@@ -1066,20 +1102,17 @@ export function createRouterDiscoveryPlugin(
1066
1102
  // Discover routers FIRST, then arm the manifest-readiness gate on the
1067
1103
  // server module discovery actually read the registry from.
1068
1104
  //
1069
- // We deliberately do NOT pre-import "@rangojs/router/server" before the
1070
- // entry to arm the gate early. During a Vite dependency re-optimization
1071
- // (dev boot after a lockfile change, or `vite dev --force`), a module
1072
- // imported here before the entry resolves to the pre-optimize copy of
1073
- // the runner's module graph, while discoverRouters' entry import — which
1074
- // awaits the in-flight re-optimization — resolves to the post-optimize
1075
- // copy. createRouter() then populates RouterRegistry on the fresh copy,
1076
- // but a stale pre-imported "@rangojs/router/server" reads the other
1077
- // copy's empty Map and discovery throws a spurious "No routers found"
1078
- // even though the app is configured correctly. discoverRouters imports
1079
- // the entry first and reads the registry off the same instance, keeping
1080
- // read and write on one copy. The virtual manifest module's own gate
1081
- // (s.discoveryDone, armed by beginDiscoveryGate) already blocks early
1082
- // requests during discovery on the Node path, so arming
1105
+ // We deliberately do NOT pre-import "@rangojs/router/server" here to
1106
+ // arm the gate early. During a Vite dependency re-optimization (dev
1107
+ // boot after a lockfile change, or `vite dev --force`) or any runner
1108
+ // full reload, a module imported before the entry can be a stale copy
1109
+ // of the runner's module graph: createRouter() populates RouterRegistry
1110
+ // on the fresh copy and the stale one reads an empty Map, a spurious
1111
+ // "No routers found" (#637). discoverRouters returns the instance it
1112
+ // read the registry from and verified the runner still serves (it
1113
+ // redoes a pass that a reload split). The virtual manifest module's
1114
+ // own gate (s.discoveryDone, armed by beginDiscoveryGate) already
1115
+ // blocks early requests during discovery on the Node path, so arming
1083
1116
  // manifestReadyPromise after discovery is sufficient here.
1084
1117
  const serverMod = await timed(debugDiscovery, "discoverRouters", () =>
1085
1118
  discoverRouters(s, rscEnv, (server.environments as any)?.ssr),
@@ -1105,6 +1138,12 @@ export function createRouterDiscoveryPlugin(
1105
1138
  propagateDiscoveryState(rscEnv),
1106
1139
  );
1107
1140
  } catch (err: any) {
1141
+ // Recovery mode: the next in-scan source change re-runs discovery
1142
+ // (canRefreshRuntimeDiscovery) instead of waiting for a restart.
1143
+ s.lastDiscoveryError = {
1144
+ message: err?.message ?? String(err),
1145
+ at: Date.now(),
1146
+ };
1108
1147
  emitDiscoveryFailure(
1109
1148
  err,
1110
1149
  optimizerHashBefore,
@@ -1630,6 +1669,15 @@ export function createRouterDiscoveryPlugin(
1630
1669
  (filePath.includes("named-routes.gen.ts") ||
1631
1670
  filePath.includes("urls.gen.ts"));
1632
1671
 
1672
+ // Files no discovery scan reaches are ignored unless the RSC graph
1673
+ // loaded them (docs/manifests.md, "A reload no file of the app asked for").
1674
+ const isOutsideApp = (filePath: string): boolean =>
1675
+ isFileOutsideApp(
1676
+ s.projectRoot,
1677
+ filePath,
1678
+ (server.environments as any)?.rsc?.moduleGraph,
1679
+ );
1680
+
1633
1681
  const regenerateGeneratedRouteFiles = () => {
1634
1682
  if (s.perRouterManifests.length > 0) {
1635
1683
  writeRouteTypesFiles(s);
@@ -1676,6 +1724,18 @@ export function createRouterDiscoveryPlugin(
1676
1724
  // only the expensive regeneration is debounced.
1677
1725
  let routeChangeTimer: ReturnType<typeof setTimeout> | undefined;
1678
1726
 
1727
+ // Drop the routes-manifest virtual module's cached transform so the
1728
+ // next runner evaluation regenerates it from current state.
1729
+ const invalidateRoutesManifestModule = (rscEnv: any) => {
1730
+ for (const virtualId of [
1731
+ VIRTUAL_ROUTES_MANIFEST_ID,
1732
+ `\0${VIRTUAL_ROUTES_MANIFEST_ID}`,
1733
+ ]) {
1734
+ const virtualMod = rscEnv.moduleGraph?.getModuleById?.(virtualId);
1735
+ if (virtualMod) rscEnv.moduleGraph.invalidateModule(virtualMod);
1736
+ }
1737
+ };
1738
+
1679
1739
  // Re-run runtime discovery so factory-generated routes that the
1680
1740
  // static parser cannot see are refreshed after source changes.
1681
1741
  // The state-machine concerns (queued/pending/gatePending) are
@@ -1696,6 +1756,7 @@ export function createRouterDiscoveryPlugin(
1696
1756
  const hmrStart = performance.now();
1697
1757
  try {
1698
1758
  if (hasMainRunner) {
1759
+ const recoveringColdStart = s.perRouterManifests.length === 0;
1699
1760
  await timed(debugDiscovery, "hmr discoverRouters", () =>
1700
1761
  discoverRouters(s, rscEnv, (server.environments as any)?.ssr),
1701
1762
  );
@@ -1705,6 +1766,9 @@ export function createRouterDiscoveryPlugin(
1705
1766
  await timed(debugDiscovery, "hmr propagateDiscoveryState", () =>
1706
1767
  propagateDiscoveryState(rscEnv),
1707
1768
  );
1769
+ // The manifest module emitted after a failed cold discovery has
1770
+ // no gen-file import, so no gen write ever invalidates it.
1771
+ if (recoveringColdStart) invalidateRoutesManifestModule(rscEnv);
1708
1772
  } else {
1709
1773
  // Cloudflare HMR: invalidate the temp server's RSC module
1710
1774
  // graph (or close+recreate as a fallback) so the runner
@@ -1924,16 +1988,7 @@ export function createRouterDiscoveryPlugin(
1924
1988
  // stale replay after propagateDiscoveryState would clobber
1925
1989
  // the refreshed registry as the realm's last write and the
1926
1990
  // next router evaluation would materialize the old mount.
1927
- for (const virtualId of [
1928
- VIRTUAL_ROUTES_MANIFEST_ID,
1929
- `\0${VIRTUAL_ROUTES_MANIFEST_ID}`,
1930
- ]) {
1931
- const virtualMod =
1932
- rscEnv.moduleGraph?.getModuleById?.(virtualId);
1933
- if (virtualMod) {
1934
- rscEnv.moduleGraph.invalidateModule(virtualMod);
1935
- }
1936
- }
1991
+ invalidateRoutesManifestModule(rscEnv);
1937
1992
  } catch (err: any) {
1938
1993
  debugDiscovery?.(
1939
1994
  "watcher: clientUrls projection pre-refresh failed: %s",
@@ -1970,7 +2025,7 @@ export function createRouterDiscoveryPlugin(
1970
2025
  // Async: re-run runtime discovery to refresh factory-generated
1971
2026
  // routes that the static parser cannot resolve. Resolves the
1972
2027
  // discovery gate when complete.
1973
- if (s.perRouterManifests.length > 0) {
2028
+ if (canRefreshRuntimeDiscovery(s, !!rscEnv?.runner)) {
1974
2029
  // The cloudflare workerd reload fires inside refreshRuntimeDiscovery
1975
2030
  // on the successful cycle (see forceCloudflareWorkerReload call
1976
2031
  // there) so queued follow-up cycles also trigger it.
@@ -1987,6 +2042,13 @@ export function createRouterDiscoveryPlugin(
1987
2042
  };
1988
2043
 
1989
2044
  const handleRouteFileChange = (filePath: string) => {
2045
+ if (isOutsideApp(filePath)) {
2046
+ debugDiscovery?.(
2047
+ "watcher: skip %s (outside the discovery scan)",
2048
+ filePath,
2049
+ );
2050
+ return;
2051
+ }
1990
2052
  if (maybeHandleGeneratedRouteFileMutation(filePath)) return;
1991
2053
  if (
1992
2054
  !filePath.endsWith(".ts") &&
@@ -2124,9 +2186,7 @@ export function createRouterDiscoveryPlugin(
2124
2186
  }
2125
2187
  const seen = new Set<any>();
2126
2188
  for (const file of routerSourceFiles) {
2127
- const mods = rscGraph.getModulesByFile(
2128
- file.replaceAll("\\", "/"),
2129
- );
2189
+ const mods = modulesForFile(rscGraph, file);
2130
2190
  if (!mods) {
2131
2191
  debugDiscovery?.(
2132
2192
  "watcher: clientUrls invalidation found no rsc modules for %s",
@@ -2166,8 +2226,9 @@ export function createRouterDiscoveryPlugin(
2166
2226
  // and resets `discoveryDone` to a fresh pending promise (so
2167
2227
  // workerd reloads triggered by the same source change can't
2168
2228
  // observe a stale resolved gate from cold-start). Resolved
2169
- // by the trailing refreshRuntimeDiscovery() cycle.
2170
- if (s.perRouterManifests.length > 0) {
2229
+ // by the trailing refreshRuntimeDiscovery() cycle, which runs
2230
+ // under the same predicate.
2231
+ if (canRefreshRuntimeDiscovery(s, !!mainRscEnv?.runner)) {
2171
2232
  gate.noteRouteEvent();
2172
2233
  }
2173
2234
  scheduleRouteRegeneration();
@@ -2194,7 +2255,7 @@ export function createRouterDiscoveryPlugin(
2194
2255
  // Same no-runner guard as change/add: stale perRouterManifests would
2195
2256
  // reintroduce reverted content.
2196
2257
  server.watcher.on("unlink", (filePath) => {
2197
- if (!isGeneratedRouteFile(filePath)) return;
2258
+ if (!isGeneratedRouteFile(filePath) || isOutsideApp(filePath)) return;
2198
2259
  const hasRunner = !!(server.environments as any)?.rsc?.runner;
2199
2260
  if (!hasRunner) return;
2200
2261
  regenerateGeneratedRouteFiles();
@@ -1,33 +0,0 @@
1
- /**
2
- * The clientUrls revalidate() chain evaluator, shared by the browser
3
- * collector (navigation.ts) and the public testing primitive
4
- * (testing/run-client-revalidate.ts) so the two can never drift.
5
- *
6
- * Semantics mirror the server's evaluateRevalidation
7
- * (src/router/revalidation.ts): a boolean verdict is a hard decision and
8
- * short-circuits the rest of the chain; a `{ defaultShouldRevalidate }`
9
- * object updates the running suggestion, which later predicates receive as
10
- * their `defaultShouldRevalidate`; null/undefined defers; a throwing
11
- * predicate fails open to the current suggestion (logged). One deliberate
12
- * divergence: the object form is accepted only with a boolean value — the
13
- * server is laxer, but never re-compares the value, while this decision
14
- * feeds a strict-equality delta gate and the wire encoding
15
- * (navigation.ts), where a truthy non-boolean would invert intent.
16
- */
17
- import type { ClientRevalidateArgs, ClientRevalidateFn } from "./types.js";
18
- /**
19
- * The locked default the server will apply to the request these decisions
20
- * ride on. `actionRequest` is about the REQUEST, not the user gesture: only
21
- * the action POST itself is evaluated server-side with actionContext
22
- * (default `true`); the follow-up refetch GETs an action can trigger carry
23
- * no actionContext and get navigation defaults — even though their
24
- * predicates still see `isAction()` as true.
25
- */
26
- export declare function lockedClientDefault(options: {
27
- actionRequest: boolean;
28
- currentParams: Record<string, string>;
29
- nextParams: Record<string, string>;
30
- currentUrl: URL;
31
- nextUrl: URL;
32
- }): boolean;
33
- export declare function runClientRevalidateChain(fns: readonly ClientRevalidateFn[], baseArgs: Omit<ClientRevalidateArgs, "defaultShouldRevalidate">, lockedDefault: boolean, label: string): boolean;
@@ -1,25 +0,0 @@
1
- /**
2
- * Wire protocol for CLIENT-RUN per-loader revalidation decisions.
3
- *
4
- * clientUrls() revalidate() predicates are declared in a "use client" module,
5
- * so they cannot cross the projection boundary as functions — instead they
6
- * EXECUTE in the browser and only their DECISIONS cross: the browser attaches
7
- * this header to partial-navigation and action requests, and the synthesized
8
- * per-loader revalidate() on every materialized loader stub reads it back
9
- * (see materializeRouteItems in server-projection.ts).
10
- *
11
- * Trust model: same class as `_rsc_segments` — a decision can only make the
12
- * CLIENT's own view staler (skip) or fresher (force), and the synthesized
13
- * predicates exist only on client-urls loader stubs, so the header can never
14
- * influence server-tree loaders. Requests without the header (no-JS, PE,
15
- * prefetch, document loads) fall back to the locked server defaults.
16
- */
17
- export declare const CLIENT_REVALIDATION_HEADER = "X-Rango-Client-Reval";
18
- export interface ClientRevalidationDecisions {
19
- /** Loader $$ids whose client predicate said false (keep held data). */
20
- readonly skip: readonly string[];
21
- /** Loader $$ids whose client predicate said true against a false default. */
22
- readonly force: readonly string[];
23
- }
24
- export declare function encodeClientRevalidationDecisions(decisions: ClientRevalidationDecisions): string | null;
25
- export declare function decodeClientRevalidationDecisions(headerValue: string | null): ClientRevalidationDecisions | null;
@@ -1,43 +0,0 @@
1
- /**
2
- * runClientRevalidate — unit-test clientUrls() revalidate() predicates.
3
- *
4
- * Builds the same {@link ClientRevalidateArgs} the browser collector passes
5
- * and evaluates the predicate(s) through the SAME chain evaluator production
6
- * uses (client-urls/revalidate-chain.ts) — locked default, boolean
7
- * short-circuit, soft-verdict threading, and fail-open are the production
8
- * code paths, not a re-implementation. Pass an array to test a chain.
9
- *
10
- * Synchronous: client revalidate functions must be sync.
11
- */
12
- import type { ClientRevalidateFn } from "../client-urls/types.js";
13
- /**
14
- * Options for {@link runClientRevalidate}. Defaults model a same-URL
15
- * navigation with no action (locked default `false`).
16
- */
17
- export interface RunClientRevalidateOptions {
18
- currentUrl?: string | URL;
19
- nextUrl?: string | URL;
20
- currentParams?: Record<string, string>;
21
- nextParams?: Record<string, string>;
22
- stale?: boolean;
23
- /**
24
- * The triggering action: a single imported reference (id resolved via
25
- * `$id ?? $$id`; throws if the function carries neither) or a raw actionId
26
- * string. A namespace/object is rejected — it cannot identify the ONE
27
- * action that triggered the request. Omit for a plain navigation.
28
- */
29
- action?: ((...args: never[]) => unknown) | string;
30
- /**
31
- * Model an action-triggered refetch GET: predicates see `isAction()` as
32
- * true, but the locked default stays the navigation default, matching how
33
- * the server evaluates that request (no actionContext). Defaults to
34
- * treating a provided `action` as the action POST itself.
35
- */
36
- actionRequest?: boolean;
37
- }
38
- /**
39
- * Run one clientUrls `revalidate()` predicate — or a chain, in declaration
40
- * order — against production-built args. Returns the final boolean decision
41
- * (locked default if every predicate defers or throws).
42
- */
43
- export declare function runClientRevalidate(fn: ClientRevalidateFn | readonly ClientRevalidateFn[], opts?: RunClientRevalidateOptions): boolean;
@@ -1,83 +0,0 @@
1
- /**
2
- * The clientUrls revalidate() chain evaluator, shared by the browser
3
- * collector (navigation.ts) and the public testing primitive
4
- * (testing/run-client-revalidate.ts) so the two can never drift.
5
- *
6
- * Semantics mirror the server's evaluateRevalidation
7
- * (src/router/revalidation.ts): a boolean verdict is a hard decision and
8
- * short-circuits the rest of the chain; a `{ defaultShouldRevalidate }`
9
- * object updates the running suggestion, which later predicates receive as
10
- * their `defaultShouldRevalidate`; null/undefined defers; a throwing
11
- * predicate fails open to the current suggestion (logged). One deliberate
12
- * divergence: the object form is accepted only with a boolean value — the
13
- * server is laxer, but never re-compares the value, while this decision
14
- * feeds a strict-equality delta gate and the wire encoding
15
- * (navigation.ts), where a truthy non-boolean would invert intent.
16
- */
17
-
18
- import { paramsEqual } from "../router/params-util.js";
19
- import type { ClientRevalidateArgs, ClientRevalidateFn } from "./types.js";
20
-
21
- /**
22
- * The locked default the server will apply to the request these decisions
23
- * ride on. `actionRequest` is about the REQUEST, not the user gesture: only
24
- * the action POST itself is evaluated server-side with actionContext
25
- * (default `true`); the follow-up refetch GETs an action can trigger carry
26
- * no actionContext and get navigation defaults — even though their
27
- * predicates still see `isAction()` as true.
28
- */
29
- export function lockedClientDefault(options: {
30
- actionRequest: boolean;
31
- currentParams: Record<string, string>;
32
- nextParams: Record<string, string>;
33
- currentUrl: URL;
34
- nextUrl: URL;
35
- }): boolean {
36
- if (options.actionRequest) return true;
37
- return (
38
- !paramsEqual(options.currentParams, options.nextParams) ||
39
- options.currentUrl.search !== options.nextUrl.search
40
- );
41
- }
42
-
43
- export function runClientRevalidateChain(
44
- fns: readonly ClientRevalidateFn[],
45
- baseArgs: Omit<ClientRevalidateArgs, "defaultShouldRevalidate">,
46
- lockedDefault: boolean,
47
- label: string,
48
- ): boolean {
49
- let suggestion = lockedDefault;
50
- for (const fn of fns) {
51
- let verdict: ReturnType<ClientRevalidateFn>;
52
- try {
53
- verdict = fn({ ...baseArgs, defaultShouldRevalidate: suggestion });
54
- } catch (error) {
55
- console.error(
56
- `[@rangojs/router] clientUrls revalidate() threw for ${label}; using default decision:`,
57
- error,
58
- );
59
- continue;
60
- }
61
- if (
62
- process.env.NODE_ENV !== "production" &&
63
- verdict != null &&
64
- typeof (verdict as { then?: unknown }).then === "function"
65
- ) {
66
- console.warn(
67
- `[rango] clientUrls revalidate() for ${label} returned a Promise; ` +
68
- `predicates must be synchronous (return a boolean, ` +
69
- `{ defaultShouldRevalidate }, or null/undefined). The async result ` +
70
- `was IGNORED and the default (${suggestion}) was kept.`,
71
- );
72
- }
73
- if (typeof verdict === "boolean") return verdict;
74
- if (
75
- verdict &&
76
- typeof verdict === "object" &&
77
- typeof verdict.defaultShouldRevalidate === "boolean"
78
- ) {
79
- suggestion = verdict.defaultShouldRevalidate;
80
- }
81
- }
82
- return suggestion;
83
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Wire protocol for CLIENT-RUN per-loader revalidation decisions.
3
- *
4
- * clientUrls() revalidate() predicates are declared in a "use client" module,
5
- * so they cannot cross the projection boundary as functions — instead they
6
- * EXECUTE in the browser and only their DECISIONS cross: the browser attaches
7
- * this header to partial-navigation and action requests, and the synthesized
8
- * per-loader revalidate() on every materialized loader stub reads it back
9
- * (see materializeRouteItems in server-projection.ts).
10
- *
11
- * Trust model: same class as `_rsc_segments` — a decision can only make the
12
- * CLIENT's own view staler (skip) or fresher (force), and the synthesized
13
- * predicates exist only on client-urls loader stubs, so the header can never
14
- * influence server-tree loaders. Requests without the header (no-JS, PE,
15
- * prefetch, document loads) fall back to the locked server defaults.
16
- */
17
-
18
- export const CLIENT_REVALIDATION_HEADER = "X-Rango-Client-Reval";
19
-
20
- export interface ClientRevalidationDecisions {
21
- /** Loader $$ids whose client predicate said false (keep held data). */
22
- readonly skip: readonly string[];
23
- /** Loader $$ids whose client predicate said true against a false default. */
24
- readonly force: readonly string[];
25
- }
26
-
27
- export function encodeClientRevalidationDecisions(
28
- decisions: ClientRevalidationDecisions,
29
- ): string | null {
30
- if (decisions.skip.length === 0 && decisions.force.length === 0) return null;
31
- // encodeURIComponent keeps the header value ISO-8859-1-safe regardless of
32
- // what characters loader module ids contain.
33
- return encodeURIComponent(
34
- JSON.stringify({ skip: decisions.skip, force: decisions.force }),
35
- );
36
- }
37
-
38
- export function decodeClientRevalidationDecisions(
39
- headerValue: string | null,
40
- ): ClientRevalidationDecisions | null {
41
- if (!headerValue) return null;
42
- try {
43
- const parsed = JSON.parse(decodeURIComponent(headerValue)) as {
44
- skip?: unknown;
45
- force?: unknown;
46
- };
47
- const toIds = (value: unknown): readonly string[] =>
48
- Array.isArray(value)
49
- ? value.filter((entry): entry is string => typeof entry === "string")
50
- : [];
51
- return { skip: toIds(parsed.skip), force: toIds(parsed.force) };
52
- } catch {
53
- // A malformed header is treated as absent: locked defaults apply.
54
- return null;
55
- }
56
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * runClientRevalidate — unit-test clientUrls() revalidate() predicates.
3
- *
4
- * Builds the same {@link ClientRevalidateArgs} the browser collector passes
5
- * and evaluates the predicate(s) through the SAME chain evaluator production
6
- * uses (client-urls/revalidate-chain.ts) — locked default, boolean
7
- * short-circuit, soft-verdict threading, and fail-open are the production
8
- * code paths, not a re-implementation. Pass an array to test a chain.
9
- *
10
- * Synchronous: client revalidate functions must be sync.
11
- */
12
-
13
- import { makeIsAction } from "../router/is-action.js";
14
- import { resolveTestActionId } from "./internal/action-id.js";
15
- import {
16
- lockedClientDefault,
17
- runClientRevalidateChain,
18
- } from "../client-urls/revalidate-chain.js";
19
- import { toURL } from "./to-url.js";
20
- import type {
21
- ClientRevalidateArgs,
22
- ClientRevalidateFn,
23
- } from "../client-urls/types.js";
24
-
25
- const DEFAULT_URL = "http://localhost/";
26
-
27
- /**
28
- * Options for {@link runClientRevalidate}. Defaults model a same-URL
29
- * navigation with no action (locked default `false`).
30
- */
31
- export interface RunClientRevalidateOptions {
32
- currentUrl?: string | URL;
33
- nextUrl?: string | URL;
34
- currentParams?: Record<string, string>;
35
- nextParams?: Record<string, string>;
36
- stale?: boolean;
37
- /**
38
- * The triggering action: a single imported reference (id resolved via
39
- * `$id ?? $$id`; throws if the function carries neither) or a raw actionId
40
- * string. A namespace/object is rejected — it cannot identify the ONE
41
- * action that triggered the request. Omit for a plain navigation.
42
- */
43
- action?: ((...args: never[]) => unknown) | string;
44
- /**
45
- * Model an action-triggered refetch GET: predicates see `isAction()` as
46
- * true, but the locked default stays the navigation default, matching how
47
- * the server evaluates that request (no actionContext). Defaults to
48
- * treating a provided `action` as the action POST itself.
49
- */
50
- actionRequest?: boolean;
51
- }
52
-
53
- /**
54
- * Run one clientUrls `revalidate()` predicate — or a chain, in declaration
55
- * order — against production-built args. Returns the final boolean decision
56
- * (locked default if every predicate defers or throws).
57
- */
58
- export function runClientRevalidate(
59
- fn: ClientRevalidateFn | readonly ClientRevalidateFn[],
60
- opts: RunClientRevalidateOptions = {},
61
- ): boolean {
62
- const currentUrl = toURL(opts.currentUrl, new URL(DEFAULT_URL));
63
- const nextUrl = toURL(opts.nextUrl, currentUrl);
64
- const currentParams = opts.currentParams ?? {};
65
- const nextParams = opts.nextParams ?? currentParams;
66
- const inAction = opts.action !== undefined;
67
- const actionId = resolveTestActionId(opts.action, "runClientRevalidate");
68
- const defaultShouldRevalidate = lockedClientDefault({
69
- actionRequest: opts.actionRequest ?? inAction,
70
- currentParams,
71
- nextParams,
72
- currentUrl,
73
- nextUrl,
74
- });
75
-
76
- const baseArgs: Omit<ClientRevalidateArgs, "defaultShouldRevalidate"> = {
77
- currentUrl,
78
- nextUrl,
79
- currentParams,
80
- nextParams,
81
- stale: opts.stale ?? false,
82
- isAction: makeIsAction(actionId, inAction),
83
- ...(actionId !== undefined ? { actionId } : {}),
84
- };
85
-
86
- return runClientRevalidateChain(
87
- Array.isArray(fn) ? fn : [fn],
88
- baseArgs,
89
- defaultShouldRevalidate,
90
- "runClientRevalidate predicate",
91
- );
92
- }