@rangojs/router 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
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,7 +51,9 @@ 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,
56
+ recordDiscoveryFailure,
54
57
  VIRTUAL_ROUTES_MANIFEST_ID,
55
58
  type DiscoveryState,
56
59
  type PluginOptions,
@@ -75,6 +78,7 @@ import {
75
78
  import { describeDiscoveryFailure } from "./discovery/discovery-errors.js";
76
79
  import { findTransitionWhenError } from "../transition-when-ref.js";
77
80
  import { transitionWhenHoistPlugin } from "./plugins/transition-when-hoist.js";
81
+ import { clientUrlsRevalidateHoistPlugin } from "./plugins/client-urls-revalidate-hoist.js";
78
82
  import {
79
83
  createDevPrerenderCache,
80
84
  devPrerenderCacheKey,
@@ -106,6 +110,35 @@ const debugDev = createRangoDebugger(NS.dev);
106
110
 
107
111
  export { VIRTUAL_ROUTES_MANIFEST_ID };
108
112
 
113
+ /** The slice of a Vite environment module graph the route-file watcher reads. */
114
+ interface ModulesByFileGraph {
115
+ getModulesByFile(file: string): ReadonlySet<unknown> | undefined;
116
+ }
117
+
118
+ /** Modules a Vite graph holds for `file`; Vite keys its file index by normalized path. */
119
+ function modulesForFile(
120
+ graph: ModulesByFileGraph,
121
+ file: string,
122
+ ): ReadonlySet<unknown> | undefined {
123
+ return graph.getModulesByFile(normalizePath(file));
124
+ }
125
+
126
+ /**
127
+ * Whether the dev route-file watcher ignores `file`: it sits in a directory
128
+ * every discovery scan skips (isInSkippedScanDir) and the RSC graph has not
129
+ * loaded it. See docs/manifests.md, "A reload no file of the app asked for".
130
+ */
131
+ export function isFileOutsideApp(
132
+ root: string,
133
+ file: string,
134
+ rscGraph: ModulesByFileGraph | undefined,
135
+ ): boolean {
136
+ return (
137
+ isInSkippedScanDir(root, file) &&
138
+ !(rscGraph && modulesForFile(rscGraph, file)?.size)
139
+ );
140
+ }
141
+
109
142
  // ============================================================================
110
143
  // Node ESM Loader Hook Registration
111
144
  // ============================================================================
@@ -299,6 +332,10 @@ export async function createTempRscServer(
299
332
  // Inline transition({ when }) literals become client references here
300
333
  // too, or discovery would see (and reject) the plain function.
301
334
  transitionWhenHoistPlugin(),
335
+ // clientUrls() revalidate() predicates move to the server here too:
336
+ // discovery serializes the SSR copy's markers and the RSC realm
337
+ // registers the predicates the materialized stubs call.
338
+ ...clientUrlsRevalidateHoistPlugin(),
302
339
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
303
340
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
304
341
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
@@ -1014,12 +1051,20 @@ export function createRouterDiscoveryPlugin(
1014
1051
  acquireBuildEnv(s, viteCommand, viteMode),
1015
1052
  );
1016
1053
 
1017
- tempRscEnv = (
1018
- await timed(debugDiscovery, "getOrCreateTempServer", () =>
1019
- getOrCreateTempServer(),
1020
- )
1021
- ).env;
1022
- if (tempRscEnv) {
1054
+ const temp = await timed(
1055
+ debugDiscovery,
1056
+ "getOrCreateTempServer",
1057
+ () => getOrCreateTempServer(),
1058
+ );
1059
+ tempRscEnv = temp.env;
1060
+ if (!tempRscEnv) {
1061
+ // Recovery mode, as in the catch below. getOrCreateTempServer
1062
+ // already logged why the temp runner failed.
1063
+ recordDiscoveryFailure(
1064
+ s,
1065
+ temp.error ?? "temp runner unavailable for discovery",
1066
+ );
1067
+ } else {
1023
1068
  optimizerHashBefore =
1024
1069
  tempRscEnv.depsOptimizer?.metadata?.browserHash;
1025
1070
  await timed(debugDiscovery, "discoverRouters (cloudflare)", () =>
@@ -1034,6 +1079,7 @@ export function createRouterDiscoveryPlugin(
1034
1079
  );
1035
1080
  }
1036
1081
  } catch (err: any) {
1082
+ recordDiscoveryFailure(s, err);
1037
1083
  emitDiscoveryFailure(
1038
1084
  err,
1039
1085
  optimizerHashBefore,
@@ -1066,20 +1112,17 @@ export function createRouterDiscoveryPlugin(
1066
1112
  // Discover routers FIRST, then arm the manifest-readiness gate on the
1067
1113
  // server module discovery actually read the registry from.
1068
1114
  //
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
1115
+ // We deliberately do NOT pre-import "@rangojs/router/server" here to
1116
+ // arm the gate early. During a Vite dependency re-optimization (dev
1117
+ // boot after a lockfile change, or `vite dev --force`) or any runner
1118
+ // full reload, a module imported before the entry can be a stale copy
1119
+ // of the runner's module graph: createRouter() populates RouterRegistry
1120
+ // on the fresh copy and the stale one reads an empty Map, a spurious
1121
+ // "No routers found" (#637). discoverRouters returns the instance it
1122
+ // read the registry from and verified the runner still serves (it
1123
+ // redoes a pass that a reload split). The virtual manifest module's
1124
+ // own gate (s.discoveryDone, armed by beginDiscoveryGate) already
1125
+ // blocks early requests during discovery on the Node path, so arming
1083
1126
  // manifestReadyPromise after discovery is sufficient here.
1084
1127
  const serverMod = await timed(debugDiscovery, "discoverRouters", () =>
1085
1128
  discoverRouters(s, rscEnv, (server.environments as any)?.ssr),
@@ -1105,6 +1148,9 @@ export function createRouterDiscoveryPlugin(
1105
1148
  propagateDiscoveryState(rscEnv),
1106
1149
  );
1107
1150
  } catch (err: any) {
1151
+ // Recovery mode: the next in-scan source change re-runs discovery
1152
+ // (canRefreshRuntimeDiscovery) instead of waiting for a restart.
1153
+ recordDiscoveryFailure(s, err);
1108
1154
  emitDiscoveryFailure(
1109
1155
  err,
1110
1156
  optimizerHashBefore,
@@ -1630,6 +1676,15 @@ export function createRouterDiscoveryPlugin(
1630
1676
  (filePath.includes("named-routes.gen.ts") ||
1631
1677
  filePath.includes("urls.gen.ts"));
1632
1678
 
1679
+ // Files no discovery scan reaches are ignored unless the RSC graph
1680
+ // loaded them (docs/manifests.md, "A reload no file of the app asked for").
1681
+ const isOutsideApp = (filePath: string): boolean =>
1682
+ isFileOutsideApp(
1683
+ s.projectRoot,
1684
+ filePath,
1685
+ (server.environments as any)?.rsc?.moduleGraph,
1686
+ );
1687
+
1633
1688
  const regenerateGeneratedRouteFiles = () => {
1634
1689
  if (s.perRouterManifests.length > 0) {
1635
1690
  writeRouteTypesFiles(s);
@@ -1676,6 +1731,18 @@ export function createRouterDiscoveryPlugin(
1676
1731
  // only the expensive regeneration is debounced.
1677
1732
  let routeChangeTimer: ReturnType<typeof setTimeout> | undefined;
1678
1733
 
1734
+ // Drop the routes-manifest virtual module's cached transform so the
1735
+ // next runner evaluation regenerates it from current state.
1736
+ const invalidateRoutesManifestModule = (rscEnv: any) => {
1737
+ for (const virtualId of [
1738
+ VIRTUAL_ROUTES_MANIFEST_ID,
1739
+ `\0${VIRTUAL_ROUTES_MANIFEST_ID}`,
1740
+ ]) {
1741
+ const virtualMod = rscEnv.moduleGraph?.getModuleById?.(virtualId);
1742
+ if (virtualMod) rscEnv.moduleGraph.invalidateModule(virtualMod);
1743
+ }
1744
+ };
1745
+
1679
1746
  // Re-run runtime discovery so factory-generated routes that the
1680
1747
  // static parser cannot see are refreshed after source changes.
1681
1748
  // The state-machine concerns (queued/pending/gatePending) are
@@ -1684,18 +1751,12 @@ export function createRouterDiscoveryPlugin(
1684
1751
  const refreshRuntimeDiscovery = async () => {
1685
1752
  const rscEnv = (server.environments as any)?.rsc;
1686
1753
  const hasMainRunner = !!rscEnv?.runner;
1687
- // Cloudflare HMR has no main RSC runner (workerd is a separate
1688
- // runtime). When we have a populated runtime manifest from cold
1689
- // start, we can re-discover via the temp Node runner — the same
1690
- // mechanism getOrCreateTempServer() uses at startup. Without a
1691
- // populated manifest there's nothing useful to do, so bail
1692
- // before involving the gate machine at all.
1693
- if (!hasMainRunner && s.perRouterManifests.length === 0) return;
1694
1754
  let previousRouteShape = routeShapeSignature();
1695
1755
  await gate.runRefreshCycle(async () => {
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,12 +1766,20 @@ 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
- // Cloudflare HMR: invalidate the temp server's RSC module
1710
- // graph (or close+recreate as a fallback) so the runner
1711
- // re-reads the freshly edited source. Keeping the same
1773
+ // Cloudflare HMR has no main RSC runner (workerd is a separate
1774
+ // runtime): re-discover on the temp Node runner. Invalidate its
1775
+ // RSC module graph (or close+recreate as a fallback) so the
1776
+ // runner re-reads the freshly edited source. Keeping the same
1712
1777
  // Vite instance alive avoids disrupting workerd's transport
1713
- // during the first post-cold-start module-fetch window.
1778
+ // during the first post-cold-start module-fetch window. After
1779
+ // a cold start whose temp runner failed there is none, and
1780
+ // getOrCreateTempServer creates one. A recovering cycle needs
1781
+ // no routes-manifest invalidation here: the worker reload below
1782
+ // invalidates its whole module graph.
1714
1783
  const tempRscEnv = await timed(
1715
1784
  debugDiscovery,
1716
1785
  "hmr refreshTempRscEnv (cloudflare)",
@@ -1766,10 +1835,7 @@ export function createRouterDiscoveryPlugin(
1766
1835
  forceCloudflareWorkerReload(rscEnv, expectedEpoch);
1767
1836
  }
1768
1837
  } catch (err: any) {
1769
- s.lastDiscoveryError = {
1770
- message: err?.message ?? String(err),
1771
- at: Date.now(),
1772
- };
1838
+ recordDiscoveryFailure(s, err);
1773
1839
  // An invalid transition({ when }) is a definition error the
1774
1840
  // user must see: terminal error plus the browser's Vite overlay,
1775
1841
  // not a warning while the last-good route tree keeps serving.
@@ -1924,16 +1990,7 @@ export function createRouterDiscoveryPlugin(
1924
1990
  // stale replay after propagateDiscoveryState would clobber
1925
1991
  // the refreshed registry as the realm's last write and the
1926
1992
  // 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
- }
1993
+ invalidateRoutesManifestModule(rscEnv);
1937
1994
  } catch (err: any) {
1938
1995
  debugDiscovery?.(
1939
1996
  "watcher: clientUrls projection pre-refresh failed: %s",
@@ -1970,7 +2027,7 @@ export function createRouterDiscoveryPlugin(
1970
2027
  // Async: re-run runtime discovery to refresh factory-generated
1971
2028
  // routes that the static parser cannot resolve. Resolves the
1972
2029
  // discovery gate when complete.
1973
- if (s.perRouterManifests.length > 0) {
2030
+ if (canRefreshRuntimeDiscovery(s)) {
1974
2031
  // The cloudflare workerd reload fires inside refreshRuntimeDiscovery
1975
2032
  // on the successful cycle (see forceCloudflareWorkerReload call
1976
2033
  // there) so queued follow-up cycles also trigger it.
@@ -1987,6 +2044,13 @@ export function createRouterDiscoveryPlugin(
1987
2044
  };
1988
2045
 
1989
2046
  const handleRouteFileChange = (filePath: string) => {
2047
+ if (isOutsideApp(filePath)) {
2048
+ debugDiscovery?.(
2049
+ "watcher: skip %s (outside the discovery scan)",
2050
+ filePath,
2051
+ );
2052
+ return;
2053
+ }
1990
2054
  if (maybeHandleGeneratedRouteFileMutation(filePath)) return;
1991
2055
  if (
1992
2056
  !filePath.endsWith(".ts") &&
@@ -2124,9 +2188,7 @@ export function createRouterDiscoveryPlugin(
2124
2188
  }
2125
2189
  const seen = new Set<any>();
2126
2190
  for (const file of routerSourceFiles) {
2127
- const mods = rscGraph.getModulesByFile(
2128
- file.replaceAll("\\", "/"),
2129
- );
2191
+ const mods = modulesForFile(rscGraph, file);
2130
2192
  if (!mods) {
2131
2193
  debugDiscovery?.(
2132
2194
  "watcher: clientUrls invalidation found no rsc modules for %s",
@@ -2166,8 +2228,9 @@ export function createRouterDiscoveryPlugin(
2166
2228
  // and resets `discoveryDone` to a fresh pending promise (so
2167
2229
  // workerd reloads triggered by the same source change can't
2168
2230
  // observe a stale resolved gate from cold-start). Resolved
2169
- // by the trailing refreshRuntimeDiscovery() cycle.
2170
- if (s.perRouterManifests.length > 0) {
2231
+ // by the trailing refreshRuntimeDiscovery() cycle, which runs
2232
+ // under the same predicate.
2233
+ if (canRefreshRuntimeDiscovery(s)) {
2171
2234
  gate.noteRouteEvent();
2172
2235
  }
2173
2236
  scheduleRouteRegeneration();
@@ -2194,7 +2257,7 @@ export function createRouterDiscoveryPlugin(
2194
2257
  // Same no-runner guard as change/add: stale perRouterManifests would
2195
2258
  // reintroduce reverted content.
2196
2259
  server.watcher.on("unlink", (filePath) => {
2197
- if (!isGeneratedRouteFile(filePath)) return;
2260
+ if (!isGeneratedRouteFile(filePath) || isOutsideApp(filePath)) return;
2198
2261
  const hasRunner = !!(server.environments as any)?.rsc?.runner;
2199
2262
  if (!hasRunner) return;
2200
2263
  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
- }