@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1

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 (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -17,6 +17,7 @@ import {
17
17
  findNestedRouterConflict,
18
18
  findRouterFiles,
19
19
  } from "../build/generate-route-types.js";
20
+ import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
20
21
  import { createVersionPlugin } from "./plugins/version-plugin.js";
21
22
  import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
22
23
  import {
@@ -52,6 +53,11 @@ import {
52
53
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
53
54
  import { createDiscoveryGate } from "./discovery/gate-state.js";
54
55
  import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
56
+ import { resolveRscEntryFromConfig } from "./utils/shared-utils.js";
57
+ import {
58
+ pickForwardedRunnerConfig,
59
+ selectForwardableResolvePlugins,
60
+ } from "./utils/forward-user-plugins.js";
55
61
  import { createRangoDebugger, timed, timedSync, NS } from "./debug.js";
56
62
 
57
63
  const debugDiscovery = createRangoDebugger(NS.discovery);
@@ -99,7 +105,7 @@ function ensureCloudflareProtocolLoaderRegistered(): void {
99
105
  // register() requires Node 18.19+ / 20.6+. Older Node still has the
100
106
  // Vite transform as primary defense.
101
107
  console.warn(
102
- `[rsc-router] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`,
108
+ `[rango] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`,
103
109
  );
104
110
  }
105
111
  }
@@ -129,14 +135,26 @@ async function createTempRscServer(
129
135
  // instead of crashing Node's native loader.
130
136
  ensureCloudflareProtocolLoaderRegistered();
131
137
  const { default: rsc } = await import("@vitejs/plugin-rsc");
138
+ // Mirror the user's resolution config + plugins so discovery (and the
139
+ // prerender/static rendering that shares this runner) resolves modules the
140
+ // same way the real environment does. Falls back to the legacy alias-only
141
+ // behavior if configResolved hasn't populated the parity slice yet.
142
+ const runnerConfig = state.userRunnerConfig;
143
+ const resolveConfig = runnerConfig?.resolve ?? {
144
+ alias: state.userResolveAlias,
145
+ };
146
+ const oxcConfig = runnerConfig?.oxc ?? {
147
+ jsx: { runtime: "automatic", importSource: "react" },
148
+ };
132
149
  return createViteServer({
133
150
  root: state.projectRoot,
134
151
  configFile: false,
135
152
  server: { middlewareMode: true },
136
153
  appType: "custom",
137
154
  logLevel: "silent",
138
- resolve: { alias: state.userResolveAlias },
139
- esbuild: { jsx: "automatic", jsxImportSource: "react" },
155
+ resolve: resolveConfig,
156
+ ...(runnerConfig?.define ? { define: runnerConfig.define } : {}),
157
+ oxc: oxcConfig as any,
140
158
  ...(options.cacheDir && { cacheDir: options.cacheDir }),
141
159
  plugins: [
142
160
  rsc({
@@ -155,6 +173,10 @@ async function createTempRscServer(
155
173
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
156
174
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
157
175
  exposeRouterId(),
176
+ // Forwarded user resolution plugins (e.g. vite-tsconfig-paths). Stripped
177
+ // to resolveId/load and placed last so framework resolution runs first;
178
+ // Vite re-sorts by `enforce`, so `enforce: "pre"` resolvers still lead.
179
+ ...state.userResolvePlugins,
158
180
  ],
159
181
  });
160
182
  }
@@ -182,7 +204,7 @@ async function resolveBuildEnv(
182
204
  if (option === "auto") {
183
205
  if (factoryCtx.preset !== "cloudflare") {
184
206
  throw new Error(
185
- '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
207
+ '[rango] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
186
208
  "Use a factory function or plain object for other presets.",
187
209
  );
188
210
  }
@@ -204,7 +226,7 @@ async function resolveBuildEnv(
204
226
  };
205
227
  } catch (err: any) {
206
228
  throw new Error(
207
- '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
229
+ '[rango] buildEnv: "auto" requires wrangler to be installed.\n' +
208
230
  `Install it with: pnpm add -D wrangler\n${err.message}`,
209
231
  );
210
232
  }
@@ -256,7 +278,7 @@ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
256
278
  try {
257
279
  await s.buildEnvDispose();
258
280
  } catch (err: any) {
259
- console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
281
+ console.warn(`[rango] buildEnv dispose failed: ${err.message}`);
260
282
  }
261
283
  s.buildEnvDispose = null;
262
284
  }
@@ -309,23 +331,28 @@ export function createRouterDiscoveryPlugin(
309
331
  viteMode = config.mode;
310
332
  // Capture user's resolve aliases for the temp server
311
333
  s.userResolveAlias = config.resolve.alias;
334
+ // Capture the data-only resolution config (resolve.*, define, oxc) and
335
+ // the user's resolution plugins (resolveId/load) so the discovery temp
336
+ // server resolves modules the same way the real environment does.
337
+ // Without this, both flavors of user resolution are absent during
338
+ // discovery/prerender/static rendering even though they apply at request
339
+ // time: third-party resolvers (e.g. vite-tsconfig-paths, forwarded as
340
+ // plugins) and Vite 8's native resolve.tsconfigPaths (forwarded in the
341
+ // data slice). See utils/forward-user-plugins.ts.
342
+ s.userRunnerConfig = pickForwardedRunnerConfig(config);
343
+ s.userResolvePlugins = selectForwardableResolvePlugins(
344
+ config.plugins as any,
345
+ );
312
346
  // Node preset: pick up auto-discovered router path from the config() hook.
313
347
  // The auto-discover plugin runs in config() using Vite's resolved root,
314
348
  // populating the mutable ref before configResolved fires.
315
349
  if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
316
350
  s.resolvedEntryPath = opts.routerPathRef.path;
317
351
  }
318
- // Cloudflare preset: read entry from resolved environment config.
319
- // The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
320
- // and sets optimizeDeps.entries on the RSC environment.
352
+ // Cloudflare preset: entry comes from the resolved RSC env config.
321
353
  if (!s.resolvedEntryPath) {
322
- const rscEnvConfig = (config.environments as any)?.["rsc"];
323
- const entries = rscEnvConfig?.optimizeDeps?.entries;
324
- if (typeof entries === "string") {
325
- s.resolvedEntryPath = entries;
326
- } else if (Array.isArray(entries) && entries.length > 0) {
327
- s.resolvedEntryPath = entries[0];
328
- }
354
+ const entry = resolveRscEntryFromConfig(config);
355
+ if (entry) s.resolvedEntryPath = entry;
329
356
  }
330
357
  // Generate combined named-routes.gen.ts from static source parsing.
331
358
  // Runs before the dev server starts so the gen file exists immediately for IDE.
@@ -512,9 +539,7 @@ export function createRouterDiscoveryPlugin(
512
539
  "getOrCreateTempServer: FAILED message=%s",
513
540
  err.message,
514
541
  );
515
- console.warn(
516
- `[rsc-router] Failed to create temp runner: ${err.message}`,
517
- );
542
+ console.warn(`[rango] Failed to create temp runner: ${err.message}`);
518
543
  }
519
544
  return null;
520
545
  }
@@ -653,7 +678,7 @@ export function createRouterDiscoveryPlugin(
653
678
  }
654
679
  } catch (err: any) {
655
680
  console.warn(
656
- `[rsc-router] Cloudflare dev discovery failed: ${err.message}\n${err.stack}`,
681
+ `[rango] Cloudflare dev discovery failed: ${err.message}\n${err.stack}`,
657
682
  );
658
683
  }
659
684
 
@@ -705,7 +730,7 @@ export function createRouterDiscoveryPlugin(
705
730
  );
706
731
  } catch (err: any) {
707
732
  console.warn(
708
- `[rsc-router] Router discovery failed: ${err.message}\n${err.stack}`,
733
+ `[rango] Router discovery failed: ${err.message}\n${err.stack}`,
709
734
  );
710
735
  } finally {
711
736
  debugDiscovery?.(
@@ -764,20 +789,15 @@ export function createRouterDiscoveryPlugin(
764
789
  if (s.mergedRouteTrie && serverMod.setRouteTrie) {
765
790
  serverMod.setRouteTrie(s.mergedRouteTrie);
766
791
  }
767
- if (serverMod.setRouterManifest) {
768
- for (const [routerId, manifest] of s.perRouterManifestDataMap) {
769
- serverMod.setRouterManifest(routerId, manifest);
770
- }
771
- }
772
- if (serverMod.setRouterTrie) {
773
- for (const [routerId, trie] of s.perRouterTrieMap) {
774
- serverMod.setRouterTrie(routerId, trie);
775
- }
776
- }
777
- if (serverMod.setRouterPrecomputedEntries) {
778
- for (const [routerId, entries] of s.perRouterPrecomputedMap) {
779
- serverMod.setRouterPrecomputedEntries(routerId, entries);
780
- }
792
+ const perRouterSetters: Array<[Map<string, any>, string]> = [
793
+ [s.perRouterManifestDataMap, "setRouterManifest"],
794
+ [s.perRouterTrieMap, "setRouterTrie"],
795
+ [s.perRouterPrecomputedMap, "setRouterPrecomputedEntries"],
796
+ ];
797
+ for (const [map, fn] of perRouterSetters) {
798
+ const setter = serverMod[fn];
799
+ if (typeof setter !== "function") continue;
800
+ for (const [routerId, value] of map) setter(routerId, value);
781
801
  }
782
802
  };
783
803
 
@@ -820,7 +840,7 @@ export function createRouterDiscoveryPlugin(
820
840
  registry = serverMod.RouterRegistry ?? null;
821
841
  } catch (err: any) {
822
842
  console.warn(
823
- `[rsc-router] Dev prerender module refresh failed: ${err.message}`,
843
+ `[rango] Dev prerender module refresh failed: ${err.message}`,
824
844
  );
825
845
  res.statusCode = 500;
826
846
  res.end(`Prerender handler error: ${err.message}`);
@@ -886,7 +906,7 @@ export function createRouterDiscoveryPlugin(
886
906
  return;
887
907
  } catch (err: any) {
888
908
  console.warn(
889
- `[rsc-router] Dev prerender failed for ${pathname}: ${err.message}`,
909
+ `[rango] Dev prerender failed for ${pathname}: ${err.message}`,
890
910
  );
891
911
  }
892
912
  }
@@ -987,9 +1007,34 @@ export function createRouterDiscoveryPlugin(
987
1007
  writeRouteTypesFiles(s),
988
1008
  );
989
1009
  }
1010
+ if (s.lastDiscoveryError) {
1011
+ debugDiscovery?.(
1012
+ "hmr: cleared lastDiscoveryError (%s) after successful rediscovery",
1013
+ s.lastDiscoveryError.message,
1014
+ );
1015
+ s.lastDiscoveryError = null;
1016
+ }
1017
+ // Cloudflare dev: on a successful cycle drop the workerd runner's
1018
+ // cached worker-entry chain so the next request re-evaluates
1019
+ // createRouter() with the new routes. Fired here in the work path
1020
+ // (not the caller's .then()) so a queued follow-up cycle that
1021
+ // succeeds after an earlier failed cycle still reloads:
1022
+ // runRefreshCycle recurses queued work without awaiting it, so the
1023
+ // original call already resolved on the failed cycle. A failed
1024
+ // cycle throws above and never reaches here, so a broken edit
1025
+ // never reloads the worker onto bad source.
1026
+ if (rscEnv && !rscEnv.runner) forceCloudflareWorkerReload(rscEnv);
990
1027
  } catch (err: any) {
1028
+ s.lastDiscoveryError = {
1029
+ message: err?.message ?? String(err),
1030
+ at: Date.now(),
1031
+ };
991
1032
  console.warn(
992
- `[rsc-router] Runtime re-discovery failed: ${err.message}`,
1033
+ `[rango] Runtime re-discovery failed: ${err.message}`,
1034
+ );
1035
+ debugDiscovery?.(
1036
+ "hmr: lastDiscoveryError set (%s) — manifest preserved at last-good; recovery mode active (any in-scan source change will trigger rediscovery)",
1037
+ err?.message,
993
1038
  );
994
1039
  } finally {
995
1040
  debugDiscovery?.(
@@ -1000,6 +1045,49 @@ export function createRouterDiscoveryPlugin(
1000
1045
  });
1001
1046
  };
1002
1047
 
1048
+ // Cloudflare dev only. workerd serves every request through the
1049
+ // runner-worker singleton, which re-resolves the worker entry per
1050
+ // request via runner.import("virtual:cloudflare/worker-entry"). The
1051
+ // route table lives in the user's createRouter() instance, captured
1052
+ // when that entry chain (entry -> router -> urls) was last evaluated
1053
+ // and then cached in the runner's evaluatedModules. The route-file
1054
+ // watcher refreshes discovery + types on the Node side, but the worker
1055
+ // keeps serving the cached (stale) router: route-definition modules
1056
+ // have no import.meta.hot boundary, so Vite never sends the worker an
1057
+ // HMR update for them and the entry chain is never evicted.
1058
+ //
1059
+ // Fix: after discovery completes, (1) invalidate the worker env's
1060
+ // Node-side module graph, then (2) send a full-reload to the worker.
1061
+ // Step (2) alone is insufficient: the full-reload handler clears the
1062
+ // runner's evaluatedModules and re-imports entrypoints, but each
1063
+ // re-import fetches the module back through this Node-side graph, which
1064
+ // still holds the pre-edit transform of urls.tsx — so createRouter()
1065
+ // rebuilds the stale route table and the new route 404s/hits the
1066
+ // catch-all. Invalidating the graph forces a fresh transform on
1067
+ // re-fetch (the same mechanism refreshTempRscEnv uses for discovery),
1068
+ // so the re-import re-runs createRouter() with the new routes. This is
1069
+ // the programmatic equivalent of the dev-server "r + enter" restart,
1070
+ // scoped to the worker environment instead of tearing down the server.
1071
+ const forceCloudflareWorkerReload = (rscEnv: any) => {
1072
+ if (!rscEnv?.hot) return;
1073
+ try {
1074
+ const graph = rscEnv.moduleGraph;
1075
+ if (graph?.invalidateAll) {
1076
+ graph.invalidateAll();
1077
+ debugDiscovery?.("hmr: invalidated workerd rsc module graph");
1078
+ }
1079
+ rscEnv.hot.send({ type: "full-reload" });
1080
+ debugDiscovery?.(
1081
+ "hmr: forced workerd rsc env reload (full-reload)",
1082
+ );
1083
+ } catch (err: any) {
1084
+ debugDiscovery?.(
1085
+ "hmr: workerd reload failed: %s",
1086
+ err?.message ?? err,
1087
+ );
1088
+ }
1089
+ };
1090
+
1003
1091
  const scheduleRouteRegeneration = () => {
1004
1092
  clearTimeout(routeChangeTimer);
1005
1093
  routeChangeTimer = setTimeout(() => {
@@ -1028,9 +1116,7 @@ export function createRouterDiscoveryPlugin(
1028
1116
  }
1029
1117
  }
1030
1118
  } catch (err: any) {
1031
- console.error(
1032
- `[rsc-router] Route regeneration error: ${err.message}`,
1033
- );
1119
+ console.error(`[rango] Route regeneration error: ${err.message}`);
1034
1120
  }
1035
1121
  debugDiscovery?.(
1036
1122
  "watcher: regenerated gen files (%sms)",
@@ -1040,12 +1126,15 @@ export function createRouterDiscoveryPlugin(
1040
1126
  // routes that the static parser cannot resolve. Resolves the
1041
1127
  // discovery gate when complete.
1042
1128
  if (s.perRouterManifests.length > 0) {
1129
+ // The cloudflare workerd reload fires inside refreshRuntimeDiscovery
1130
+ // on the successful cycle (see forceCloudflareWorkerReload call
1131
+ // there) so queued follow-up cycles also trigger it.
1043
1132
  refreshRuntimeDiscovery().catch((err: any) => {
1044
1133
  console.warn(
1045
- `[rsc-router] Runtime re-discovery error: ${err.message}`,
1134
+ `[rango] Runtime re-discovery error: ${err.message}`,
1046
1135
  );
1047
- // Even on error, unblock the gate so workerd's reload
1048
- // doesn't hang indefinitely against the previous manifest.
1136
+ // Even on error, unblock the gate so workerd's reload doesn't
1137
+ // hang indefinitely against the previous manifest.
1049
1138
  resolveDiscoveryGate();
1050
1139
  });
1051
1140
  }
@@ -1059,27 +1148,74 @@ export function createRouterDiscoveryPlugin(
1059
1148
  !filePath.endsWith(".tsx") &&
1060
1149
  !filePath.endsWith(".js") &&
1061
1150
  !filePath.endsWith(".jsx")
1062
- )
1151
+ ) {
1152
+ if (s.lastDiscoveryError) {
1153
+ debugDiscovery?.(
1154
+ "watcher: skip non-source %s [LASTERR %s]",
1155
+ filePath,
1156
+ s.lastDiscoveryError.message,
1157
+ );
1158
+ }
1063
1159
  return;
1160
+ }
1064
1161
  // Apply scan filter as early-exit before reading file
1065
- if (s.scanFilter && !s.scanFilter(filePath)) return;
1162
+ if (s.scanFilter && !s.scanFilter(filePath)) {
1163
+ if (s.lastDiscoveryError) {
1164
+ debugDiscovery?.(
1165
+ "watcher: skip scan-filter %s [LASTERR %s]",
1166
+ filePath,
1167
+ s.lastDiscoveryError.message,
1168
+ );
1169
+ }
1170
+ return;
1171
+ }
1172
+ // Recovery mode: when the previous HMR re-discovery failed, the
1173
+ // import graph is incomplete and the manifest is stuck at the
1174
+ // last-good state. The fix may land in a non-route file (e.g. a
1175
+ // helper imported by the router, a missing module being created,
1176
+ // or a "use client" component) that the narrow content sniff
1177
+ // would otherwise filter out. While in recovery, treat any
1178
+ // in-scan source change as a candidate for rediscovery; the
1179
+ // tighter filter resumes once discovery succeeds again.
1180
+ const inRecoveryMode = !!s.lastDiscoveryError;
1066
1181
  try {
1067
1182
  const source = readFileSync(filePath, "utf-8");
1068
1183
  const trimmed = source.trimStart();
1069
- if (
1184
+ const isUseClient =
1070
1185
  trimmed.startsWith('"use client"') ||
1071
- trimmed.startsWith("'use client'")
1072
- )
1073
- return;
1074
- const hasUrls = source.includes("urls(");
1075
- const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
1076
- if (!hasUrls && !hasCreateRouter) return;
1077
- debugDiscovery?.(
1078
- "watcher: %s matches (urls=%s, router=%s)",
1079
- filePath,
1080
- hasUrls,
1081
- hasCreateRouter,
1082
- );
1186
+ trimmed.startsWith("'use client'");
1187
+ if (!inRecoveryMode && isUseClient) return;
1188
+ // Cheap raw pre-check first; only when a candidate token is present
1189
+ // do we confirm it occurs in real code (not a comment/string) via a
1190
+ // single allocation-free code-region scan. Most saved files contain
1191
+ // neither token and skip the scan entirely. This avoids a comment or
1192
+ // string mention spuriously marking a file relevant and triggering an
1193
+ // unnecessary re-discovery on save.
1194
+ let hasUrls = source.includes("urls(");
1195
+ let hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
1196
+ if (hasUrls) hasUrls = firstCodeMatchIndex(source, /urls\(/g) >= 0;
1197
+ if (hasCreateRouter) {
1198
+ hasCreateRouter =
1199
+ firstCodeMatchIndex(source, /\bcreateRouter\s*[<(]/g) >= 0;
1200
+ }
1201
+ if (!inRecoveryMode && !hasUrls && !hasCreateRouter) return;
1202
+ if (inRecoveryMode) {
1203
+ debugDiscovery?.(
1204
+ "watcher: recovery rediscovery for %s (urls=%s, router=%s, useClient=%s) [LASTERR %s]",
1205
+ filePath,
1206
+ hasUrls,
1207
+ hasCreateRouter,
1208
+ isUseClient,
1209
+ s.lastDiscoveryError!.message,
1210
+ );
1211
+ } else {
1212
+ debugDiscovery?.(
1213
+ "watcher: %s matches (urls=%s, router=%s)",
1214
+ filePath,
1215
+ hasUrls,
1216
+ hasCreateRouter,
1217
+ );
1218
+ }
1083
1219
  // Invalidate cache when a router file changes (new router added/removed)
1084
1220
  if (hasCreateRouter) {
1085
1221
  const nestedRouterConflict = findNestedRouterConflict([
@@ -1106,7 +1242,15 @@ export function createRouterDiscoveryPlugin(
1106
1242
  gate.noteRouteEvent();
1107
1243
  }
1108
1244
  scheduleRouteRegeneration();
1109
- } catch {
1245
+ } catch (readErr: any) {
1246
+ if (s.lastDiscoveryError) {
1247
+ debugDiscovery?.(
1248
+ "watcher: read error %s: %s [LASTERR %s]",
1249
+ filePath,
1250
+ readErr?.message,
1251
+ s.lastDiscoveryError.message,
1252
+ );
1253
+ }
1110
1254
  // Ignore read errors for deleted/moved files
1111
1255
  }
1112
1256
  };
@@ -1169,7 +1313,7 @@ export function createRouterDiscoveryPlugin(
1169
1313
  const rscEnv = (tempServer.environments as any)?.rsc;
1170
1314
  if (!rscEnv?.runner) {
1171
1315
  console.warn(
1172
- "[rsc-router] RSC environment runner not available during build, skipping manifest generation",
1316
+ "[rango] RSC environment runner not available during build, skipping manifest generation",
1173
1317
  );
1174
1318
  return;
1175
1319
  }
@@ -1212,7 +1356,8 @@ export function createRouterDiscoveryPlugin(
1212
1356
  .filter(Boolean)
1213
1357
  .join("\n");
1214
1358
  throw new Error(
1215
- `[rsc-router] Build-time router discovery failed:\n${details}`,
1359
+ `[rango] Build-time router discovery failed:\n${details}`,
1360
+ { cause: err },
1216
1361
  );
1217
1362
  } finally {
1218
1363
  delete (globalThis as any).__rscRouterDiscoveryActive;
@@ -1239,7 +1384,7 @@ export function createRouterDiscoveryPlugin(
1239
1384
  // `consumeSelfGenWrite` inside `maybeHandleGeneratedRouteFileMutation`),
1240
1385
  // AND vite's own HMR pipeline (which invalidates the gen file's
1241
1386
  // importers and triggers a second workerd full reload — visible to the
1242
- // user as a duplicate "[RSCRouter] HMR: version changed" on the client).
1387
+ // user as a duplicate "[Rango] HMR: version changed" on the client).
1243
1388
  //
1244
1389
  // `peekSelfGenWrite` is the authoritative filter: its map only contains
1245
1390
  // paths that `markSelfGenWrite` has registered, so it natively works
@@ -48,7 +48,7 @@ function findImportInsertionPos(
48
48
  ): number {
49
49
  let program: ProgramNode;
50
50
  try {
51
- program = parseAst(code, { jsx: true });
51
+ program = parseAst(code, { lang: "tsx" });
52
52
  } catch {
53
53
  return 0;
54
54
  }
@@ -127,7 +127,7 @@ export function findHandlerCalls(
127
127
  ): HandlerCallSite[] {
128
128
  let program: ProgramNode;
129
129
  try {
130
- program = parseAst(code, { jsx: true });
130
+ program = parseAst(code, { lang: "tsx" });
131
131
  } catch {
132
132
  return [];
133
133
  }
@@ -239,7 +239,7 @@ export function getImportedLocalNames(
239
239
  parseAst: (code: string, options?: any) => ProgramNode,
240
240
  ): Set<string> {
241
241
  try {
242
- const program = parseAst(code, { jsx: true });
242
+ const program = parseAst(code, { lang: "tsx" });
243
243
  return getImportedLocalNamesFromProgram(program, importedName);
244
244
  } catch {
245
245
  return new Set<string>();
@@ -256,7 +256,7 @@ export function extractImportDeclarations(
256
256
  ): string[] {
257
257
  let program: ProgramNode;
258
258
  try {
259
- program = parseAst(code, { jsx: true });
259
+ program = parseAst(code, { lang: "tsx" });
260
260
  } catch {
261
261
  return [];
262
262
  }
@@ -380,7 +380,7 @@ export function extractModuleLevelDeclarations(
380
380
  ): string[] {
381
381
  let program: ProgramNode;
382
382
  try {
383
- program = parseAst(code, { jsx: true });
383
+ program = parseAst(code, { lang: "tsx" });
384
384
  } catch {
385
385
  return [];
386
386
  }
@@ -468,19 +468,19 @@ export function transformInlineHandlers(
468
468
  handlerNames,
469
469
  );
470
470
 
471
- // Track line occurrences for same-line collision handling
472
- const lineCounts = new Map<number, number>();
473
-
474
471
  // Collect all import statements to prepend
475
472
  const importStatements: string[] = [];
476
473
 
477
- for (const site of inlineSites) {
478
- const lineCount = lineCounts.get(site.lineNumber) ?? 0;
479
- lineCounts.set(site.lineNumber, lineCount + 1);
480
-
481
- const hash = hashInlineId(filePath, site.lineNumber, lineCount);
474
+ for (const [siteIndex, site] of inlineSites.entries()) {
475
+ // Key the extracted handler on its source-order index (per fnName), NOT its
476
+ // line number. The id flows into BOTH the export name and the virtual module
477
+ // path (which hashId hashes for the runtime $$id), and line numbers shift
478
+ // between the prerender and production build contexts. The index is invariant
479
+ // to those shifts, keeping the prerender manifest key == the runtime id.
480
+ const hash = hashInlineId(filePath, fnName, siteIndex);
482
481
  const exportName = `__sh_${hash}`;
483
- const virtualId = `\0${virtualPrefix}${filePath}:${site.lineNumber}${lineCount > 0 ? `:${lineCount}` : ""}`;
482
+ const idSuffix = `${filePath}:${fnName}:${siteIndex}`;
483
+ const virtualId = `\0${virtualPrefix}${idSuffix}`;
484
484
 
485
485
  // Extract the full handler call expression text
486
486
  const handlerCode = code.slice(site.callStart, site.callEnd);
@@ -498,7 +498,7 @@ export function transformInlineHandlers(
498
498
  s.overwrite(site.callStart, site.callEnd, exportName);
499
499
 
500
500
  // Build the import specifier for this virtual module
501
- const importId = `${virtualPrefix}${filePath}:${site.lineNumber}${lineCount > 0 ? `:${lineCount}` : ""}`;
501
+ const importId = `${virtualPrefix}${idSuffix}`;
502
502
  importStatements.push(`import { ${exportName} } from "${importId}";`);
503
503
  }
504
504
 
@@ -59,7 +59,7 @@ export function extractHandlerExportsFromChunk(
59
59
  if (detectPassthrough) {
60
60
  const eFnName = escapeRegExp(fnName);
61
61
  const callStartRe = new RegExp(
62
- `const\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
62
+ `(?:const|let|var)\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
63
63
  );
64
64
  const callStart = callStartRe.exec(chunkCode);
65
65
  if (callStart) {
@@ -98,8 +98,10 @@ export function evictHandlerCode(
98
98
  if (passthrough) continue;
99
99
 
100
100
  const eName = escapeRegExp(name);
101
+ // Match const/let/var: Rolldown (Vite 8) emits top-level bindings in the
102
+ // non-minified RSC bundle as `var`, whereas Rollup used `const`.
101
103
  const callStartRe = new RegExp(
102
- `const\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
104
+ `(?:const|let|var)\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
103
105
  );
104
106
  const startMatch = callStartRe.exec(modified);
105
107
  if (!startMatch) continue;