@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
@@ -42,6 +42,30 @@ import {
42
42
 
43
43
  const debug = createRangoDebugger(NS.discovery);
44
44
 
45
+ const SERVER_MODULE_ID = "@rangojs/router/server";
46
+
47
+ /** Passes discoverRouters runs before giving up on a runner that keeps reloading. */
48
+ const MAX_DISCOVERY_PASSES = 3;
49
+
50
+ /** A discovery pass result: the runner reloaded mid-pass, redo it. */
51
+ const RUNNER_RELOADED = Symbol("rango.discovery.runnerReloaded");
52
+
53
+ /**
54
+ * Whether the runner's `@rangojs/router/server` is no longer `serverMod`. A
55
+ * Vite module runner full reload clears its evaluated-module cache, so the
56
+ * next import evaluates a fresh copy with empty module-level registries.
57
+ */
58
+ async function runnerReloadedSince(
59
+ rscEnv: any,
60
+ serverMod: unknown,
61
+ ): Promise<boolean> {
62
+ try {
63
+ return (await rscEnv.runner.import(SERVER_MODULE_ID)) !== serverMod;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
45
69
  /**
46
70
  * Import the user's entry via RSC runner, generate manifests for each
47
71
  * discovered router, build route tries, and optionally run prerender
@@ -49,25 +73,70 @@ const debug = createRangoDebugger(NS.discovery);
49
73
  *
50
74
  * Returns the imported `@rangojs/router/server` module so the caller
51
75
  * can access the RouterRegistry and manifest setters.
76
+ *
77
+ * A pass reads the router registry and installs clientUrls projections on ONE
78
+ * `@rangojs/router/server` instance, and checks that the runner still serves
79
+ * it. A runner full reload during the pass (a watched-file change at boot)
80
+ * splits the realm: a router module bound to the old instance registers there
81
+ * while urls modules re-evaluate against a fresh one, so include() looks its
82
+ * projection up in the fresh, empty Map ("include() could not resolve the
83
+ * server projection"). Such a pass is redone, at most
84
+ * MAX_DISCOVERY_PASSES times; the redo evaluates the entry in one realm.
52
85
  */
53
86
  export async function discoverRouters(
54
87
  state: DiscoveryState,
55
88
  rscEnv: any,
56
89
  ssrEnv?: any,
57
90
  ): Promise<any> {
58
- if (!state.resolvedEntryPath) return;
59
-
60
- // Import the router package FIRST (leaf module, no side effects on user
61
- // code) so recorded clientUrls projections can be refreshed BEFORE the entry
62
- // import: routers re-created by that import call `.routes(reference)` at
63
- // module scope and materialize whatever projection is installed at that
64
- // moment. On HMR re-discovery that must be the CURRENT module contents, not
65
- // the previous pass's snapshot (see refreshRecordedClientUrlProjections).
66
- const serverMod = await timed(
67
- debug,
68
- "inner: import @rangojs/router/server",
69
- () => rscEnv.runner.import("@rangojs/router/server"),
70
- );
91
+ const entryPath = state.resolvedEntryPath;
92
+ if (!entryPath) return;
93
+
94
+ for (let pass = 1; ; pass++) {
95
+ // Imported BEFORE the entry so recorded clientUrls projections can be
96
+ // refreshed first: routers re-created by the entry import materialize
97
+ // whatever projection is installed at that moment, which on HMR
98
+ // re-discovery must be the CURRENT module contents, not the previous
99
+ // pass's snapshot (see refreshRecordedClientUrlProjections). It is also
100
+ // the realm baseline of the reload checks; #637 read the registry off a
101
+ // pre-entry copy without one.
102
+ const serverMod = await timed(
103
+ debug,
104
+ "inner: import @rangojs/router/server",
105
+ () => rscEnv.runner.import(SERVER_MODULE_ID),
106
+ );
107
+ let result: unknown;
108
+ try {
109
+ result = await discoverRoutersPass(
110
+ state,
111
+ rscEnv,
112
+ ssrEnv,
113
+ entryPath,
114
+ serverMod,
115
+ );
116
+ } catch (error) {
117
+ if (!(await runnerReloadedSince(rscEnv, serverMod))) throw error;
118
+ result = RUNNER_RELOADED;
119
+ }
120
+ if (result !== RUNNER_RELOADED) return result;
121
+ if (pass === MAX_DISCOVERY_PASSES) {
122
+ throw new Error(
123
+ `[rango] Route discovery did not settle: the RSC module runner reloaded during ${MAX_DISCOVERY_PASSES} consecutive discovery passes.`,
124
+ );
125
+ }
126
+ debug?.(
127
+ "inner: module runner reloaded mid-pass, redoing discovery (pass %d)",
128
+ pass + 1,
129
+ );
130
+ }
131
+ }
132
+
133
+ async function discoverRoutersPass(
134
+ state: DiscoveryState,
135
+ rscEnv: any,
136
+ ssrEnv: any,
137
+ entryPath: string,
138
+ serverMod: any,
139
+ ): Promise<any> {
71
140
  await timed(debug, "inner: refresh recorded client URL projections", () =>
72
141
  refreshRecordedClientUrlProjections(state, ssrEnv, serverMod),
73
142
  );
@@ -76,10 +145,11 @@ export async function discoverRouters(
76
145
  // For node preset: this is the router file (createRouter() registers in RouterRegistry).
77
146
  // For cloudflare preset: this is the worker entry (which imports the router).
78
147
  await timed(debug, "inner: import entry", () =>
79
- rscEnv.runner.import(state.resolvedEntryPath),
148
+ rscEnv.runner.import(entryPath),
80
149
  );
150
+ if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
81
151
 
82
- let registry: Map<string, any> = serverMod.RouterRegistry;
152
+ const registry: Map<string, any> = serverMod.RouterRegistry;
83
153
 
84
154
  if (!registry || registry.size === 0) {
85
155
  // No RSC routers found directly. Check for host routers with lazy handlers
@@ -105,16 +175,10 @@ export async function discoverRouters(
105
175
  debug?.("caught error while resolving %s: %O", context, error);
106
176
  }
107
177
 
108
- // Re-read RouterRegistry - sub-app createRouter() calls should have populated it
109
- const freshServerMod = await rscEnv.runner.import(
110
- "@rangojs/router/server",
111
- );
112
- const freshRegistry: Map<string, any> = freshServerMod.RouterRegistry;
113
-
114
- if (freshRegistry && freshRegistry.size > 0) {
115
- // Update references so the manifest generation below uses the fresh data
116
- Object.assign(serverMod, freshServerMod);
117
- registry = freshRegistry;
178
+ // Sub-app createRouter() calls populate the same live registry, unless
179
+ // a reload moved them to a fresh realm.
180
+ if (await runnerReloadedSince(rscEnv, serverMod)) {
181
+ return RUNNER_RELOADED;
118
182
  }
119
183
  }
120
184
  } catch (error) {
@@ -125,7 +189,7 @@ export async function discoverRouters(
125
189
 
126
190
  // If still no routers after host router resolution, fail
127
191
  if (!registry || registry.size === 0) {
128
- throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
192
+ throw new DiscoveryError(entryPath, discoveryErrors);
129
193
  }
130
194
  }
131
195
 
@@ -351,6 +415,10 @@ export async function discoverRouters(
351
415
  }
352
416
  }
353
417
 
418
+ // A reload after the entry import leaves this pass's projections and
419
+ // manifests on a realm the runner no longer serves.
420
+ if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
421
+
354
422
  // Commit all local state to the shared discovery state atomically.
355
423
  // This ensures a failed re-discovery (e.g. from a transient module
356
424
  // evaluation error) preserves the last known-good state.
@@ -196,14 +196,38 @@ export interface DiscoveryState {
196
196
  buildEnvDispose?: (() => Promise<void> | void) | null;
197
197
 
198
198
  /**
199
- * Set when the most recent HMR re-discovery threw. Cleared on the next
200
- * successful discovery. Surfaced via debug logs so we can detect "manifest
201
- * frozen at last-good after error → user fix in non-route file → no
202
- * rediscovery trigger" scenarios.
199
+ * Set when the most recent dev discovery failed: an HMR re-discovery, or the
200
+ * cold discovery on the Node RSC runner or the Cloudflare temp runner.
201
+ * Cleared on the next successful discovery. While set, the route-file
202
+ * watcher treats any in-scan source change as a rediscovery trigger
203
+ * (recovery mode).
203
204
  */
204
205
  lastDiscoveryError?: { message: string; at: number } | null;
205
206
  }
206
207
 
208
+ /** Record a failed dev discovery (lastDiscoveryError): recovery mode. */
209
+ export function recordDiscoveryFailure(
210
+ state: Pick<DiscoveryState, "lastDiscoveryError">,
211
+ err: unknown,
212
+ ): void {
213
+ state.lastDiscoveryError = {
214
+ message: (err as { message?: string } | null)?.message ?? String(err),
215
+ at: Date.now(),
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Whether the route-file watcher can re-run runtime discovery: a committed
221
+ * manifest, or a recorded failure. A failed cold discovery commits none, and
222
+ * without its recorded failure the dev server stayed broken until a restart,
223
+ * on the Node runner and on Cloudflare alike.
224
+ */
225
+ export function canRefreshRuntimeDiscovery(
226
+ state: Pick<DiscoveryState, "perRouterManifests" | "lastDiscoveryError">,
227
+ ): boolean {
228
+ return state.perRouterManifests.length > 0 || !!state.lastDiscoveryError;
229
+ }
230
+
207
231
  export function createDiscoveryState(
208
232
  entryPath: string | undefined,
209
233
  opts: PluginOptions | undefined,
@@ -46,6 +46,22 @@ function devDiscoveryBootstrap(state: DiscoveryState): string[] {
46
46
  return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
47
47
  }
48
48
 
49
+ /** Clear, then install every projection in state.clientUrlProjectionMap. */
50
+ function clientUrlProjectionReplay(state: DiscoveryState): string[] {
51
+ // Sorted: the map fills in transform order, which varies between runs,
52
+ // and this module's bytes feed cache versions (build-versions.ts).
53
+ const projections = [...(state.clientUrlProjectionMap ?? [])].sort(
54
+ ([a], [b]) => compareStrings(a, b),
55
+ );
56
+ return [
57
+ `clearClientUrlProjections();`,
58
+ ...projections.map(
59
+ ([referenceId, projection]) =>
60
+ `setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
61
+ ),
62
+ ];
63
+ }
64
+
49
65
  /**
50
66
  * Generate the code for the main virtual:rsc-router/routes-manifest module.
51
67
  */
@@ -110,19 +126,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
110
126
  TRANSITION_WHEN_VALIDATION,
111
127
  ];
112
128
 
113
- if (hasClientUrlModules) {
114
- lines.push(`clearClientUrlProjections();`);
115
- // Sorted: the map fills in transform order, which varies between runs,
116
- // and this module's bytes feed cache versions (build-versions.ts).
117
- const projections = [...(state.clientUrlProjectionMap ?? [])].sort(
118
- ([a], [b]) => compareStrings(a, b),
119
- );
120
- for (const [referenceId, projection] of projections) {
121
- lines.push(
122
- `setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
123
- );
124
- }
125
- }
129
+ if (hasClientUrlModules) lines.push(...clientUrlProjectionReplay(state));
126
130
 
127
131
  if (varIdx > 0) {
128
132
  lines.push(
@@ -197,13 +201,14 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
197
201
  return lines.join("\n");
198
202
  }
199
203
 
204
+ // No manifest: discovery has not committed one (a failed cold pass commits
205
+ // nothing). The projections the failed pass computed still replay, so the
206
+ // realm serving requests can resolve include()s of clientUrls() modules.
200
207
  const lines: string[] = [
201
- `import { ${hasClientUrlModules ? "clearClientUrlProjections, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
208
+ `import { ${hasClientUrlModules ? "clearClientUrlProjections, setClientUrlProjection, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
202
209
  TRANSITION_WHEN_VALIDATION,
203
210
  ];
204
- if (hasClientUrlModules) {
205
- lines.push(`clearClientUrlProjections();`);
206
- }
211
+ if (hasClientUrlModules) lines.push(...clientUrlProjectionReplay(state));
207
212
  if (!state.isBuildMode) {
208
213
  const origin =
209
214
  state.devServerOrigin ||