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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -20,7 +20,13 @@ import {
20
20
  expandPrerenderRoutes,
21
21
  renderStaticHandlers,
22
22
  } from "./prerender-collection.js";
23
+ import {
24
+ resolveHostRouterHandlers,
25
+ DiscoveryError,
26
+ type CaughtDiscoveryError,
27
+ } from "./discovery-errors.js";
23
28
  import { createRangoDebugger, timed, NS } from "../debug.js";
29
+ import { computeProductionHash } from "../plugins/client-ref-hashing.js";
24
30
 
25
31
  const debug = createRangoDebugger(NS.discovery);
26
32
 
@@ -56,32 +62,25 @@ export async function discoverRouters(
56
62
  if (!registry || registry.size === 0) {
57
63
  // No RSC routers found directly. Check for host routers with lazy handlers
58
64
  // that need to be resolved to trigger sub-app createRouter() calls.
65
+ //
66
+ // Handler failures are collected rather than swallowed: when the registry
67
+ // is still empty afterwards, these errors (typically a sub-app whose router
68
+ // module failed to import) are the most likely cause and are surfaced in
69
+ // the terminal "No routers found" error below.
70
+ const discoveryErrors: CaughtDiscoveryError[] = [];
59
71
  try {
60
72
  const hostRegistry: Map<string, any> | undefined =
61
73
  serverMod.HostRouterRegistry;
62
74
 
63
75
  if (hostRegistry && hostRegistry.size > 0) {
64
76
  console.log(
65
- `[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`,
77
+ `[rango] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`,
66
78
  );
67
79
 
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
- }
80
+ const handlerErrors = await resolveHostRouterHandlers(hostRegistry);
81
+ discoveryErrors.push(...handlerErrors);
82
+ for (const { context, error } of handlerErrors) {
83
+ debug?.("caught error while resolving %s: %O", context, error);
85
84
  }
86
85
 
87
86
  // Re-read RouterRegistry - sub-app createRouter() calls should have populated it
@@ -96,15 +95,15 @@ export async function discoverRouters(
96
95
  registry = freshRegistry;
97
96
  }
98
97
  }
99
- } catch {
100
- // Host-router discovery is best-effort; skip if unavailable
98
+ } catch (error) {
99
+ // Host-router discovery is best-effort; record the failure so it can be
100
+ // surfaced if no routers are found.
101
+ discoveryErrors.push({ context: "host-router discovery", error });
101
102
  }
102
103
 
103
104
  // If still no routers after host router resolution, fail
104
105
  if (!registry || registry.size === 0) {
105
- throw new Error(
106
- `[rsc-router] No routers found in registry after importing ${state.resolvedEntryPath}`,
107
- );
106
+ throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
108
107
  }
109
108
  }
110
109
 
