@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -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
+ }
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Prerender Collection
3
+ *
4
+ * Expands prerender routes into concrete URLs and renders them at build
5
+ * time. Also handles Static handler rendering for segment-level static
6
+ * generation.
7
+ */
8
+
9
+ import { contextSet } from "../../context-var.js";
10
+ import {
11
+ encodePathParam,
12
+ substituteRouteParams,
13
+ runWithConcurrency,
14
+ groupByConcurrency,
15
+ notifyOnError,
16
+ stageBuildAssetModule,
17
+ } from "../utils/prerender-utils.js";
18
+ import type { DiscoveryState } from "./state.js";
19
+ import { createRangoDebugger, NS } from "../debug.js";
20
+
21
+ const debug = createRangoDebugger(NS.prerender);
22
+
23
+ /**
24
+ * Expand prerender routes into concrete URLs and render them via the
25
+ * RSC runner. Stages asset modules and stores key-to-file entries in
26
+ * state.prerenderManifestEntries.
27
+ */
28
+ export async function expandPrerenderRoutes(
29
+ state: DiscoveryState,
30
+ rscEnv: any,
31
+ registry: Map<string, any>,
32
+ allManifests: Array<{ id: string; manifest: any }>,
33
+ ): Promise<void> {
34
+ if (!state.opts?.enableBuildPrerender || !state.isBuildMode) return;
35
+
36
+ const overallStart = debug ? performance.now() : 0;
37
+ debug?.(
38
+ "expandPrerenderRoutes: start (%d router manifest(s))",
39
+ allManifests.length,
40
+ );
41
+
42
+ type PrerenderEntry = {
43
+ urlPath: string;
44
+ routeName: string;
45
+ concurrency: number;
46
+ buildVars?: Record<string, any>;
47
+ isPassthroughRoute?: boolean;
48
+ };
49
+ const entries: PrerenderEntry[] = [];
50
+
51
+ // Build a merged route map for getParams context reverse()
52
+ const allRoutes: Record<string, string> = {};
53
+ for (const { manifest: m } of allManifests) {
54
+ if (m.routeManifest) Object.assign(allRoutes, m.routeManifest);
55
+ }
56
+ const getParamsReverse = (name: string, params?: Record<string, string>) => {
57
+ const pattern = allRoutes[name];
58
+ if (!pattern) throw new Error(`Unknown route: "${name}"`);
59
+ if (!params) return pattern;
60
+ return substituteRouteParams(pattern, params);
61
+ };
62
+
63
+ let resolvedRoutes = 0;
64
+ let totalDynamic = 0;
65
+
66
+ // Count dynamic routes upfront for progress reporting
67
+ for (const { manifest } of allManifests) {
68
+ if (!manifest.prerenderRoutes) continue;
69
+ for (const routeName of manifest.prerenderRoutes) {
70
+ const pattern = manifest.routeManifest[routeName];
71
+ if (pattern && (pattern.includes(":") || pattern.includes("*"))) {
72
+ totalDynamic++;
73
+ }
74
+ }
75
+ }
76
+
77
+ // Periodic progress log so long getParams() calls don't look stalled
78
+ const paramsStart = performance.now();
79
+ const progressInterval =
80
+ totalDynamic > 0
81
+ ? setInterval(() => {
82
+ const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
83
+ console.log(
84
+ `[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
85
+ );
86
+ }, 5000)
87
+ : undefined;
88
+
89
+ try {
90
+ for (const { manifest } of allManifests) {
91
+ if (!manifest.prerenderRoutes) continue;
92
+ const defs = manifest._prerenderDefs || {};
93
+ const passthroughSet = new Set(manifest.passthroughRoutes || []);
94
+ for (const routeName of manifest.prerenderRoutes) {
95
+ const pattern = manifest.routeManifest[routeName];
96
+ if (!pattern) continue;
97
+ const def = defs[routeName];
98
+ const isPassthroughRoute = passthroughSet.has(routeName);
99
+ const hasDynamic = pattern.includes(":") || pattern.includes("*");
100
+ if (!hasDynamic) {
101
+ // Static route: use pattern directly (strip trailing slash for URL)
102
+ entries.push({
103
+ urlPath: pattern.replace(/\/$/, "") || "/",
104
+ routeName,
105
+ concurrency: 1,
106
+ isPassthroughRoute,
107
+ });
108
+ } else {
109
+ // Dynamic route: call getParams() to enumerate param combinations
110
+ if (def?.getParams) {
111
+ const getParamsStart = debug ? performance.now() : 0;
112
+ try {
113
+ const buildVars: Record<string, any> = {};
114
+ const buildEnv = state.resolvedBuildEnv;
115
+ const getParamsCtx = {
116
+ build: true as const,
117
+ dev: !state.isBuildMode,
118
+ set: ((keyOrVar: any, value: any) => {
119
+ contextSet(buildVars, keyOrVar, value);
120
+ }) as any,
121
+ reverse: getParamsReverse,
122
+ get env() {
123
+ if (buildEnv !== undefined) return buildEnv;
124
+ throw new Error(
125
+ "[rsc-router] ctx.env is not available during build-time getParams(). " +
126
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
127
+ );
128
+ },
129
+ };
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
+ );
137
+ const concurrency = def.options?.concurrency ?? 1;
138
+ const hasBuildVars =
139
+ Object.keys(buildVars).length > 0 ||
140
+ Object.getOwnPropertySymbols(buildVars).length > 0;
141
+ for (const params of paramsList) {
142
+ let url = substituteRouteParams(
143
+ pattern,
144
+ params as Record<string, string>,
145
+ encodePathParam,
146
+ );
147
+ // Anonymous wildcard fallback: use conventional keys if provided
148
+ if (url.includes("*")) {
149
+ const wildcardValue =
150
+ (params as Record<string, string>)["*"] ??
151
+ (params as Record<string, string>).splat;
152
+ if (wildcardValue !== undefined) {
153
+ url = url.replace(
154
+ /\*[^/]*$/,
155
+ encodePathParam(wildcardValue),
156
+ );
157
+ }
158
+ }
159
+ entries.push({
160
+ urlPath: url.replace(/\/$/, "") || "/",
161
+ routeName,
162
+ concurrency,
163
+ ...(hasBuildVars ? { buildVars } : {}),
164
+ isPassthroughRoute,
165
+ });
166
+ }
167
+ resolvedRoutes++;
168
+ } catch (err: any) {
169
+ resolvedRoutes++;
170
+ // Skip in getParams() skips the entire route
171
+ if (err.name === "Skip") {
172
+ console.log(
173
+ `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
174
+ );
175
+ notifyOnError(
176
+ registry,
177
+ err,
178
+ "prerender",
179
+ routeName,
180
+ undefined,
181
+ true,
182
+ );
183
+ continue;
184
+ }
185
+ // Regular error: fail the build
186
+ console.error(
187
+ `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
188
+ );
189
+ notifyOnError(registry, err, "prerender", routeName);
190
+ throw err;
191
+ }
192
+ } else {
193
+ console.warn(
194
+ `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
195
+ );
196
+ }
197
+ }
198
+ }
199
+ }
200
+ } finally {
201
+ if (progressInterval) {
202
+ clearInterval(progressInterval);
203
+ const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
204
+ console.log(
205
+ `[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
206
+ );
207
+ }
208
+ }
209
+
210
+ if (entries.length === 0) {
211
+ debug?.(
212
+ "no prerender entries (done in %sms)",
213
+ (performance.now() - overallStart).toFixed(1),
214
+ );
215
+ return;
216
+ }
217
+
218
+ // Determine the max concurrency for the log header
219
+ const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
220
+ const concurrencyNote =
221
+ maxConcurrency > 1 ? ` (concurrency: ${maxConcurrency})` : "";
222
+ console.log(
223
+ `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
224
+ );
225
+ debug?.(
226
+ "prerender loop: %d entries, max concurrency %d",
227
+ entries.length,
228
+ maxConcurrency,
229
+ );
230
+
231
+ const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
232
+
233
+ const manifestEntries: Record<string, string> = {};
234
+ let doneCount = 0;
235
+ let skipCount = 0;
236
+ const startTotal = performance.now();
237
+
238
+ // Group entries by concurrency for batched rendering.
239
+ // Within each group, all entries share the same concurrency limit.
240
+ const groups = groupByConcurrency(entries);
241
+
242
+ for (const group of groups) {
243
+ await runWithConcurrency(
244
+ group.entries,
245
+ group.concurrency,
246
+ async (entry) => {
247
+ const startUrl = performance.now();
248
+ for (const [, routerInstance] of registry) {
249
+ if (!routerInstance.matchForPrerender) continue;
250
+ try {
251
+ const result = await routerInstance.matchForPrerender(
252
+ entry.urlPath,
253
+ {},
254
+ entry.buildVars,
255
+ entry.isPassthroughRoute,
256
+ state.resolvedBuildEnv,
257
+ );
258
+ if (!result) continue;
259
+
260
+ // Handler returned ctx.passthrough() — skip manifest entry
261
+ if (result.passthrough) {
262
+ const elapsed = (performance.now() - startUrl).toFixed(0);
263
+ console.log(
264
+ `[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
265
+ );
266
+ doneCount++;
267
+ break;
268
+ }
269
+
270
+ const paramHash = hashParams(result.params || {});
271
+ const mainKey = `${result.routeName}/${paramHash}`;
272
+ const mainValue = JSON.stringify({
273
+ segments: result.segments,
274
+ handles: result.handles,
275
+ });
276
+ manifestEntries[mainKey] = stageBuildAssetModule(
277
+ state.projectRoot,
278
+ "__pr",
279
+ mainValue,
280
+ );
281
+ if (result.interceptSegments?.length) {
282
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
283
+ const interceptValue = JSON.stringify({
284
+ segments: [...result.segments, ...result.interceptSegments],
285
+ handles: {
286
+ ...result.handles,
287
+ ...(result.interceptHandles || {}),
288
+ },
289
+ });
290
+ manifestEntries[interceptKey] = stageBuildAssetModule(
291
+ state.projectRoot,
292
+ "__pr",
293
+ interceptValue,
294
+ );
295
+ }
296
+ const elapsed = (performance.now() - startUrl).toFixed(0);
297
+ console.log(
298
+ `[rsc-router] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
299
+ );
300
+ doneCount++;
301
+ break;
302
+ } catch (err: any) {
303
+ if (err.name === "Skip") {
304
+ const elapsed = (performance.now() - startUrl).toFixed(0);
305
+ console.log(
306
+ `[rsc-router] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
307
+ );
308
+ skipCount++;
309
+ notifyOnError(
310
+ registry,
311
+ err,
312
+ "prerender",
313
+ entry.routeName,
314
+ entry.urlPath,
315
+ true,
316
+ );
317
+ break;
318
+ }
319
+ // Regular error: log, notify, and fail the build
320
+ const elapsed = (performance.now() - startUrl).toFixed(0);
321
+ console.error(
322
+ `[rsc-router] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
323
+ );
324
+ notifyOnError(
325
+ registry,
326
+ err,
327
+ "prerender",
328
+ entry.routeName,
329
+ entry.urlPath,
330
+ );
331
+ throw err;
332
+ }
333
+ }
334
+ },
335
+ );
336
+ }
337
+
338
+ const totalElapsed = (performance.now() - startTotal).toFixed(0);
339
+ if (doneCount > 0) {
340
+ state.prerenderManifestEntries = manifestEntries;
341
+ }
342
+ const parts = [`${doneCount} done`];
343
+ if (skipCount > 0) parts.push(`${skipCount} skipped`);
344
+ console.log(
345
+ `[rsc-router] 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),
353
+ );
354
+ }
355
+
356
+ /**
357
+ * Render Static handlers at build time. Each Static handler is called
358
+ * with a synthetic BuildContext and its output is RSC-serialized.
359
+ * Stages asset modules and stores handlerId-to-file entries in
360
+ * state.staticManifestEntries.
361
+ */
362
+ export async function renderStaticHandlers(
363
+ state: DiscoveryState,
364
+ rscEnv: any,
365
+ registry: Map<string, any>,
366
+ ): Promise<void> {
367
+ if (
368
+ !state.opts?.enableBuildPrerender ||
369
+ !state.isBuildMode ||
370
+ !state.resolvedStaticModules?.size
371
+ )
372
+ return;
373
+
374
+ const overallStart = debug ? performance.now() : 0;
375
+ debug?.(
376
+ "renderStaticHandlers: start (%d static module(s))",
377
+ state.resolvedStaticModules.size,
378
+ );
379
+
380
+ const manifestEntries: Record<string, string> = {};
381
+ let staticDone = 0;
382
+ let staticSkip = 0;
383
+ let totalStaticCount = 0;
384
+
385
+ // Count handlers for the log header
386
+ for (const [, exportNames] of state.resolvedStaticModules) {
387
+ totalStaticCount += exportNames.length;
388
+ }
389
+ const startStatic = performance.now();
390
+ console.log(
391
+ `[rsc-router] Rendering ${totalStaticCount} static handler(s)...`,
392
+ );
393
+
394
+ for (const [moduleId, exportNames] of state.resolvedStaticModules) {
395
+ let mod: any;
396
+ try {
397
+ mod = await rscEnv!.runner.import(moduleId);
398
+ } catch (err: any) {
399
+ console.error(
400
+ `[rsc-router] Failed to import static module ${moduleId}: ${err.message}`,
401
+ );
402
+ notifyOnError(registry, err, "static");
403
+ throw err;
404
+ }
405
+
406
+ for (const name of exportNames) {
407
+ const def = mod[name];
408
+ if (!def || def.__brand !== "staticHandler" || !def.$$id) continue;
409
+ // Passthrough handlers stay live in the bundle
410
+ if (def.options?.passthrough) continue;
411
+
412
+ const startHandler = performance.now();
413
+ let handled = false;
414
+ for (const [, routerInstance] of registry) {
415
+ if (!routerInstance.renderStaticSegment) continue;
416
+ try {
417
+ const result = await routerInstance.renderStaticSegment(
418
+ def.handler,
419
+ def.$$id,
420
+ (def as any).$$routePrefix,
421
+ state.resolvedBuildEnv,
422
+ !state.isBuildMode,
423
+ );
424
+ if (result) {
425
+ const hasHandles = Object.keys(result.handles).length > 0;
426
+ const exportValue = hasHandles
427
+ ? JSON.stringify(result)
428
+ : JSON.stringify(result.encoded);
429
+ manifestEntries[def.$$id] = stageBuildAssetModule(
430
+ state.projectRoot,
431
+ "__st",
432
+ exportValue,
433
+ );
434
+ const elapsed = (performance.now() - startHandler).toFixed(0);
435
+ console.log(
436
+ `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
437
+ );
438
+ staticDone++;
439
+ handled = true;
440
+ break;
441
+ }
442
+ } catch (err: any) {
443
+ if (err.name === "Skip") {
444
+ const elapsed = (performance.now() - startHandler).toFixed(0);
445
+ console.log(
446
+ `[rsc-router] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
447
+ );
448
+ staticSkip++;
449
+ notifyOnError(registry, err, "static", undefined, undefined, true);
450
+ handled = true;
451
+ break;
452
+ }
453
+ // Regular error: log, notify, and fail the build
454
+ const elapsed = (performance.now() - startHandler).toFixed(0);
455
+ console.error(
456
+ `[rsc-router] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
457
+ );
458
+ notifyOnError(registry, err, "static");
459
+ throw err;
460
+ }
461
+ }
462
+ if (!handled) {
463
+ console.warn(
464
+ `[rsc-router] No router could render static handler "${name}"`,
465
+ );
466
+ }
467
+ }
468
+ }
469
+
470
+ const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
471
+ if (staticDone > 0) {
472
+ state.staticManifestEntries = manifestEntries;
473
+ }
474
+ const staticParts = [`${staticDone} done`];
475
+ if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
476
+ console.log(
477
+ `[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
478
+ );
479
+ debug?.(
480
+ "renderStaticHandlers done: %d done, %d skipped, %sms (overall %sms)",
481
+ staticDone,
482
+ staticSkip,
483
+ totalStaticElapsed,
484
+ (performance.now() - overallStart).toFixed(1),
485
+ );
486
+ }