@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4

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 (253) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2151 -846
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +364 -0
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +46 -4
  18. package/skills/layout/SKILL.md +28 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +47 -12
  22. package/skills/migrate-nextjs/SKILL.md +562 -0
  23. package/skills/migrate-react-router/SKILL.md +769 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +71 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -22
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +57 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +647 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +84 -11
  47. package/src/browser/navigation-client.ts +76 -28
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +64 -26
  51. package/src/browser/prefetch/cache.ts +129 -21
  52. package/src/browser/prefetch/fetch.ts +148 -16
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +30 -2
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-reconciler.ts +36 -14
  71. package/src/browser/segment-structure-assert.ts +2 -2
  72. package/src/browser/server-action-bridge.ts +23 -30
  73. package/src/browser/types.ts +21 -0
  74. package/src/build/collect-fallback-refs.ts +107 -0
  75. package/src/build/generate-manifest.ts +60 -35
  76. package/src/build/generate-route-types.ts +2 -0
  77. package/src/build/index.ts +2 -0
  78. package/src/build/route-trie.ts +52 -25
  79. package/src/build/route-types/codegen.ts +4 -4
  80. package/src/build/route-types/include-resolution.ts +1 -1
  81. package/src/build/route-types/per-module-writer.ts +7 -4
  82. package/src/build/route-types/router-processing.ts +55 -14
  83. package/src/build/route-types/scan-filter.ts +1 -1
  84. package/src/build/route-types/source-scan.ts +118 -0
  85. package/src/build/runtime-discovery.ts +9 -20
  86. package/src/cache/cache-scope.ts +28 -42
  87. package/src/cache/cf/cf-cache-store.ts +54 -13
  88. package/src/client.rsc.tsx +3 -0
  89. package/src/client.tsx +92 -182
  90. package/src/context-var.ts +5 -5
  91. package/src/decode-loader-results.ts +36 -0
  92. package/src/errors.ts +30 -1
  93. package/src/handle.ts +26 -13
  94. package/src/host/index.ts +2 -2
  95. package/src/host/router.ts +129 -57
  96. package/src/host/types.ts +31 -2
  97. package/src/host/utils.ts +1 -1
  98. package/src/href-client.ts +140 -20
  99. package/src/index.rsc.ts +9 -4
  100. package/src/index.ts +53 -15
  101. package/src/loader-store.ts +500 -0
  102. package/src/loader.rsc.ts +2 -5
  103. package/src/loader.ts +3 -10
  104. package/src/missing-id-error.ts +68 -0
  105. package/src/outlet-context.ts +1 -1
  106. package/src/prerender.ts +4 -4
  107. package/src/response-utils.ts +37 -0
  108. package/src/reverse.ts +65 -36
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +384 -257
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +100 -28
  113. package/src/route-definition/resolve-handler-use.ts +6 -0
  114. package/src/route-definition/use-item-types.ts +32 -0
  115. package/src/route-types.ts +26 -41
  116. package/src/router/basename.ts +14 -0
  117. package/src/router/content-negotiation.ts +15 -2
  118. package/src/router/error-handling.ts +1 -1
  119. package/src/router/handler-context.ts +21 -38
  120. package/src/router/intercept-resolution.ts +4 -18
  121. package/src/router/lazy-includes.ts +8 -8
  122. package/src/router/loader-resolution.ts +19 -2
  123. package/src/router/manifest.ts +22 -13
  124. package/src/router/match-api.ts +4 -3
  125. package/src/router/match-handlers.ts +63 -20
  126. package/src/router/match-middleware/cache-lookup.ts +44 -91
  127. package/src/router/match-middleware/cache-store.ts +3 -2
  128. package/src/router/match-result.ts +53 -32
  129. package/src/router/metrics.ts +1 -1
  130. package/src/router/middleware-types.ts +15 -26
  131. package/src/router/middleware.ts +99 -84
  132. package/src/router/pattern-matching.ts +101 -17
  133. package/src/router/prerender-match.ts +1 -1
  134. package/src/router/preview-match.ts +3 -1
  135. package/src/router/request-classification.ts +4 -28
  136. package/src/router/revalidation.ts +58 -2
  137. package/src/router/router-interfaces.ts +45 -28
  138. package/src/router/router-options.ts +40 -1
  139. package/src/router/router-registry.ts +2 -5
  140. package/src/router/segment-resolution/fresh.ts +27 -6
  141. package/src/router/segment-resolution/revalidation.ts +147 -106
  142. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  143. package/src/router/substitute-pattern-params.ts +56 -0
  144. package/src/router/telemetry.ts +99 -0
  145. package/src/router/trie-matching.ts +18 -13
  146. package/src/router/types.ts +8 -0
  147. package/src/router/url-params.ts +49 -0
  148. package/src/router.ts +38 -23
  149. package/src/rsc/handler-context.ts +2 -2
  150. package/src/rsc/handler.ts +28 -69
  151. package/src/rsc/helpers.ts +91 -43
  152. package/src/rsc/index.ts +1 -1
  153. package/src/rsc/origin-guard.ts +28 -10
  154. package/src/rsc/progressive-enhancement.ts +4 -0
  155. package/src/rsc/response-route-handler.ts +46 -53
  156. package/src/rsc/rsc-rendering.ts +35 -51
  157. package/src/rsc/runtime-warnings.ts +9 -10
  158. package/src/rsc/server-action.ts +17 -37
  159. package/src/rsc/ssr-setup.ts +16 -0
  160. package/src/rsc/types.ts +8 -2
  161. package/src/search-params.ts +4 -4
  162. package/src/segment-content-promise.ts +67 -0
  163. package/src/segment-loader-promise.ts +122 -0
  164. package/src/segment-system.tsx +132 -116
  165. package/src/serialize.ts +243 -0
  166. package/src/server/context.ts +143 -53
  167. package/src/server/cookie-store.ts +28 -4
  168. package/src/server/request-context.ts +20 -42
  169. package/src/ssr/index.tsx +5 -1
  170. package/src/static-handler.ts +1 -1
  171. package/src/testing/cache-status.ts +166 -0
  172. package/src/testing/collect-handle.ts +63 -0
  173. package/src/testing/dispatch.ts +440 -0
  174. package/src/testing/dom.entry.ts +22 -0
  175. package/src/testing/e2e/fixture.ts +154 -0
  176. package/src/testing/e2e/index.ts +149 -0
  177. package/src/testing/e2e/matchers.ts +51 -0
  178. package/src/testing/e2e/page-helpers.ts +272 -0
  179. package/src/testing/e2e/parity.ts +306 -0
  180. package/src/testing/e2e/server.ts +183 -0
  181. package/src/testing/flight-matchers.ts +104 -0
  182. package/src/testing/flight-runtime.d.ts +21 -0
  183. package/src/testing/flight.entry.ts +22 -0
  184. package/src/testing/flight.ts +182 -0
  185. package/src/testing/generated-routes.ts +223 -0
  186. package/src/testing/index.ts +105 -0
  187. package/src/testing/internal/context.ts +193 -0
  188. package/src/testing/render-route.tsx +536 -0
  189. package/src/testing/run-loader.ts +296 -0
  190. package/src/testing/run-middleware.ts +170 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +183 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/route-entry.ts +11 -0
  202. package/src/types/segments.ts +35 -2
  203. package/src/urls/include-helper.ts +34 -67
  204. package/src/urls/index.ts +0 -3
  205. package/src/urls/path-helper-types.ts +41 -7
  206. package/src/urls/path-helper.ts +17 -52
  207. package/src/urls/pattern-types.ts +36 -19
  208. package/src/urls/response-types.ts +22 -29
  209. package/src/urls/type-extraction.ts +26 -116
  210. package/src/urls/urls-function.ts +1 -5
  211. package/src/use-loader.tsx +413 -42
  212. package/src/vite/debug.ts +185 -0
  213. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  214. package/src/vite/discovery/discover-routers.ts +101 -51
  215. package/src/vite/discovery/discovery-errors.ts +194 -0
  216. package/src/vite/discovery/gate-state.ts +171 -0
  217. package/src/vite/discovery/prerender-collection.ts +67 -26
  218. package/src/vite/discovery/route-types-writer.ts +40 -84
  219. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  220. package/src/vite/discovery/state.ts +33 -0
  221. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  222. package/src/vite/index.ts +2 -0
  223. package/src/vite/plugin-types.ts +67 -0
  224. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  225. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  226. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  228. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  229. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  230. package/src/vite/plugins/expose-action-id.ts +54 -30
  231. package/src/vite/plugins/expose-id-utils.ts +12 -8
  232. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  233. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  234. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  235. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  236. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  237. package/src/vite/plugins/performance-tracks.ts +29 -25
  238. package/src/vite/plugins/use-cache-transform.ts +65 -50
  239. package/src/vite/plugins/version-injector.ts +39 -23
  240. package/src/vite/plugins/version-plugin.ts +59 -2
  241. package/src/vite/plugins/virtual-entries.ts +2 -2
  242. package/src/vite/rango.ts +116 -29
  243. package/src/vite/router-discovery.ts +750 -100
  244. package/src/vite/utils/ast-handler-extract.ts +15 -15
  245. package/src/vite/utils/banner.ts +1 -1
  246. package/src/vite/utils/bundle-analysis.ts +4 -2
  247. package/src/vite/utils/client-chunks.ts +190 -0
  248. package/src/vite/utils/forward-user-plugins.ts +193 -0
  249. package/src/vite/utils/manifest-utils.ts +21 -5
  250. package/src/vite/utils/package-resolution.ts +41 -1
  251. package/src/vite/utils/prerender-utils.ts +21 -6
  252. package/src/vite/utils/shared-utils.ts +107 -26
  253. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,171 @@
