@rangojs/router 0.0.0-experimental.102 → 0.0.0-experimental.104

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.
@@ -34,6 +34,7 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
34
34
  | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
35
35
  | `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
36
36
  | `/fonts` | Load web fonts with preload hints |
37
+ | `/bundle-analysis` | Audit your app's production bundle for server leaks and oversized chunks |
37
38
  | `/migrate-nextjs` | Migrate a Next.js App Router project to Rango |
38
39
  | `/migrate-react-router` | Migrate a React Router / Remix project to Rango |
39
40
 
package/src/host/index.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * const router = createHostRouter();
13
13
  *
14
- * router.host(['.']).map(() => import('./apps/main'));
15
- * router.host(['admin.*']).map(() => import('./apps/admin'));
14
+ * router.host(['.']).lazy(() => import('./apps/main'));
15
+ * router.host(['admin.*']).lazy(() => import('./apps/admin'));
16
16
  *
17
17
  * export default {
18
18
  * fetch(request) {
@@ -52,6 +52,34 @@ export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> =
52
52
 
53
53
  let hostRouterAutoId = 0;
54
54
 
55
+ /** Whether a value is thenable (a Promise or Promise-like). */
56
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
57
+ return (
58
+ value !== null &&
59
+ (typeof value === "object" || typeof value === "function") &&
60
+ typeof (value as { then?: unknown }).then === "function"
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Whether a resolved value looks like a module namespace from a lazy import -
66
+ * an object with a `default` export that is a function (a Handler) or a host
67
+ * router (an object with `match`). Used to detect a `.map(() => import(...))`
68
+ * misuse: an inline handler should return a Response, not a module.
69
+ */
70
+ function looksLikeLazyModule(value: unknown): boolean {
71
+ if (value === null || typeof value !== "object" || !("default" in value)) {
72
+ return false;
73
+ }
74
+ const defaultExport = (value as { default: unknown }).default;
75
+ return (
76
+ typeof defaultExport === "function" ||
77
+ (typeof defaultExport === "object" &&
78
+ defaultExport !== null &&
79
+ "match" in defaultExport)
80
+ );
81
+ }
82
+
55
83
  /**
56
84
  * Create a host router
57
85
  */
@@ -77,32 +105,44 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
77
105
  ): HostRouteBuilder {
78
106
  const middleware: Middleware[] = [];
79
107
 
108
+ function register(
109
+ handler: Handler | LazyHandler,
110
+ kind: RouteEntry["kind"],
111
+ ): HostRouter {
112
+ const entry: RouteEntry = {
113
+ patterns,
114
+ middleware,
115
+ handler,
116
+ kind,
117
+ isFallback,
118
+ };
119
+
120
+ if (isFallback) {
121
+ fallbackRoute = entry;
122
+ } else {
123
+ routes.push(entry);
124
+ }
125
+
126
+ log(
127
+ `Registered ${isFallback ? "fallback" : "route"} (${kind}):`,
128
+ patterns.join(", "),
129
+ );
130
+
131
+ return router;
132
+ }
133
+
80
134
  return {
81
135
  use(...mw: Middleware[]): HostRouteBuilder {
82
136
  middleware.push(...mw);
83
137
  return this;
84
138
  },
85
139
 
86
- map(handler: Handler | LazyHandler): HostRouter {
87
- const entry: RouteEntry = {
88
- patterns,
89
- middleware,
90
- handler,
91
- isFallback,
92
- };
93
-
94
- if (isFallback) {
95
- fallbackRoute = entry;
96
- } else {
97
- routes.push(entry);
98
- }
99
-
100
- log(
101
- `Registered ${isFallback ? "fallback" : "route"}:`,
102
- patterns.join(", "),
103
- );
140
+ map(handler: Handler): HostRouter {
141
+ return register(handler, "handler");
142
+ },
104
143
 
105
- return router;
144
+ lazy(handler: LazyHandler): HostRouter {
145
+ return register(handler, "lazy");
106
146
  },
107
147
  };
108
148
  }
@@ -169,50 +209,82 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
169
209
  }
170
210
 
171
211
  /**
172
- * Execute handler (lazy or direct)
212
+ * Execute a route entry, branching on its declared kind:
213
+ * - "lazy": await the loader, then delegate to the default export
214
+ * (a nested HostRouter via `.match`, or a request Handler directly).
215
+ * - "handler": call the inline handler with the request. A `.map()` handler
216
+ * that resolves to a module namespace (`{ default }`) is almost certainly
217
+ * a misused lazy import, so it is rejected with a clear message rather
218
+ * than silently returning a module object as the response.
173
219
  */
174
220
  async function executeHandler(
175
- handler: Handler | LazyHandler,
221
+ entry: RouteEntry,
176
222
  request: Request,
177
223
  input: RouterRequestInput<any>,
178
224
  ): Promise<Response> {
179
- // Check if it's a lazy handler (function that returns promise)
180
- if (typeof handler === "function") {
181
- const result = handler(request, input);
182
-
183
- // If it returns a promise with default export
184
- if (result && typeof result === "object" && "then" in result) {
185
- const module = await result;
186
- if (
187
- typeof module === "object" &&
188
- module !== null &&
189
- "default" in module
190
- ) {
191
- const defaultExport = (module as { default: Handler | HostRouter })
192
- .default;
193
-
194
- // If default export is a router with match method
195
- if (
196
- typeof defaultExport === "object" &&
197
- defaultExport !== null &&
198
- "match" in defaultExport
199
- ) {
200
- return (defaultExport as HostRouter).match(request, input);
201
- }
225
+ const { handler, kind } = entry;
202
226
 
203
- // Otherwise treat as handler
204
- return (defaultExport as Handler)(request, input);
205
- }
206
- // If promise resolves to Response
207
- return result as Promise<Response>;
227
+ if (typeof handler !== "function") {
228
+ throw new InvalidHandlerError(handler, {
229
+ cause: { handlerType: typeof handler },
230
+ });
231
+ }
232
+
233
+ if (kind === "lazy") {
234
+ return executeLazyMount(handler as LazyHandler, request, input);
235
+ }
236
+
237
+ const result = (handler as Handler)(request, input);
238
+
239
+ // Inline handlers may be async; await to obtain the Response and to run the
240
+ // misuse guard below.
241
+ if (isThenable(result)) {
242
+ const awaited = await result;
243
+ if (looksLikeLazyModule(awaited)) {
244
+ throw new HostRouterError(
245
+ ".map() is for inline request handlers; use .lazy(() => import(...)) for lazy host mounts.",
246
+ );
247
+ }
248
+ return awaited as Response;
249
+ }
250
+
251
+ return result;
252
+ }
253
+
254
+ /**
255
+ * Resolve a `.lazy()` mount: invoke the zero-arg loader, then dispatch to the
256
+ * module's default export.
257
+ */
258
+ async function executeLazyMount(
259
+ loader: LazyHandler,
260
+ request: Request,
261
+ input: RouterRequestInput<any>,
262
+ ): Promise<Response> {
263
+ const module = await loader();
264
+
265
+ if (typeof module === "object" && module !== null && "default" in module) {
266
+ const defaultExport = (module as { default: Handler | HostRouter })
267
+ .default;
268
+
269
+ // Default export is a nested host router
270
+ if (
271
+ typeof defaultExport === "object" &&
272
+ defaultExport !== null &&
273
+ "match" in defaultExport
274
+ ) {
275
+ return (defaultExport as HostRouter).match(request, input);
208
276
  }
209
277
 
210
- // Direct handler
211
- return result as Response | Promise<Response>;
278
+ // Otherwise treat the default export as a request handler
279
+ return (defaultExport as Handler)(request, input);
212
280
  }
213
281
 
214
- throw new InvalidHandlerError(handler, {
215
- cause: { handlerType: typeof handler },
282
+ throw new InvalidHandlerError(loader, {
283
+ cause: {
284
+ reason:
285
+ "lazy mount did not resolve to a module with a default export; " +
286
+ "use .lazy(() => import('./sub-app')) where the module default-exports a handler or host router",
287
+ },
216
288
  });
217
289
  }
218
290
 
@@ -252,6 +324,7 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
252
324
  return {
253
325
  pattern,
254
326
  handler: route.handler,
327
+ kind: route.kind,
255
328
  };
256
329
  }
257
330
  }
@@ -288,8 +361,7 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
288
361
  allMiddleware,
289
362
  request,
290
363
  fallbackInput,
291
- () =>
292
- executeHandler(fallbackRoute!.handler, request, fallbackInput),
364
+ () => executeHandler(fallbackRoute!, request, fallbackInput),
293
365
  );
294
366
  }
295
367
 
@@ -330,14 +402,14 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
330
402
 
331
403
  // Execute middleware chain and handler
332
404
  return executeMiddleware(allMiddleware, request, input, () =>
333
- executeHandler(matchedRoute.handler, request, input),
405
+ executeHandler(matchedRoute, request, input),
334
406
  );
335
407
  },
336
408
  };
337
409
 
338
410
  // Register in the global HostRouterRegistry for build-time discovery.
339
411
  // The routes array and fallbackRoute ref are live - they reflect routes
340
- // added via .host().map() after this point.
412
+ // added via .host().map()/.lazy() after this point.
341
413
  const registryId = `host-router-${hostRouterAutoId++}`;
342
414
  HostRouterRegistry.set(registryId, {
343
415
  get routes() {
package/src/host/types.ts CHANGED
@@ -35,12 +35,24 @@ export type Middleware = (
35
35
  */
36
36
  export type HostPattern = string | string[];
37
37
 
38
+ /**
39
+ * Whether a route entry is an inline request handler or a lazy module mount.
40
+ *
41
+ * Stored on the entry so discovery and runtime act on the consumer's declared
42
+ * intent instead of inferring it from the function's shape (arity/return value),
43
+ * which is ambiguous: a lazy loader may declare an ignored param, and an inline
44
+ * handler may be async. `.map()` registers `"handler"`, `.lazy()` registers
45
+ * `"lazy"`.
46
+ */
47
+ export type RouteEntryKind = "handler" | "lazy";
48
+
38
49
  /**
39
50
  * Result from testing a hostname against patterns
40
51
  */
41
52
  export interface HostMatchResult {
42
53
  pattern: string;
43
54
  handler: Handler | LazyHandler;
55
+ kind: RouteEntryKind;
44
56
  }
45
57
 
46
58
  /**
@@ -53,9 +65,24 @@ export interface HostRouteBuilder {
53
65
  use(...middleware: Middleware[]): HostRouteBuilder;
54
66
 
55
67
  /**
56
- * Map to a handler or lazy import
68
+ * Map to an inline request handler `(request, input) => Response`.
69
+ *
70
+ * For a lazily-imported sub-app or handler module, use {@link lazy} instead -
71
+ * `.map(() => import(...))` is rejected (the return type is not a `Response`)
72
+ * and would not be discovered at build time.
73
+ */
74
+ map(handler: Handler): HostRouter;
75
+
76
+ /**
77
+ * Mount a lazily-imported handler or host router:
78
+ * `.lazy(() => import("./sub-app"))`.
79
+ *
80
+ * The loader takes no arguments and resolves to a module whose `default`
81
+ * export is a request `Handler` or a nested `HostRouter`. Only `.lazy()`
82
+ * entries are invoked during build-time discovery to trigger the sub-app's
83
+ * `createRouter()` registration.
57
84
  */
58
- map(handler: Handler | LazyHandler): HostRouter;
85
+ lazy(handler: LazyHandler): HostRouter;
59
86
  }
60
87
 
61
88
  /**
@@ -134,6 +161,8 @@ export interface RouteEntry {
134
161
  patterns: string[];
135
162
  middleware: Middleware[];
136
163
  handler: Handler | LazyHandler;
164
+ /** Whether `handler` is an inline request handler or a lazy module mount. */
165
+ kind: RouteEntryKind;
137
166
  isFallback?: boolean;
138
167
  }
139
168
 
package/src/host/utils.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * app: ['*', 'www.*']
16
16
  * });
17
17
  *
18
- * router.host(hosts.admin).map(...); // Type-safe!
18
+ * router.host(hosts.admin).lazy(() => import("./apps/admin")); // Type-safe!
19
19
  * ```
