@rangojs/router 0.0.0-experimental.103 → 0.0.0-experimental.105
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.
- package/README.md +4 -4
- package/dist/vite/index.js +181 -34
- package/package.json +3 -2
- package/skills/host-router/SKILL.md +45 -20
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/vite/discovery/discover-routers.ts +20 -22
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/state.ts +17 -0
- package/src/vite/rango.ts +16 -4
- package/src/vite/router-discovery.ts +34 -2
- package/src/vite/utils/forward-user-plugins.ts +164 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
package/src/host/router.ts
CHANGED
|
@@ -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
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
221
|
+
entry: RouteEntry,
|
|
176
222
|
request: Request,
|
|
177
223
|
input: RouterRequestInput<any>,
|
|
178
224
|
): Promise<Response> {
|
|
179
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
//
|
|
211
|
-
return
|
|
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(
|
|
215
|
-
cause: {
|
|
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
|
|
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
|
|
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
|
-
|
|
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).
|
|
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[]>>(
|
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
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
|
|
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,
|
package/src/vite/rango.ts
CHANGED
|
@@ -131,6 +131,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
131
131
|
},
|
|
132
132
|
resolve: {
|
|
133
133
|
alias: rangoAliases,
|
|
134
|
+
// Force a single React/React-DOM copy across all three RSC
|
|
135
|
+
// environments. RSC requires exactly one react/react-dom instance
|
|
136
|
+
// per environment runtime; consumer install topologies (pnpm
|
|
137
|
+
// strict layout, experimental React pins, third-party "use client"
|
|
138
|
+
// packages) can otherwise resolve duplicate copies, causing
|
|
139
|
+
// "Invalid hook call" / lost context. Child environments inherit
|
|
140
|
+
// this root dedupe, and Vite merges it with any consumer dedupe.
|
|
141
|
+
dedupe: ["react", "react-dom"],
|
|
134
142
|
},
|
|
135
143
|
build: {
|
|
136
144
|
rollupOptions: { onwarn },
|
|
@@ -157,10 +165,6 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
157
165
|
build: {
|
|
158
166
|
outDir: "./dist/rsc/ssr",
|
|
159
167
|
},
|
|
160
|
-
resolve: {
|
|
161
|
-
// Ensure single React instance in SSR child environment
|
|
162
|
-
dedupe: ["react", "react-dom"],
|
|
163
|
-
},
|
|
164
168
|
// Pre-bundle SSR entry and React for proper module linking with childEnvironments
|
|
165
169
|
// All deps must be listed to avoid late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
|
|
166
170
|
optimizeDeps: {
|
|
@@ -289,6 +293,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
289
293
|
},
|
|
290
294
|
resolve: {
|
|
291
295
|
alias: rangoAliases,
|
|
296
|
+
// Force a single React/React-DOM copy across all three RSC
|
|
297
|
+
// environments. RSC requires exactly one react/react-dom instance
|
|
298
|
+
// per environment runtime; consumer install topologies (pnpm
|
|
299
|
+
// strict layout, experimental React pins, third-party "use client"
|
|
300
|
+
// packages) can otherwise resolve duplicate copies, causing
|
|
301
|
+
// "Invalid hook call" / lost context. Child environments inherit
|
|
302
|
+
// this root dedupe, and Vite merges it with any consumer dedupe.
|
|
303
|
+
dedupe: ["react", "react-dom"],
|
|
292
304
|
},
|
|
293
305
|
environments: {
|
|
294
306
|
client: {
|