1
+ import type { Debugger } from "../debug.js";
2
+
3
+ /**
4
+ * Manifest-readiness gate + rediscovery scheduler.
5
+ *
6
+ * Owns the four pieces of state that cooperate to keep
7
+ * `s.discoveryDone` (the promise the manifest virtual module's `load()`
8
+ * hook awaits) consistent across HMR fan-out:
9
+ *
10
+ * - **gatePending**: a Promise has been issued and not yet resolved.
11
+ * Workerd's manifest virtual module load() is blocked on it.
12
+ * - **inProgress**: a refresh's work callback is currently executing.
13
+ * - **queued**: a refresh was attempted while one was already in
14
+ * flight; the active run consumes this in its `finally` and
15
+ * recurses.
16
+ * - **pendingEvents**: a route-file event has been received (gate
17
+ * already reset) but the corresponding refresh's work hasn't started
18
+ * yet — i.e. the debounce hasn't fired. Set in `noteRouteEvent`,
19
+ * cleared at the start of each refresh cycle. Refresh's finally MUST
20
+ * hold the gate if this is true even when `queued` is false,
21
+ * otherwise an event whose debounce fires AFTER the active refresh
22
+ * completes (the "tail-race" window) would observe a resolved gate.
23
+ *
24
+ * The HMR-event flow (cloudflare-stress repro):
25
+ *
26
+ * t=0 Touch 1 → noteRouteEvent → pendingEvents=true, beginGate
27
+ * (gate1 pending)
28
+ * → debounce 100ms
29
+ * t=100 runRefreshCycle(work) → clear pendingEvents, work starts
30
+ * t=750 Touch 2 → noteRouteEvent → pendingEvents=true (no-op gate)
31
+ * → debounce fires at t=850
32
+ * t=800 refresh A's finally → queued=false, pendingEvents=true
33
+ * → HOLD gate (don't resolve)
34
+ * t=850 runRefreshCycle (debounce) → clear pendingEvents, work starts
35
+ * t=1500 refresh B's finally → queued=false, pendingEvents=false
36
+ * → resolveGate (gate1 resolves)
37
+ *
38
+ * @internal Exported only for unit tests.
39
+ */
40
+ export interface DiscoveryGate {
41
+ /**
42
+ * Reset the gate to a fresh pending Promise via `s.discoveryDone`.
43
+ * No-op when a gate is already pending — file watchers can fire
44
+ * multiple events for one save, and replacing the resolver would
45
+ * orphan the original promise (workerd's manifest load() would hang).
46
+ */
47
+ beginGate(): void;
48
+ /**
49
+ * Resolve the current pending gate. No-op when no gate is pending.
50
+ * Called at the tail of the last refresh cycle in a burst.
51
+ */
52
+ resolveGate(): void;
53
+ /**
54
+ * Record that a route-file event has arrived. Sets `pendingEvents`
55
+ * and begins the gate. Idempotent for both flags.
56
+ */
57
+ noteRouteEvent(): void;
58
+ /**
59
+ * Run one refresh cycle, managing queue + pending state around it.
60
+ * If a cycle is already in flight, sets `queued=true` and returns.
61
+ * Otherwise clears `pendingEvents`, runs `work`, and in `finally`:
62
+ *
63
+ * - queued → recurse, gate stays pending
64
+ * - pendingEvents → hold gate (next debounced cycle resolves)
65
+ * - neither → resolveGate
66
+ */
67
+ runRefreshCycle(work: () => Promise<void>): Promise<void>;
68
+ /** Snapshot of internal state. Test-only. */
69
+ readonly state: () => Readonly<{
70
+ gatePending: boolean;
71
+ inProgress: boolean;
72
+ queued: boolean;
73
+ pendingEvents: boolean;
74
+ }>;
75
+ }
76
+
77
+ /** State container the gate writes `discoveryDone` into. */
78
+ export interface GateOwner {
79
+ discoveryDone: Promise<void> | null | undefined;
80
+ }
81
+
82
+ export function createDiscoveryGate(
83
+ s: GateOwner,
84
+ debug?: Debugger,
85
+ ): DiscoveryGate {
86
+ let gatePending = false;
87
+ let gateResolver: () => void = () => {};
88
+ let inProgress = false;
89
+ let queued = false;
90
+ let pendingEvents = false;
91
+
92
+ const beginGate = (): void => {
93
+ if (gatePending) return;
94
+ s.discoveryDone = new Promise<void>((resolve) => {
95
+ gateResolver = resolve;
96
+ });
97
+ gatePending = true;
98
+ };
99
+
100
+ const resolveGate = (): void => {
101
+ if (!gatePending) return;
102
+ // Defer resolution while a refresh cycle is in flight or queued, or
103
+ // while an unprocessed route-file event is pending its debounce.
104
+ // Without this guard, cold-start's `discover().then(resolveGate)`
105
+ // could fire while an HMR-triggered runRefreshCycle is mid-flight,
106
+ // prematurely unblocking workerd's manifest load() against the
107
+ // stale cold-start gen. The active cycle's `finally` calls
108
+ // resolveGate again at the tail and finishes the resolution then.
109
+ if (inProgress || queued || pendingEvents) {
110
+ debug?.(
111
+ "hmr: resolveGate deferred — work in flight (inProgress=%s queued=%s pendingEvents=%s)",
112
+ inProgress,
113
+ queued,
114
+ pendingEvents,
115
+ );
116
+ return;
117
+ }
118
+ gatePending = false;
119
+ debug?.("hmr: discoveryDone resolved");
120
+ gateResolver();
121
+ };
122
+
123
+ const noteRouteEvent = (): void => {
124
+ pendingEvents = true;
125
+ beginGate();
126
+ };
127
+
128
+ const runRefreshCycle = async (work: () => Promise<void>): Promise<void> => {
129
+ if (inProgress) {
130
+ queued = true;
131
+ debug?.("hmr: rediscovery in flight — queued for a follow-up cycle");
132
+ return;
133
+ }
134
+ // Snapshot the current pendingEvents into "we're about to process";
135
+ // events arriving from now on re-set it.
136
+ pendingEvents = false;
137
+ inProgress = true;
138
+ try {
139
+ await work();
140
+ } finally {
141
+ inProgress = false;
142
+ if (queued) {
143
+ queued = false;
144
+ debug?.("hmr: consuming queued rediscovery");
145
+ runRefreshCycle(work).catch((err: unknown) => {
146
+ debug?.(
147
+ "hmr: queued cycle rejected — releasing gate (%s)",
148
+ err instanceof Error ? err.message : String(err),
149
+ );
150
+ // Belt-and-suspenders: even if the queued cycle's own try/catch
151
+ // missed something, ensure workerd doesn't hang.
152
+ resolveGate();
153
+ });
154
+ } else if (pendingEvents) {
155
+ debug?.(
156
+ "hmr: holding gate for pending events (debounce not yet fired)",
157
+ );
158
+ } else {
159
+ resolveGate();
160
+ }
161
+ }
162
+ };
163
+
164
+ return {
165
+ beginGate,
166
+ resolveGate,
167
+ noteRouteEvent,
168
+ runRefreshCycle,
169
+ state: () => ({ gatePending, inProgress, queued, pendingEvents }),
170
+ };
171
+ }
@@ -16,6 +16,9 @@ import {
16
16
  stageBuildAssetModule,
17
17
  } from "../utils/prerender-utils.js";