20
20
  */
21
21
  export function defineHosts<T extends Record<string, string | string[]>>(
package/src/index.ts CHANGED
@@ -305,9 +305,14 @@ export {
305
305
  // Path-based response type lookup from RegisteredRoutes
306
306
  export type { PathResponse } from "./href-client.js";
307
307
 
308
- // Telemetry sink
309
- export { createConsoleSink } from "./router/telemetry.js";
310
- export { createOTelSink } from "./router/telemetry-otel.js";
308
+ // Telemetry types only — the createConsoleSink/createOTelSink values are
309
+ // server-only and live in index.rsc.ts (the `react-server` condition of the
310
+ // bare `@rangojs/router` import). Re-exporting them as values from this
311
+ // (default/client) entry would pull telemetry.ts and telemetry-otel.ts into
312
+ // the client module graph; both tree-shake to zero bytes but still appear in
313
+ // bundle analysis output and slow build-time module resolution. Consumers
314
+ // who need the values in non-RSC contexts can import from
315
+ // `@rangojs/router/server`.
311
316
  export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
312
317
  export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
313
318
 
@@ -20,6 +20,11 @@ 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";
24
29
 
25
30
  const debug = createRangoDebugger(NS.discovery);
@@ -56,6 +61,12 @@ export async function discoverRouters(
56
61
  if (!registry || registry.size === 0) {
57
62
  // No RSC routers found directly. Check for host routers with lazy handlers
58
63
  // that need to be resolved to trigger sub-app createRouter() calls.
64
+ //
65
+ // Handler failures are collected rather than swallowed: when the registry
66
+ // is still empty afterwards, these errors (typically a sub-app whose router
67
+ // module failed to import) are the most likely cause and are surfaced in
68
+ // the terminal "No routers found" error below.
69
+ const discoveryErrors: CaughtDiscoveryError[] = [];
59
70
  try {
60
71
  const hostRegistry: Map<string, any> | undefined =
61
72
  serverMod.HostRouterRegistry;
@@ -65,23 +76,10 @@ export async function discoverRouters(
65
76
  `[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`,
66
77
  );
67
78
 
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
- }
79
+ const handlerErrors = await resolveHostRouterHandlers(hostRegistry);
80
+ discoveryErrors.push(...handlerErrors);
81
+ for (const { context, error } of handlerErrors) {
82
+ debug?.("caught error while resolving %s: %O", context, error);
85
83
  }
86
84
 
87
85
  // Re-read RouterRegistry - sub-app createRouter() calls should have populated it
@@ -96,15 +94,15 @@ export async function discoverRouters(
96
94
  registry = freshRegistry;
97
95
  }
98
96
  }
99
- } catch {
100
- // Host-router discovery is best-effort; skip if unavailable
97
+ } catch (error) {
98
+ // Host-router discovery is best-effort; record the failure so it can be
99
+ // surfaced if no routers are found.
100
+ discoveryErrors.push({ context: "host-router discovery", error });
101
101
  }
102
102
 
103
103
  // If still no routers after host router resolution, fail
104
104
  if (!registry || registry.size === 0) {
105
- throw new Error(
106
- `[rsc-router] No routers found in registry after importing ${state.resolvedEntryPath}`,
107
- );
105
+ throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
108
106
  }
109
107
  }
110
108
 
@@ -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 = `[rsc-router] 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
+ }
@@ -45,6 +45,21 @@ export interface DiscoveryState {
45
45
  projectRoot: string;
46
46
  isBuildMode: boolean;
47
47
  userResolveAlias: any;
48
+ /**
49
+ * Data-only slice of the user's resolved config (resolve.*, define, esbuild)
50
+ * mirrored into the discovery temp server so it resolves and transforms
51
+ * modules the same way the real environment does. See
52
+ * `utils/forward-user-plugins.ts`.
53
+ */
54
+ userRunnerConfig:
55
+ | import("../utils/forward-user-plugins.js").ForwardedRunnerConfig
56
+ | undefined;
57
+ /**
58
+ * User resolution plugins (resolveId/load), stripped to their resolution
59
+ * surface, forwarded into the discovery temp server. Lets third-party
60
+ * resolvers such as vite-tsconfig-paths participate in discovery.
61
+ */
62
+ userResolvePlugins: import("vite").Plugin[];
48
63
  scanFilter: ScanFilter | undefined;
49
64
  cachedRouterFiles: string[] | undefined;
50
65
  opts: PluginOptions | undefined;
@@ -95,6 +110,8 @@ export function createDiscoveryState(
95
110
  projectRoot: "",
96
111
  isBuildMode: false,
97
112
  userResolveAlias: undefined,
113
+ userRunnerConfig: undefined,
114
+ userResolvePlugins: [],
98
115
  scanFilter: undefined,
99
116
  cachedRouterFiles: undefined,
100
117
  opts,