@@ -145,6 +144,28 @@ export async function discoverRouters(
145
144
  // Collect all manifests for trie building (avoid re-running generateManifest)
146
145
  const allManifests: Array<{ id: string; manifest: any }> = [];
147
146
 
147
+ // Built-in clientChunks context (present only when the built-in strategy is
148
+ // active). Collect the production hashes of "use client" error/notFound
149
+ // fallback modules so the strategy can route them into app-fallback.
150
+ const clientChunkCtx = state.opts?.clientChunkCtx;
151
+ const collectClientFallbackRef = clientChunkCtx
152
+ ? (refKey: string) =>
153
+ clientChunkCtx.fallbackRefs.add(
154
+ computeProductionHash(state.projectRoot, refKey),
155
+ )
156
+ : undefined;
157
+ // Router-level boundary defaults (`createRouter({ defaultErrorBoundary, ... })`)
158
+ // are NOT in EntryData, so generateManifestFull's walk misses them. Collect any
159
+ // "use client" default boundary directly off the router instance. The value is
160
+ // commonly a handler function wrapping the client boundary in server providers,
161
+ // so collectFallbackClientRefs invokes + walks the tree. Routed through buildMod
162
+ // so it runs in the same RSC runner realm the boundary value came from.
163
+ const collectFromBoundaryNode = (node: unknown): void => {
164
+ if (collectClientFallbackRef && buildMod.collectFallbackClientRefs) {
165
+ buildMod.collectFallbackClientRefs(node, collectClientFallbackRef);
166
+ }
167
+ };
168
+
148
169
  const manifestGenStart = debug ? performance.now() : 0;
149
170
  for (const [id, router] of registry) {
150
171
  if (!router.urlpatterns || !generateManifestFull) {
@@ -154,10 +175,23 @@ export async function discoverRouters(
154
175
  const manifest = generateManifestFull(
155
176
  router.urlpatterns,
156
177
  routerMountIndex,
157
- router.__basename ? { urlPrefix: router.__basename } : undefined,
178
+ {
179
+ ...(router.__basename ? { urlPrefix: router.__basename } : {}),
180
+ ...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
181
+ },
158
182
  );
159
183
  routerMountIndex++;
160
184
  allManifests.push({ id, manifest });
185
+
186
+ // Router-level "use client" boundary defaults -> app-fallback (the
187
+ // route-tree errorBoundary()/notFoundBoundary() helpers are already
188
+ // collected inside generateManifestFull via collectClientFallbackRef).
189
+ if (collectClientFallbackRef) {
190
+ collectFromBoundaryNode(router.__defaultErrorBoundary);
191
+ collectFromBoundaryNode(router.__defaultNotFoundBoundary);
192
+ collectFromBoundaryNode(router.__notFound);
193
+ }
194
+
161
195
  const routeCount = Object.keys(manifest.routeManifest).length;
162
196
  const staticRoutes = Object.values(manifest.routeManifest).filter(
163
197
  (p: any) => !p.includes(":") && !p.includes("*"),
@@ -226,7 +260,7 @@ export async function discoverRouters(
226
260
  newPerRouterPrecomputedMap.set(id, routerPrecomputed);
227
261
 
228
262
  console.log(
229
- `[rsc-router] Router "${id}" -> ${routeCount} routes ` +
263
+ `[rango] Router "${id}" -> ${routeCount} routes ` +
230
264
  `(${staticRoutes} static, ${dynamicRoutes} dynamic)`,
231
265
  );
232
266
  }
@@ -242,7 +276,7 @@ export async function discoverRouters(
242
276
  );
243
277
  if (autoIds.length > 1) {
244
278
  console.warn(
245
- `[rsc-router] WARNING: ${autoIds.length} routers use auto-generated IDs (${autoIds.join(", ")}). ` +
279
+ `[rango] WARNING: ${autoIds.length} routers use auto-generated IDs (${autoIds.join(", ")}). ` +
246
280
  `In multi-router setups, each createRouter() must have an explicit \`id\` option ` +
247
281
  `to ensure per-router manifest data is matched correctly at runtime. ` +
248
282
  `Example: createRouter({ id: "site", ... })`,
@@ -294,18 +328,16 @@ export async function discoverRouters(
294
328
  }
295
329
  }
296
330
 
331
+ // buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
332
+ // identical to undefined, so no empty->undefined coercion is needed.
297
333
  newMergedRouteTrie = buildRouteTrie(
298
334
  newMergedRouteManifest,
299
335
  mergedRouteAncestry,
300
336
  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,
337
+ mergedRouteTrailingSlash,
338
+ prerenderRouteNames,
339
+ passthroughRouteNames,
340
+ mergedResponseTypeRoutes,
309
341
  );
310
342
 
311
343
  // Build per-router tries for multi-router isolation.
@@ -332,20 +364,10 @@ export async function discoverRouters(
332
364
  manifest.routeManifest,
333
365
  manifest._routeAncestry,
334
366
  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,
367
+ manifest.routeTrailingSlash,
368
+ perRouterPrerenderNames,
369
+ perRouterPassthroughNames,
370
+ manifest.responseTypeRoutes,
349
371
  );
350
372
  newPerRouterTrieMap.set(id, perRouterTrie);
351
373
  }
@@ -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
+ }
@@ -81,7 +81,7 @@ export async function expandPrerenderRoutes(
81
81
  ? setInterval(() => {
82
82
  const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
83
83
  console.log(
84
- `[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
84
+ `[rango] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
85
85
  );
86
86
  }, 5000)
87
87
  : undefined;
@@ -122,7 +122,7 @@ export async function expandPrerenderRoutes(
122
122
  get env() {
123
123
  if (buildEnv !== undefined) return buildEnv;
124
124
  throw new Error(
125
- "[rsc-router] ctx.env is not available during build-time getParams(). " +
125
+ "[rango] ctx.env is not available during build-time getParams(). " +
126
126
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
127
127
  );
128
128
  },
@@ -170,7 +170,7 @@ export async function expandPrerenderRoutes(
170
170
  // Skip in getParams() skips the entire route
171
171
  if (err.name === "Skip") {
172
172
  console.log(
173
- `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
173
+ `[rango] SKIP route "${routeName}" - ${err.message}`,
174
174
  );
175
175
  notifyOnError(
176
176
  registry,
@@ -184,14 +184,14 @@ export async function expandPrerenderRoutes(
184
184
  }
185
185
  // Regular error: fail the build
186
186
  console.error(
187
- `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
187
+ `[rango] Failed to get params for prerender route "${routeName}": ${err.message}`,
188
188
  );
189
189
  notifyOnError(registry, err, "prerender", routeName);
190
190
  throw err;
191
191
  }
192
192
  } else {
193
193
  console.warn(
194
- `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
194
+ `[rango] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
195
195
  );
196
196
  }
197
197
  }
@@ -202,7 +202,7 @@ export async function expandPrerenderRoutes(
202
202
  clearInterval(progressInterval);
203
203
  const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
204
204
  console.log(
205
- `[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
205
+ `[rango] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
206
206
  );
207
207
  }
208
208
  }
@@ -220,7 +220,7 @@ export async function expandPrerenderRoutes(
220
220
  const concurrencyNote =
221
221
  maxConcurrency > 1 ? ` (concurrency: ${maxConcurrency})` : "";
222
222
  console.log(
223
- `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
223
+ `[rango] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
224
224
  );
225
225
  debug?.(
226
226
  "prerender loop: %d entries, max concurrency %d",
@@ -261,7 +261,7 @@ export async function expandPrerenderRoutes(
261
261
  if (result.passthrough) {
262
262
  const elapsed = (performance.now() - startUrl).toFixed(0);
263
263
  console.log(
264
- `[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
264
+ `[rango] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
265
265
  );
266
266
  doneCount++;
267
267
  break;
@@ -295,7 +295,7 @@ export async function expandPrerenderRoutes(
295
295
  }
296
296
  const elapsed = (performance.now() - startUrl).toFixed(0);
297
297
  console.log(
298
- `[rsc-router] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
298
+ `[rango] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
299
299
  );
300
300
  doneCount++;
301
301
  break;
@@ -303,7 +303,7 @@ export async function expandPrerenderRoutes(
303
303
  if (err.name === "Skip") {
304
304
  const elapsed = (performance.now() - startUrl).toFixed(0);
305
305
  console.log(
306
- `[rsc-router] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
306
+ `[rango] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
307
307
  );
308
308
  skipCount++;
309
309
  notifyOnError(
@@ -319,7 +319,7 @@ export async function expandPrerenderRoutes(
319
319
  // Regular error: log, notify, and fail the build
320
320
  const elapsed = (performance.now() - startUrl).toFixed(0);
321
321
  console.error(
322
- `[rsc-router] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
322
+ `[rango] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
323
323
  );
324
324
  notifyOnError(
325
325
  registry,
@@ -342,7 +342,7 @@ export async function expandPrerenderRoutes(
342
342
  const parts = [`${doneCount} done`];
343
343
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
344
344
  console.log(
345
- `[rsc-router] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
345
+ `[rango] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
346
346
  );
347
347
  debug?.(
348
348
  "expandPrerenderRoutes done: %d done, %d skipped, %sms (overall %sms)",
@@ -387,9 +387,7 @@ export async function renderStaticHandlers(
387
387
  totalStaticCount += exportNames.length;
388
388
  }
389
389
  const startStatic = performance.now();
390
- console.log(
391
- `[rsc-router] Rendering ${totalStaticCount} static handler(s)...`,
392
- );
390
+ console.log(`[rango] Rendering ${totalStaticCount} static handler(s)...`);
393
391
 
394
392
  for (const [moduleId, exportNames] of state.resolvedStaticModules) {
395
393
  let mod: any;
@@ -397,7 +395,7 @@ export async function renderStaticHandlers(
397
395
  mod = await rscEnv!.runner.import(moduleId);
398
396
  } catch (err: any) {
399
397
  console.error(
400
- `[rsc-router] Failed to import static module ${moduleId}: ${err.message}`,
398
+ `[rango] Failed to import static module ${moduleId}: ${err.message}`,
401
399
  );
402
400
  notifyOnError(registry, err, "static");
403
401
  throw err;
@@ -432,9 +430,7 @@ export async function renderStaticHandlers(
432
430
  exportValue,
433
431
  );
434
432
  const elapsed = (performance.now() - startHandler).toFixed(0);
435
- console.log(
436
- `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
437
- );
433
+ console.log(`[rango] OK ${name.padEnd(40)} (${elapsed}ms)`);
438
434
  staticDone++;
439
435
  handled = true;
440
436
  break;
@@ -443,7 +439,7 @@ export async function renderStaticHandlers(
443
439
  if (err.name === "Skip") {
444
440
  const elapsed = (performance.now() - startHandler).toFixed(0);
445
441
  console.log(
446
- `[rsc-router] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
442
+ `[rango] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
447
443
  );
448
444
  staticSkip++;
449
445
  notifyOnError(registry, err, "static", undefined, undefined, true);
@@ -453,16 +449,14 @@ export async function renderStaticHandlers(
453
449
  // Regular error: log, notify, and fail the build
454
450
  const elapsed = (performance.now() - startHandler).toFixed(0);
455
451
  console.error(
456
- `[rsc-router] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
452
+ `[rango] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
457
453
  );
458
454
  notifyOnError(registry, err, "static");
459
455
  throw err;
460
456
  }
461
457
  }
462
458
  if (!handled) {
463
- console.warn(
464
- `[rsc-router] No router could render static handler "${name}"`,
465
- );
459
+ console.warn(`[rango] No router could render static handler "${name}"`);
466
460
  }
467
461
  }
468
462
  }
@@ -474,7 +468,7 @@ export async function renderStaticHandlers(
474
468
  const staticParts = [`${staticDone} done`];
475
469
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
476
470
  console.log(
477
- `[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
471
+ `[rango] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
478
472
  );
479
473
  debug?.(
480
474
  "renderStaticHandlers done: %d done, %d skipped, %sms (overall %sms)",