18
18
  import type { DiscoveryState } from "./state.js";
19
+ import { createRangoDebugger, NS } from "../debug.js";
20
+
21
+ const debug = createRangoDebugger(NS.prerender);
19
22
 
20
23
  /**
21
24
  * Expand prerender routes into concrete URLs and render them via the
@@ -30,6 +33,12 @@ export async function expandPrerenderRoutes(
30
33
  ): Promise<void> {
31
34
  if (!state.opts?.enableBuildPrerender || !state.isBuildMode) return;
32
35
 
36
+ const overallStart = debug ? performance.now() : 0;
37
+ debug?.(
38
+ "expandPrerenderRoutes: start (%d router manifest(s))",
39
+ allManifests.length,
40
+ );
41
+
33
42
  type PrerenderEntry = {
34
43
  urlPath: string;
35
44
  routeName: string;
@@ -72,7 +81,7 @@ export async function expandPrerenderRoutes(
72
81
  ? setInterval(() => {
73
82
  const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
74
83
  console.log(
75
- `[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
84
+ `[rango] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
76
85
  );
77
86
  }, 5000)
78
87
  : undefined;
@@ -99,6 +108,7 @@ export async function expandPrerenderRoutes(
99
108
  } else {
100
109
  // Dynamic route: call getParams() to enumerate param combinations
101
110
  if (def?.getParams) {
111
+ const getParamsStart = debug ? performance.now() : 0;
102
112
  try {
103
113
  const buildVars: Record<string, any> = {};
104
114
  const buildEnv = state.resolvedBuildEnv;
@@ -112,12 +122,18 @@ export async function expandPrerenderRoutes(
112
122
  get env() {
113
123
  if (buildEnv !== undefined) return buildEnv;
114
124
  throw new Error(
115
- "[rsc-router] ctx.env is not available during build-time getParams(). " +
125
+ "[rango] ctx.env is not available during build-time getParams(). " +
116
126
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
117
127
  );
118
128
  },
119
129
  };
120
130
  const paramsList = await def.getParams(getParamsCtx);
131
+ debug?.(
132
+ "getParams %s -> %d params (%sms)",
133
+ routeName,
134
+ paramsList.length,
135
+ (performance.now() - getParamsStart).toFixed(1),
136
+ );
121
137
  const concurrency = def.options?.concurrency ?? 1;
122
138
  const hasBuildVars =
123
139
  Object.keys(buildVars).length > 0 ||
@@ -154,7 +170,7 @@ export async function expandPrerenderRoutes(
154
170
  // Skip in getParams() skips the entire route
155
171
  if (err.name === "Skip") {
156
172
  console.log(
157
- `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
173
+ `[rango] SKIP route "${routeName}" - ${err.message}`,
158
174
  );
159
175
  notifyOnError(
160
176
  registry,
@@ -168,14 +184,14 @@ export async function expandPrerenderRoutes(
168
184
  }
169
185
  // Regular error: fail the build
170
186
  console.error(
171
- `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
187
+ `[rango] Failed to get params for prerender route "${routeName}": ${err.message}`,
172
188
  );
173
189
  notifyOnError(registry, err, "prerender", routeName);
174
190
  throw err;
175
191
  }
176
192
  } else {
177
193
  console.warn(
178
- `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
194
+ `[rango] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
179
195
  );
180
196
  }
181
197
  }
@@ -186,19 +202,30 @@ export async function expandPrerenderRoutes(
186
202
  clearInterval(progressInterval);
187
203
  const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
188
204
  console.log(
189
- `[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
205
+ `[rango] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
190
206
  );
191
207
  }
192
208
  }
193
209
 
194
- if (entries.length === 0) return;
210
+ if (entries.length === 0) {
211
+ debug?.(
212
+ "no prerender entries (done in %sms)",
213
+ (performance.now() - overallStart).toFixed(1),
214
+ );
215
+ return;
216
+ }
195
217
 
196
218
  // Determine the max concurrency for the log header
197
219
  const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
198
220
  const concurrencyNote =
199
221
  maxConcurrency > 1 ? ` (concurrency: ${maxConcurrency})` : "";
200
222
  console.log(
201
- `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
223
+ `[rango] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
224
+ );
225
+ debug?.(
226
+ "prerender loop: %d entries, max concurrency %d",
227
+ entries.length,
228
+ maxConcurrency,
202
229
  );
203
230
 
204
231
  const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
@@ -234,7 +261,7 @@ export async function expandPrerenderRoutes(
234
261
  if (result.passthrough) {
235
262
  const elapsed = (performance.now() - startUrl).toFixed(0);
236
263
  console.log(
237
- `[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
264
+ `[rango] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
238
265
  );
239
266
  doneCount++;
240
267
  break;
@@ -268,7 +295,7 @@ export async function expandPrerenderRoutes(
268
295
  }
269
296
  const elapsed = (performance.now() - startUrl).toFixed(0);
270
297
  console.log(
271
- `[rsc-router] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
298
+ `[rango] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
272
299
  );
273
300
  doneCount++;
274
301
  break;
@@ -276,7 +303,7 @@ export async function expandPrerenderRoutes(
276
303
  if (err.name === "Skip") {
277
304
  const elapsed = (performance.now() - startUrl).toFixed(0);
278
305
  console.log(
279
- `[rsc-router] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
306
+ `[rango] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
280
307
  );
281
308
  skipCount++;
282
309
  notifyOnError(
@@ -292,7 +319,7 @@ export async function expandPrerenderRoutes(
292
319
  // Regular error: log, notify, and fail the build
293
320
  const elapsed = (performance.now() - startUrl).toFixed(0);
294
321
  console.error(
295
- `[rsc-router] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
322
+ `[rango] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
296
323
  );
297
324
  notifyOnError(
298
325
  registry,
@@ -315,7 +342,14 @@ export async function expandPrerenderRoutes(
315
342
  const parts = [`${doneCount} done`];
316
343
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
317
344
  console.log(
318
- `[rsc-router] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
345
+ `[rango] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
346
+ );
347
+ debug?.(
348
+ "expandPrerenderRoutes done: %d done, %d skipped, %sms (overall %sms)",
349
+ doneCount,
350
+ skipCount,
351
+ totalElapsed,
352
+ (performance.now() - overallStart).toFixed(1),
319
353
  );
320
354
  }
321
355
 
@@ -337,6 +371,12 @@ export async function renderStaticHandlers(
337
371
  )
338
372
  return;
339
373
 
374
+ const overallStart = debug ? performance.now() : 0;
375
+ debug?.(
376
+ "renderStaticHandlers: start (%d static module(s))",
377
+ state.resolvedStaticModules.size,
378
+ );
379
+
340
380
  const manifestEntries: Record<string, string> = {};
341
381
  let staticDone = 0;
342
382
  let staticSkip = 0;
@@ -347,9 +387,7 @@ export async function renderStaticHandlers(
347
387
  totalStaticCount += exportNames.length;
348
388
  }
349
389
  const startStatic = performance.now();
350
- console.log(
351
- `[rsc-router] Rendering ${totalStaticCount} static handler(s)...`,
352
- );
390
+ console.log(`[rango] Rendering ${totalStaticCount} static handler(s)...`);
353
391
 
354
392
  for (const [moduleId, exportNames] of state.resolvedStaticModules) {
355
393
  let mod: any;
@@ -357,7 +395,7 @@ export async function renderStaticHandlers(
357
395
  mod = await rscEnv!.runner.import(moduleId);
358
396
  } catch (err: any) {
359
397
  console.error(
360
- `[rsc-router] Failed to import static module ${moduleId}: ${err.message}`,
398
+ `[rango] Failed to import static module ${moduleId}: ${err.message}`,
361
399
  );
362
400
  notifyOnError(registry, err, "static");
363
401
  throw err;
@@ -392,9 +430,7 @@ export async function renderStaticHandlers(
392
430
  exportValue,
393
431
  );
394
432
  const elapsed = (performance.now() - startHandler).toFixed(0);
395
- console.log(
396
- `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
397
- );
433
+ console.log(`[rango] OK ${name.padEnd(40)} (${elapsed}ms)`);
398
434
  staticDone++;
399
435
  handled = true;
400
436
  break;
@@ -403,7 +439,7 @@ export async function renderStaticHandlers(
403
439
  if (err.name === "Skip") {
404
440
  const elapsed = (performance.now() - startHandler).toFixed(0);
405
441
  console.log(
406
- `[rsc-router] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
442
+ `[rango] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
407
443
  );
408
444
  staticSkip++;
409
445
  notifyOnError(registry, err, "static", undefined, undefined, true);
@@ -413,16 +449,14 @@ export async function renderStaticHandlers(
413
449
  // Regular error: log, notify, and fail the build
414
450
  const elapsed = (performance.now() - startHandler).toFixed(0);
415
451
  console.error(
416
- `[rsc-router] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
452
+ `[rango] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
417
453
  );
418
454
  notifyOnError(registry, err, "static");
419
455
  throw err;
420
456
  }
421
457
  }
422
458
  if (!handled) {
423
- console.warn(
424
- `[rsc-router] No router could render static handler "${name}"`,
425
- );
459
+ console.warn(`[rango] No router could render static handler "${name}"`);
426
460
  }
427
461
  }
428
462
  }
@@ -434,6 +468,13 @@ export async function renderStaticHandlers(
434
468
  const staticParts = [`${staticDone} done`];
435
469
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
436
470
  console.log(
437
- `[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
471
+ `[rango] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
472
+ );
473
+ debug?.(
474
+ "renderStaticHandlers done: %d done, %d skipped, %sms (overall %sms)",
475
+ staticDone,
476
+ staticSkip,
477
+ totalStaticElapsed,
478
+ (performance.now() - overallStart).toFixed(1),
438
479
  );
439
480
  }
@@ -5,13 +5,15 @@
5
5
  * from discovered router manifests and static source parsing.
6
6
  */
