@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -11,6 +11,16 @@ import {
11
11
  formatNestedRouterConflictError,
12
12
  findNestedRouterConflict,
13
13
  } from "../../build/generate-route-types.js";
14
+ // Pure data transforms over generateManifestFull's output. Imported directly
15
+ // from source (not the public ./build barrel, and not the runner) because they
16
+ // are realm-independent: buildRouteTrie/buildPerRouterTrie operate on plain
17
+ // manifest data, and collectFallbackClientRefs keys on the global-registry
18
+ // Symbol.for("react.client.reference"), so it detects client references in a
19
+ // boundary tree regardless of which realm imported the walker. Only
20
+ // generateManifestFull must stay on the runner (it invokes user handlers via
21
+ // RangoContext from the runner realm) — see the runner.import below.
22
+ import { buildRouteTrie, buildPerRouterTrie } from "../../build/route-trie.js";
23
+ import { collectFallbackClientRefs } from "../../build/collect-fallback-refs.js";
14
24
  import {
15
25
  flattenLeafEntries,
16
26
  buildRouteToStaticPrefix,
@@ -20,7 +30,13 @@ import {
20
30
  expandPrerenderRoutes,
21
31
  renderStaticHandlers,
22
32
  } from "./prerender-collection.js";
33
+ import {
34
+ resolveHostRouterHandlers,
35
+ DiscoveryError,
36
+ type CaughtDiscoveryError,
37
+ } from "./discovery-errors.js";
23
38
  import { createRangoDebugger, timed, NS } from "../debug.js";
39
+ import { computeProductionHash } from "../plugins/client-ref-hashing.js";
24
40
 
25
41
  const debug = createRangoDebugger(NS.discovery);
26
42
 
@@ -56,32 +72,25 @@ export async function discoverRouters(
56
72
  if (!registry || registry.size === 0) {
57
73
  // No RSC routers found directly. Check for host routers with lazy handlers
58
74
  // that need to be resolved to trigger sub-app createRouter() calls.
75
+ //
76
+ // Handler failures are collected rather than swallowed: when the registry
77
+ // is still empty afterwards, these errors (typically a sub-app whose router
78
+ // module failed to import) are the most likely cause and are surfaced in
79
+ // the terminal "No routers found" error below.
80
+ const discoveryErrors: CaughtDiscoveryError[] = [];
59
81
  try {
60
82
  const hostRegistry: Map<string, any> | undefined =
61
83
  serverMod.HostRouterRegistry;
62
84
 
63
85
  if (hostRegistry && hostRegistry.size > 0) {
64
86
  console.log(
65
- `[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`,
87
+ `[rango] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`,
66
88
  );
67
89
 
68
- for (const [, entry] of hostRegistry) {
69
- for (const route of entry.routes) {
70
- if (typeof route.handler === "function") {
71
- try {
72
- await route.handler();
73
- } catch {
74
- // Lazy handler may fail in temp server context, that's OK
75
- }
76
- }
77
- }
78
- if (entry.fallback && typeof entry.fallback.handler === "function") {
79
- try {
80
- await entry.fallback.handler();
81
- } catch {
82
- // Fallback handler may fail in temp server context
83
- }
84
- }
90
+ const handlerErrors = await resolveHostRouterHandlers(hostRegistry);
91
+ discoveryErrors.push(...handlerErrors);
92
+ for (const { context, error } of handlerErrors) {
93
+ debug?.("caught error while resolving %s: %O", context, error);
85
94
  }
86
95
 
87
96
  // Re-read RouterRegistry - sub-app createRouter() calls should have populated it
@@ -96,19 +105,22 @@ export async function discoverRouters(
96
105
  registry = freshRegistry;
97
106
  }
98
107
  }
99
- } catch {
100
- // Host-router discovery is best-effort; skip if unavailable
108
+ } catch (error) {
109
+ // Host-router discovery is best-effort; record the failure so it can be
110
+ // surfaced if no routers are found.
111
+ discoveryErrors.push({ context: "host-router discovery", error });
101
112
  }
102
113
 
103
114
  // If still no routers after host router resolution, fail
104
115
  if (!registry || registry.size === 0) {
105
- throw new Error(
106
- `[rsc-router] No routers found in registry after importing ${state.resolvedEntryPath}`,
107
- );
116
+ throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
108
117
  }
109
118
  }
110
119
 
111
- // Import build utilities for manifest generation
120
+ // generateManifestFull must run in the RSC runner realm: it invokes the
121
+ // user's urlpatterns.handler() via RangoContext, consuming router instances
122
+ // from the runner. The trie/fallback-ref builders are pure transforms over
123
+ // its output and are imported directly from source above.
112
124
  const buildMod = await timed(
113
125
  debug,
114
126
  "inner: import @rangojs/router/build",
@@ -145,6 +157,29 @@ export async function discoverRouters(
145
157
  // Collect all manifests for trie building (avoid re-running generateManifest)
146
158
  const allManifests: Array<{ id: string; manifest: any }> = [];
147
159
 
160
+ // Built-in clientChunks context (present only when the built-in strategy is
161
+ // active). Collect the production hashes of "use client" error/notFound
162
+ // fallback modules so the strategy can route them into app-fallback.
163
+ const clientChunkCtx = state.opts?.clientChunkCtx;
164
+ const collectClientFallbackRef = clientChunkCtx
165
+ ? (refKey: string) =>
166
+ clientChunkCtx.fallbackRefs.add(
167
+ computeProductionHash(state.projectRoot, refKey),
168
+ )
169
+ : undefined;
170
+ // Router-level boundary defaults (`createRouter({ defaultErrorBoundary, ... })`)
171
+ // are NOT in EntryData, so generateManifestFull's walk misses them. Collect any
172
+ // "use client" default boundary directly off the router instance. The value is
173
+ // commonly a handler function wrapping the client boundary in server providers,
174
+ // so collectFallbackClientRefs invokes + walks the tree. The walker keys on the
175
+ // global-registry Symbol.for("react.client.reference"), so it detects client
176
+ // references in a runner-realm boundary tree even when imported here directly.
177
+ const collectFromBoundaryNode = (node: unknown): void => {
178
+ if (collectClientFallbackRef) {
179
+ collectFallbackClientRefs(node, collectClientFallbackRef);
180
+ }
181
+ };
182
+
148
183
  const manifestGenStart = debug ? performance.now() : 0;
149
184
  for (const [id, router] of registry) {
150
185
  if (!router.urlpatterns || !generateManifestFull) {
@@ -154,10 +189,23 @@ export async function discoverRouters(
154
189
  const manifest = generateManifestFull(
155
190
  router.urlpatterns,
156
191
  routerMountIndex,
157
- router.__basename ? { urlPrefix: router.__basename } : undefined,
192
+ {
193
+ ...(router.__basename ? { urlPrefix: router.__basename } : {}),
194
+ ...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
195
+ },
158
196
  );
159
197
  routerMountIndex++;
160
198
  allManifests.push({ id, manifest });
199
+
200
+ // Router-level "use client" boundary defaults -> app-fallback (the
201
+ // route-tree errorBoundary()/notFoundBoundary() helpers are already
202
+ // collected inside generateManifestFull via collectClientFallbackRef).
203
+ if (collectClientFallbackRef) {
204
+ collectFromBoundaryNode(router.__defaultErrorBoundary);
205
+ collectFromBoundaryNode(router.__defaultNotFoundBoundary);
206
+ collectFromBoundaryNode(router.__notFound);
207
+ }
208
+
161
209
  const routeCount = Object.keys(manifest.routeManifest).length;
162
210
  const staticRoutes = Object.values(manifest.routeManifest).filter(
163
211
  (p: any) => !p.includes(":") && !p.includes("*"),
@@ -209,24 +257,23 @@ export async function discoverRouters(
209
257
  // Flatten prefix tree leaf nodes into precomputed entries.
210
258
  // Leaf nodes (no children) can have their routes used directly by
211
259
  // evaluateLazyEntry() without running the handler at runtime.
260
+ // Walk once into a per-router array, then fold it into the merged array;
261
+ // the merged and per-router entries are identical, so a second walk is
262
+ // redundant. Append order is preserved within and across routers.
263
+ const routerPrecomputed: PrecomputedEntry[] = [];
212
264
  flattenLeafEntries(
213
265
  manifest.prefixTree,
214
266
  manifest.routeManifest,
215
- newMergedPrecomputedEntries,
267
+ routerPrecomputed,
216
268
  );
269
+ newMergedPrecomputedEntries.push(...routerPrecomputed);
217
270
 
218
271
  // Store per-router manifest and precomputed entries for isolated virtual modules.
219
272
  newPerRouterManifestDataMap.set(id, manifest.routeManifest);
220
- const routerPrecomputed: PrecomputedEntry[] = [];
221
- flattenLeafEntries(
222
- manifest.prefixTree,
223
- manifest.routeManifest,
224
- routerPrecomputed,
225
- );
226
273
  newPerRouterPrecomputedMap.set(id, routerPrecomputed);
227
274
 
228
275
  console.log(
229
- `[rsc-router] Router "${id}" -> ${routeCount} routes ` +
276
+ `[rango] Router "${id}" -> ${routeCount} routes ` +
230
277
  `(${staticRoutes} static, ${dynamicRoutes} dynamic)`,
231
278
  );
232
279
  }
@@ -242,7 +289,7 @@ export async function discoverRouters(
242
289
  );
243
290
  if (autoIds.length > 1) {
244
291
  console.warn(
245
- `[rsc-router] WARNING: ${autoIds.length} routers use auto-generated IDs (${autoIds.join(", ")}). ` +
292
+ `[rango] WARNING: ${autoIds.length} routers use auto-generated IDs (${autoIds.join(", ")}). ` +
246
293
  `In multi-router setups, each createRouter() must have an explicit \`id\` option ` +
247
294
  `to ensure per-router manifest data is matched correctly at runtime. ` +
248
295
  `Example: createRouter({ id: "site", ... })`,
@@ -260,8 +307,7 @@ export async function discoverRouters(
260
307
  let newMergedRouteTrie: any = null;
261
308
  const trieStart = debug ? performance.now() : 0;
262
309
  if (Object.keys(newMergedRouteManifest).length > 0) {
263
- const buildRouteTrie = buildMod.buildRouteTrie;
264
- if (buildRouteTrie && mergedRouteAncestry) {
310
+ if (mergedRouteAncestry) {
265
311
  // Build routeToStaticPrefix from saved manifests
266
312
  const routeToStaticPrefix: Record<string, string> = {};
267
313
  for (const { manifest } of allManifests) {
@@ -294,60 +340,27 @@ export async function discoverRouters(
294
340
  }
295
341
  }
296
342
 
343
+ // buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
344
+ // identical to undefined, so no empty->undefined coercion is needed.
297
345
  newMergedRouteTrie = buildRouteTrie(
298
346
  newMergedRouteManifest,
299
347
  mergedRouteAncestry,
300
348
  routeToStaticPrefix,
301
- Object.keys(mergedRouteTrailingSlash).length > 0
302
- ? mergedRouteTrailingSlash
303
- : undefined,
304
- prerenderRouteNames.size > 0 ? prerenderRouteNames : undefined,
305
- passthroughRouteNames.size > 0 ? passthroughRouteNames : undefined,
306
- Object.keys(mergedResponseTypeRoutes).length > 0
307
- ? mergedResponseTypeRoutes
308
- : undefined,
349
+ mergedRouteTrailingSlash,
350
+ prerenderRouteNames,
351
+ passthroughRouteNames,
352
+ mergedResponseTypeRoutes,
309
353
  );
310
354
 
311
- // Build per-router tries for multi-router isolation.
355
+ // Build per-router tries for multi-router isolation. Uses the single
356
+ // shared buildPerRouterTrie so the production serialized trie is built by
357
+ // exactly the same code as the dev/HMR runtime rebuild (manifest-init.ts).
358
+ // Returns null for route-less manifests (route-trie.ts).
312
359
  for (const { id, manifest } of allManifests) {
313
- if (
314
- !manifest._routeAncestry ||
315
- Object.keys(manifest._routeAncestry).length === 0
316
- )
317
- continue;
318
- const perRouterStaticPrefix: Record<string, string> = {};
319
- for (const name of Object.keys(manifest.routeManifest)) {
320
- perRouterStaticPrefix[name] = "";
360
+ const perRouterTrie = buildPerRouterTrie(manifest);
361
+ if (perRouterTrie) {
362
+ newPerRouterTrieMap.set(id, perRouterTrie);
321
363
  }
322
- buildRouteToStaticPrefix(manifest.prefixTree, perRouterStaticPrefix);
323
-
324
- const perRouterPrerenderNames = manifest.prerenderRoutes
325
- ? new Set<string>(manifest.prerenderRoutes)
326
- : undefined;
327
- const perRouterPassthroughNames = manifest.passthroughRoutes
328
- ? new Set<string>(manifest.passthroughRoutes)
329
- : undefined;
330
-
331
- const perRouterTrie = buildRouteTrie(
332
- manifest.routeManifest,
333
- manifest._routeAncestry,
334
- perRouterStaticPrefix,
335
- manifest.routeTrailingSlash &&
336
- Object.keys(manifest.routeTrailingSlash).length > 0
337
- ? manifest.routeTrailingSlash
338
- : undefined,
339
- perRouterPrerenderNames && perRouterPrerenderNames.size > 0
340
- ? perRouterPrerenderNames
341
- : undefined,
342
- perRouterPassthroughNames && perRouterPassthroughNames.size > 0
343
- ? perRouterPassthroughNames
344
- : undefined,
345
- manifest.responseTypeRoutes &&
346
- Object.keys(manifest.responseTypeRoutes).length > 0
347
- ? manifest.responseTypeRoutes
348
- : undefined,
349
- );
350
- newPerRouterTrieMap.set(id, perRouterTrie);
351
364
  }
352
365
  }
353
366
  }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Router discovery error aggregation.
3
+ *
4
+ * During host-router discovery the lazy mounts registered by a host router are
5
+ * invoked to trigger each sub-app's createRouter() registration. Some mount
6
+ * failures are expected in the temporary discovery server context (a sub-app may
7
+ * reference runtime-only bindings), so each is invoked defensively and its error
8
+ * is collected rather than thrown.
9
+ *
10
+ * Previously these errors were discarded with an empty `catch {}`. When a real
11
+ * failure - typically a sub-app whose router module fails to import - left the
12
+ * registry empty, discovery reported the misleading "No routers found" message
13
+ * with no trace of the underlying cause. The collected errors are now surfaced
14
+ * via the `DiscoveryError` thrown at the end of discovery (issue #499).
15
+ *
16
+ * Which entries to invoke is taken from the consumer's declared intent, not
17
+ * inferred from the function's shape. A host route is registered either with
18
+ * `.map((request) => Response)` (an inline request handler, `kind: "handler"`)
19
+ * or `.lazy(() => import("./sub-app"))` (a lazy mount, `kind: "lazy"`). Only
20
+ * `kind === "lazy"` entries are invoked here; inline handlers are never invoked
21
+ * during discovery (they need a Request and register no routers). Because a lazy
22
+ * entry is known to be a module loader, ANY failure it produces - a synchronous
23
+ * throw or a rejected promise - is a genuine discovery failure and is collected.
24
+ */
25
+
26
+ /** An error caught (and previously swallowed) while resolving host routers. */
27
+ export interface CaughtDiscoveryError {
28
+ /** Human-readable description of where the error was caught. */
29
+ context: string;
30
+ /** The caught value (an Error or otherwise). */
31
+ error: unknown;
32
+ }
33
+
34
+ /**
35
+ * Minimal shape of a host registry entry needed for mount resolution.
36
+ * Mirrors the runtime HostRouterRegistry value without coupling to its type.
37
+ */
38
+ interface HostRegistryRoute {
39
+ handler?: unknown;
40
+ kind?: string;
41
+ }
42
+ interface HostRegistryEntry {
43
+ routes: HostRegistryRoute[];
44
+ fallback?: HostRegistryRoute | null;
45
+ }
46
+
47
+ /** Indent every non-empty line of `text` by `pad`. */
48
+ function indent(text: string, pad: string): string {
49
+ return text
50
+ .split("\n")
51
+ .map((line) => (line.length > 0 ? pad + line : line))
52
+ .join("\n");
53
+ }
54
+
55
+ /**
56
+ * Invoke a single lazy mount to trigger its sub-app import (and createRouter()
57
+ * registration), collecting any failure under `context`. The entry is known to
58
+ * be a loader (`kind === "lazy"`), so both a synchronous throw and a rejected
59
+ * promise are genuine failures - no shape heuristics are needed.
60
+ */
61
+ async function invokeLazyMount(
62
+ loader: () => unknown,
63
+ context: string,
64
+ errors: CaughtDiscoveryError[],
65
+ ): Promise<void> {
66
+ try {
67
+ await loader();
68
+ } catch (error) {
69
+ errors.push({ context, error });
70
+ }
71
+ }
72
+
73
+ /** Whether a registry route is a `.lazy()` mount with an invokable loader. */
74
+ function isLazyMount(
75
+ route: HostRegistryRoute | null | undefined,
76
+ ): route is { handler: () => unknown; kind: "lazy" } {
77
+ return (
78
+ !!route && route.kind === "lazy" && typeof route.handler === "function"
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Invoke every lazy mount in the host registry to trigger sub-app
84
+ * createRouter() registration, collecting (not throwing) any failures.
85
+ *
86
+ * Only `.lazy()` entries are invoked; `.map()` inline request handlers are
87
+ * skipped (they need a Request and register no routers). Failures are returned
88
+ * rather than thrown because some mounts legitimately fail in the temporary
89
+ * discovery server context; the caller decides whether the failures matter,
90
+ * which is only when discovery finds no routers at all.
91
+ */
92
+ export async function resolveHostRouterHandlers(
93
+ hostRegistry: Map<string, HostRegistryEntry>,
94
+ ): Promise<CaughtDiscoveryError[]> {
95
+ const errors: CaughtDiscoveryError[] = [];
96
+
97
+ for (const [hostId, entry] of hostRegistry) {
98
+ for (const route of entry.routes) {
99
+ if (isLazyMount(route)) {
100
+ await invokeLazyMount(
101
+ route.handler,
102
+ `host "${hostId}" route handler`,
103
+ errors,
104
+ );
105
+ }
106
+ }
107
+ if (isLazyMount(entry.fallback)) {
108
+ await invokeLazyMount(
109
+ entry.fallback.handler,
110
+ `host "${hostId}" fallback handler`,
111
+ errors,
112
+ );
113
+ }
114
+ }
115
+
116
+ return errors;
117
+ }
118
+
119
+ /**
120
+ * Build the terminal "No routers found" message, appending any errors caught
121
+ * during host-router discovery so the real cause is visible.
122
+ *
123
+ * The aggregated errors are inlined into the message (in addition to being
124
+ * attached via `cause` on `DiscoveryError`) so they survive every caller: the
125
+ * dev/HMR paths log `err.message`, and the build path re-throws using
126
+ * `err.stack`, which begins with the message. None of those callers traverse
127
+ * `cause`, so the message must carry the detail. Each error includes its stack
128
+ * when available.
129
+ */
130
+ export function formatNoRoutersError(
131
+ entryPath: string | undefined,
132
+ errors: CaughtDiscoveryError[],
133
+ ): string {
134
+ const base = `[rango] No routers found in registry after importing ${entryPath}`;
135
+ if (errors.length === 0) {
136
+ return base;
137
+ }
138
+
139
+ const formatted = errors
140
+ .map(({ context, error }) => {
141
+ const err = error instanceof Error ? error : new Error(String(error));
142
+ const detail = err.stack ?? err.message;
143
+ return ` - while resolving ${context}:\n${indent(detail, " ")}`;
144
+ })
145
+ .join("\n");
146
+
147
+ return (
148
+ `${base}\n\n` +
149
+ `${errors.length} error(s) were caught during host-router discovery and ` +
150
+ `likely explain why no routers were registered:\n${formatted}`
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Reduce the caught errors to an `ErrorOptions.cause`: a single failure becomes
156
+ * the direct cause; multiple failures are wrapped in an `AggregateError` so
157
+ * each underlying error remains reachable. No errors -> no cause.
158
+ */
159
+ function toCause(errors: CaughtDiscoveryError[]): unknown {
160
+ if (errors.length === 0) return undefined;
161
+ if (errors.length === 1) return errors[0].error;
162
+ return new AggregateError(
163
+ errors.map((e) => e.error),
164
+ "Multiple host-router handlers failed during discovery",
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Thrown when router discovery completes without finding any routers.
170
+ *
171
+ * Carries the entry path and the individual failures caught while resolving
172
+ * host-router lazy handlers. The formatted detail is embedded in `message` (for
173
+ * callers that log `err.message`/`err.stack`) and the underlying error(s) are
174
+ * also attached via `cause` (a single failure directly, multiple wrapped in an
175
+ * `AggregateError`) for cause-aware tooling such as the Vite error overlay.
176
+ */
177
+ export class DiscoveryError extends Error {
178
+ /** The entry file that was imported before discovery gave up. */
179
+ readonly entryPath: string | undefined;
180
+ /** Individual failures caught while resolving host-router handlers. */
181
+ readonly caught: CaughtDiscoveryError[];
182
+
183
+ constructor(entryPath: string | undefined, caught: CaughtDiscoveryError[]) {
184
+ super(formatNoRoutersError(entryPath, caught));
185
+ const cause = toCause(caught);
186
+ if (cause !== undefined) {
187
+ this.cause = cause;
188
+ }
189
+ this.name = "DiscoveryError";
190
+ this.entryPath = entryPath;
191
+ this.caught = caught;
192
+ Object.setPrototypeOf(this, DiscoveryError.prototype);
193
+ }
194
+ }