7
7
 
8
- import { dirname, basename, join, resolve } from "node:path";
8
+ import { dirname, join, resolve } from "node:path";
9
9
  import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
10
10
  import {
11
11
  generateRouteTypesSource,
12
12
  writeCombinedRouteTypes,
13
13
  findRouterFiles,
14
14
  buildCombinedRouteMapForRouterFile,
15
+ genFileTsPath,
16
+ resolveSearchSchemas,
15
17
  } from "../../build/generate-route-types.js";
16
18
  import type { DiscoveryState } from "./state.js";
17
19
  import { markSelfGenWrite } from "./self-gen-tracking.js";
@@ -35,6 +37,22 @@ function filterUserNamedRoutes(
35
37
  return filtered;
36
38
  }
37
39
 
40
+ // Write a gen file only when content changed, marking the write as
41
+ // self-generated BEFORE writeFileSync so the watcher distinguishes it from a
42
+ // manual edit (the HMR self-gen-loop guard).
43
+ function writeGenFileIfChanged(
44
+ state: DiscoveryState,
45
+ outPath: string,
46
+ source: string,
47
+ opts?: { log?: boolean },
48
+ ): void {
49
+ const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
50
+ if (existing === source) return;
51
+ markSelfGenWrite(state, outPath, source);
52
+ writeFileSync(outPath, source);
53
+ if (opts?.log) console.log(`[rango] Generated route types -> ${outPath}`);
54
+ }
55
+
38
56
  /**
39
57
  * Write combined route types for all router files.
40
58
  * Only writes when content has changed to avoid triggering HMR loops.
@@ -48,45 +66,16 @@ export function writeCombinedRouteTypesWithTracking(
48
66
  findRouterFiles(state.projectRoot, state.scanFilter);
49
67
  state.cachedRouterFiles = routerFiles;
50
68
 
51
- // Snapshot pre-write content to detect which files actually change.
52
- const preContent = new Map<string, string>();
53
- for (const routerFilePath of routerFiles) {
54
- const routerDir = dirname(routerFilePath);
55
- const routerBasename = basename(routerFilePath).replace(
56
- /\.(tsx?|jsx?)$/,
57
- "",
58
- );
59
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
60
- try {
61
- preContent.set(outPath, readFileSync(outPath, "utf-8"));
62
- } catch {
63
- // File doesn't exist yet — any write is a real change.
64
- }
65
- }
66
-
67
- writeCombinedRouteTypes(state.projectRoot, routerFiles, opts);
68
-
69
- // Mark only files that were actually written so the watcher can
70
- // distinguish self-triggered change events from manual edits.
71
- // Marking unchanged files creates stale entries that interfere with
72
- // multi-server setups (e.g. shared webServer + isolated HMR server).
73
- for (const routerFilePath of routerFiles) {
74
- const routerDir = dirname(routerFilePath);
75
- const routerBasename = basename(routerFilePath).replace(
76
- /\.(tsx?|jsx?)$/,
77
- "",
78
- );
79
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
80
- if (!existsSync(outPath)) continue;
81
- try {
82
- const content = readFileSync(outPath, "utf-8");
83
- if (content !== preContent.get(outPath)) {
84
- markSelfGenWrite(state, outPath, content);
85
- }
86
- } catch {
87
- // Ignore transient fs errors while files are being rewritten.
88
- }
89
- }
69
+ // Mark each gen file as self-generated BEFORE it is written, via the onWrite
70
+ // callback fired at every writeFileSync site, so the watcher distinguishes
71
+ // self-triggered change events from manual edits. The callback fires only
72
+ // for files actually written, so unchanged files are never marked (stale
73
+ // entries interfere with multi-server setups such as a shared webServer plus
74
+ // an isolated HMR server).
75
+ writeCombinedRouteTypes(state.projectRoot, routerFiles, {
76
+ ...opts,
77
+ onWrite: (outPath, content) => markSelfGenWrite(state, outPath, content),
78
+ });
90
79
  }
91
80
 
92
81
  /**
@@ -104,7 +93,7 @@ export function writeRouteTypesFiles(state: DiscoveryState): void {
104
93
  if (existsSync(oldCombinedPath)) {
105
94
  unlinkSync(oldCombinedPath);
106
95
  console.log(
107
- `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
96
+ `[rango] Removed stale combined route types: ${oldCombinedPath}`,
108
97
  );
109
98
  }
110
99
  } catch {}
@@ -122,40 +111,22 @@ export function writeRouteTypesFiles(state: DiscoveryState): void {
122
111
  // the wrong location, causing non-deterministic type resolution.
123
112
  if (sourceFile.includes("node_modules")) {
124
113
  throw new Error(
125
- `[rsc-router] Router "${id}" has sourceFile inside node_modules: ${sourceFile}\n` +
114
+ `[rango] Router "${id}" has sourceFile inside node_modules: ${sourceFile}\n` +
126
115
  `This means createRouter() stack trace parsing matched a Vite internal frame.\n` +
127
116
  `Set an explicit \`id\` on createRouter() or check the call site.`,
128
117
  );
129
118
  }
130
119
 
131
- const routerDir = dirname(sourceFile);
132
- const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
133
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
120
+ const outPath = genFileTsPath(sourceFile);
134
121
 
135
122
  // Filter out auto-generated route names (e.g. "$path____debug_reverse-test")
136
123
  // to match the static parser's output and prevent HMR oscillation.
137
124
  const userRoutes = filterUserNamedRoutes(routeManifest);
138
- let effectiveSearchSchemas = routeSearchSchemas;
139
-
140
- // Runtime manifest may omit search schema metadata in some module-runner
141
- // flows. Fall back to static source parsing from the router file.
142
- if (
143
- (!effectiveSearchSchemas ||
144
- Object.keys(effectiveSearchSchemas).length === 0) &&
145
- sourceFile
146
- ) {
147
- const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
148
- if (Object.keys(staticParsed.searchSchemas).length > 0) {
149
- const filtered: Record<string, Record<string, string>> = {};
150
- for (const name of Object.keys(userRoutes)) {
151
- const schema = staticParsed.searchSchemas[name];
152
- if (schema) filtered[name] = schema;
153
- }
154
- if (Object.keys(filtered).length > 0) {
155
- effectiveSearchSchemas = filtered;
156
- }
157
- }
158
- }
125
+ const effectiveSearchSchemas = resolveSearchSchemas(
126
+ Object.keys(userRoutes),
127
+ routeSearchSchemas,
128
+ sourceFile,
129
+ );
159
130
 
160
131
  const source = generateRouteTypesSource(
161
132
  userRoutes,
@@ -163,14 +134,7 @@ export function writeRouteTypesFiles(state: DiscoveryState): void {
163
134
  ? effectiveSearchSchemas
164
135
  : undefined,
165
136
  );
166
- const existing = existsSync(outPath)
167
- ? readFileSync(outPath, "utf-8")
168
- : null;
169
- if (existing !== source) {
170
- markSelfGenWrite(state, outPath, source);
171
- writeFileSync(outPath, source);
172
- console.log(`[rsc-router] Generated route types -> ${outPath}`);
173
- }
137
+ writeGenFileIfChanged(state, outPath, source, { log: true });
174
138
  }
175
139
  }
176
140
 
@@ -236,22 +200,14 @@ export function supplementGenFilesWithRuntimeRoutes(
236
200
  }
237
201
  }
238
202
 
239
- const routerDir = dirname(sourceFile);
240
- const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
241
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
203
+ const outPath = genFileTsPath(sourceFile);
242
204
  const source = generateRouteTypesSource(
243
205
  mergedRoutes,
244
206
  Object.keys(mergedSearchSchemas).length > 0
245
207
  ? mergedSearchSchemas
246
208
  : undefined,
247
209
  );
248
- const existing = existsSync(outPath)
249
- ? readFileSync(outPath, "utf-8")
250
- : null;
251
- if (existing !== source) {
252
- markSelfGenWrite(state, outPath, source);
253
- writeFileSync(outPath, source);
254
- }
210
+ writeGenFileIfChanged(state, outPath, source);
255
211
  }
256
212
  // No manual manifest update needed: the virtual module imports the gen
257
213
  // file, so Vite's HMR automatically re-evaluates it with fresh data.
@@ -22,6 +22,32 @@ export function markSelfGenWrite(
22
22
  export function consumeSelfGenWrite(
23
23
  state: DiscoveryState,
24
24
  filePath: string,
25
+ ): boolean {
26
+ return checkSelfGenWrite(state, filePath, true);
27
+ }
28
+
29
+ /**
30
+ * Non-consuming variant. Used by the `handleHotUpdate` plugin hook to
31
+ * suppress vite's HMR cascade for our own gen-file writes WITHOUT
32
+ * consuming the entry — `consumeSelfGenWrite` (called later from the
33
+ * chokidar `change` handler in `handleRouteFileChange`) still needs to
34
+ * see and consume the same entry to short-circuit our regen path.
35
+ *
36
+ * Both hooks fire for the same file change event:
37
+ * - `handleHotUpdate` runs first (vite's HMR pipeline).
38
+ * - chokidar `change` callback runs after (filesystem watcher).
39
+ */
40
+ export function peekSelfGenWrite(
41
+ state: DiscoveryState,
42
+ filePath: string,
43
+ ): boolean {
44
+ return checkSelfGenWrite(state, filePath, false);
45
+ }
46
+
47
+ function checkSelfGenWrite(
48
+ state: DiscoveryState,
49
+ filePath: string,
50
+ consume: boolean,
25
51
  ): boolean {
26
52
  const info = state.selfWrittenGenFiles.get(filePath);
27
53
  if (!info) return false;
@@ -33,7 +59,7 @@ export function consumeSelfGenWrite(
33
59
  const current = readFileSync(filePath, "utf-8");
34
60
  const currentHash = createHash("sha256").update(current).digest("hex");
35
61
  if (currentHash === info.hash) {
36
- state.selfWrittenGenFiles.delete(filePath);
62
+ if (consume) state.selfWrittenGenFiles.delete(filePath);
37
63
  return true;
38
64
  }
39
65
  // Hash mismatch: file was changed externally. Keep the entry so a