@rangojs/router 0.0.0-experimental.103 → 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.
- package/README.md +4 -4
- package/dist/vite/index.js +173 -31
- 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/README.md
CHANGED
|
@@ -943,9 +943,9 @@ import { createHostRouter } from "@rangojs/router/host";
|
|
|
943
943
|
|
|
944
944
|
const hostRouter = createHostRouter();
|
|
945
945
|
|
|
946
|
-
hostRouter.host(["*.localhost"]).
|
|
947
|
-
hostRouter.host(["localhost"]).
|
|
948
|
-
hostRouter.fallback().
|
|
946
|
+
hostRouter.host(["*.localhost"]).lazy(() => import("./apps/admin/handler.js"));
|
|
947
|
+
hostRouter.host(["localhost"]).lazy(() => import("./apps/site/handler.js"));
|
|
948
|
+
hostRouter.fallback().lazy(() => import("./apps/site/handler.js"));
|
|
949
949
|
|
|
950
950
|
export default {
|
|
951
951
|
async fetch(request, env, ctx) {
|
|
@@ -954,7 +954,7 @@ export default {
|
|
|
954
954
|
};
|
|
955
955
|
```
|
|
956
956
|
|
|
957
|
-
|
|
957
|
+
Use `.lazy(() => import("./sub-app"))` to mount a lazily-imported sub-app (a module whose `default` export is a handler or nested host router), and `.map((request) => Response)` for an inline request handler. Only `.lazy()` mounts are imported during build-time discovery; `.map(() => import(...))` is a type error. Each sub-app has its own `createRouter()` and `urls()`. Patterns are matched in registration order — register more specific patterns (subdomains) before catch-alls.
|
|
958
958
|
|
|
959
959
|
## Meta Tags
|
|
960
960
|
|
package/dist/vite/index.js
CHANGED
|
@@ -2040,7 +2040,7 @@ import { resolve } from "node:path";
|
|
|
2040
2040
|
// package.json
|
|
2041
2041
|
var package_default = {
|
|
2042
2042
|
name: "@rangojs/router",
|
|
2043
|
-
version: "0.0.0-experimental.
|
|
2043
|
+
version: "0.0.0-experimental.104",
|
|
2044
2044
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2045
2045
|
keywords: [
|
|
2046
2046
|
"react",
|
|
@@ -2205,7 +2205,8 @@ var package_default = {
|
|
|
2205
2205
|
peerDependencies: {
|
|
2206
2206
|
"@cloudflare/vite-plugin": "^1.25.0",
|
|
2207
2207
|
"@vitejs/plugin-rsc": "^0.5.23",
|
|
2208
|
-
react: "
|
|
2208
|
+
react: ">=19.2.6 <20",
|
|
2209
|
+
"react-dom": ">=19.2.6 <20",
|
|
2209
2210
|
vite: "^7.3.0"
|
|
2210
2211
|
},
|
|
2211
2212
|
peerDependenciesMeta: {
|
|
@@ -3777,6 +3778,8 @@ function createDiscoveryState(entryPath, opts) {
|
|
|
3777
3778
|
projectRoot: "",
|
|
3778
3779
|
isBuildMode: false,
|
|
3779
3780
|
userResolveAlias: void 0,
|
|
3781
|
+
userRunnerConfig: void 0,
|
|
3782
|
+
userResolvePlugins: [],
|
|
3780
3783
|
scanFilter: void 0,
|
|
3781
3784
|
cachedRouterFiles: void 0,
|
|
3782
3785
|
opts,
|
|
@@ -4433,6 +4436,80 @@ async function renderStaticHandlers(state, rscEnv, registry) {
|
|
|
4433
4436
|
);
|
|
4434
4437
|
}
|
|
4435
4438
|
|
|
4439
|
+
// src/vite/discovery/discovery-errors.ts
|
|
4440
|
+
function indent(text, pad) {
|
|
4441
|
+
return text.split("\n").map((line) => line.length > 0 ? pad + line : line).join("\n");
|
|
4442
|
+
}
|
|
4443
|
+
async function invokeLazyMount(loader, context, errors) {
|
|
4444
|
+
try {
|
|
4445
|
+
await loader();
|
|
4446
|
+
} catch (error) {
|
|
4447
|
+
errors.push({ context, error });
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
function isLazyMount(route) {
|
|
4451
|
+
return !!route && route.kind === "lazy" && typeof route.handler === "function";
|
|
4452
|
+
}
|
|
4453
|
+
async function resolveHostRouterHandlers(hostRegistry) {
|
|
4454
|
+
const errors = [];
|
|
4455
|
+
for (const [hostId, entry] of hostRegistry) {
|
|
4456
|
+
for (const route of entry.routes) {
|
|
4457
|
+
if (isLazyMount(route)) {
|
|
4458
|
+
await invokeLazyMount(
|
|
4459
|
+
route.handler,
|
|
4460
|
+
`host "${hostId}" route handler`,
|
|
4461
|
+
errors
|
|
4462
|
+
);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
if (isLazyMount(entry.fallback)) {
|
|
4466
|
+
await invokeLazyMount(
|
|
4467
|
+
entry.fallback.handler,
|
|
4468
|
+
`host "${hostId}" fallback handler`,
|
|
4469
|
+
errors
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
return errors;
|
|
4474
|
+
}
|
|
4475
|
+
function formatNoRoutersError(entryPath, errors) {
|
|
4476
|
+
const base = `[rsc-router] No routers found in registry after importing ${entryPath}`;
|
|
4477
|
+
if (errors.length === 0) {
|
|
4478
|
+
return base;
|
|
4479
|
+
}
|
|
4480
|
+
const formatted = errors.map(({ context, error }) => {
|
|
4481
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
4482
|
+
const detail = err.stack ?? err.message;
|
|
4483
|
+
return ` - while resolving ${context}:
|
|
4484
|
+
${indent(detail, " ")}`;
|
|
4485
|
+
}).join("\n");
|
|
4486
|
+
return `${base}
|
|
4487
|
+
|
|
4488
|
+
${errors.length} error(s) were caught during host-router discovery and likely explain why no routers were registered:
|
|
4489
|
+
${formatted}`;
|
|
4490
|
+
}
|
|
4491
|
+
function toCause(errors) {
|
|
4492
|
+
if (errors.length === 0) return void 0;
|
|
4493
|
+
if (errors.length === 1) return errors[0].error;
|
|
4494
|
+
return new AggregateError(
|
|
4495
|
+
errors.map((e) => e.error),
|
|
4496
|
+
"Multiple host-router handlers failed during discovery"
|
|
4497
|
+
);
|
|
4498
|
+
}
|
|
4499
|
+
var DiscoveryError = class _DiscoveryError extends Error {
|
|
4500
|
+
constructor(entryPath, caught) {
|
|
4501
|
+
super(formatNoRoutersError(entryPath, caught));
|
|
4502
|
+
const cause = toCause(caught);
|
|
4503
|
+
if (cause !== void 0) {
|
|
4504
|
+
this.cause = cause;
|
|
4505
|
+
}
|
|
4506
|
+
this.name = "DiscoveryError";
|
|
4507
|
+
this.entryPath = entryPath;
|
|
4508
|
+
this.caught = caught;
|
|
4509
|
+
Object.setPrototypeOf(this, _DiscoveryError.prototype);
|
|
4510
|
+
}
|
|
4511
|
+
};
|
|
4512
|
+
|
|
4436
4513
|
// src/vite/discovery/discover-routers.ts
|
|
4437
4514
|
var debug10 = createRangoDebugger(NS.discovery);
|
|
4438
4515
|
async function discoverRouters(state, rscEnv) {
|
|
@@ -4449,27 +4526,17 @@ async function discoverRouters(state, rscEnv) {
|
|
|
4449
4526
|
);
|
|
4450
4527
|
let registry = serverMod.RouterRegistry;
|
|
4451
4528
|
if (!registry || registry.size === 0) {
|
|
4529
|
+
const discoveryErrors = [];
|
|
4452
4530
|
try {
|
|
4453
4531
|
const hostRegistry = serverMod.HostRouterRegistry;
|
|
4454
4532
|
if (hostRegistry && hostRegistry.size > 0) {
|
|
4455
4533
|
console.log(
|
|
4456
4534
|
`[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
|
|
4457
4535
|
);
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
await route.handler();
|
|
4463
|
-
} catch {
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
}
|
|
4467
|
-
if (entry.fallback && typeof entry.fallback.handler === "function") {
|
|
4468
|
-
try {
|
|
4469
|
-
await entry.fallback.handler();
|
|
4470
|
-
} catch {
|
|
4471
|
-
}
|
|
4472
|
-
}
|
|
4536
|
+
const handlerErrors = await resolveHostRouterHandlers(hostRegistry);
|
|
4537
|
+
discoveryErrors.push(...handlerErrors);
|
|
4538
|
+
for (const { context, error } of handlerErrors) {
|
|
4539
|
+
debug10?.("caught error while resolving %s: %O", context, error);
|
|
4473
4540
|
}
|
|
4474
4541
|
const freshServerMod = await rscEnv.runner.import(
|
|
4475
4542
|
"@rangojs/router/server"
|
|
@@ -4480,12 +4547,11 @@ async function discoverRouters(state, rscEnv) {
|
|
|
4480
4547
|
registry = freshRegistry;
|
|
4481
4548
|
}
|
|
4482
4549
|
}
|
|
4483
|
-
} catch {
|
|
4550
|
+
} catch (error) {
|
|
4551
|
+
discoveryErrors.push({ context: "host-router discovery", error });
|
|
4484
4552
|
}
|
|
4485
4553
|
if (!registry || registry.size === 0) {
|
|
4486
|
-
throw new
|
|
4487
|
-
`[rsc-router] No routers found in registry after importing ${state.resolvedEntryPath}`
|
|
4488
|
-
);
|
|
4554
|
+
throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
|
|
4489
4555
|
}
|
|
4490
4556
|
}
|
|
4491
4557
|
const buildMod = await timed(
|
|
@@ -5162,6 +5228,52 @@ function createDiscoveryGate(s, debug11) {
|
|
|
5162
5228
|
};
|
|
5163
5229
|
}
|
|
5164
5230
|
|
|
5231
|
+
// src/vite/utils/forward-user-plugins.ts
|
|
5232
|
+
function isDenied(name) {
|
|
5233
|
+
return name.startsWith("vite:") || name === "rsc" || name.startsWith("rsc:") || name.startsWith("@rangojs/router") || name.startsWith("@cloudflare/vite-plugin") || name.startsWith("vite-plugin-cloudflare");
|
|
5234
|
+
}
|
|
5235
|
+
function hasResolutionHooks(p) {
|
|
5236
|
+
return Boolean(p.resolveId || p.load);
|
|
5237
|
+
}
|
|
5238
|
+
function stripToResolutionHooks(p) {
|
|
5239
|
+
const stripped = { name: p.name };
|
|
5240
|
+
if (p.enforce) stripped.enforce = p.enforce;
|
|
5241
|
+
if (p.applyToEnvironment)
|
|
5242
|
+
stripped.applyToEnvironment = p.applyToEnvironment;
|
|
5243
|
+
if (p.resolveId) stripped.resolveId = p.resolveId;
|
|
5244
|
+
if (p.load) stripped.load = p.load;
|
|
5245
|
+
return stripped;
|
|
5246
|
+
}
|
|
5247
|
+
function selectForwardableResolvePlugins(plugins) {
|
|
5248
|
+
if (!plugins) return [];
|
|
5249
|
+
const forwarded = [];
|
|
5250
|
+
for (const p of plugins) {
|
|
5251
|
+
const name = p?.name;
|
|
5252
|
+
if (!name || isDenied(name)) continue;
|
|
5253
|
+
if (!hasResolutionHooks(p)) continue;
|
|
5254
|
+
forwarded.push(stripToResolutionHooks(p));
|
|
5255
|
+
}
|
|
5256
|
+
return forwarded;
|
|
5257
|
+
}
|
|
5258
|
+
function pickForwardedRunnerConfig(config) {
|
|
5259
|
+
const r = config.resolve ?? {};
|
|
5260
|
+
const resolve10 = {};
|
|
5261
|
+
if (r.alias !== void 0) resolve10.alias = r.alias;
|
|
5262
|
+
if (r.dedupe !== void 0) resolve10.dedupe = r.dedupe;
|
|
5263
|
+
if (r.conditions !== void 0) resolve10.conditions = r.conditions;
|
|
5264
|
+
if (r.mainFields !== void 0) resolve10.mainFields = r.mainFields;
|
|
5265
|
+
if (r.extensions !== void 0) resolve10.extensions = r.extensions;
|
|
5266
|
+
if (r.preserveSymlinks !== void 0)
|
|
5267
|
+
resolve10.preserveSymlinks = r.preserveSymlinks;
|
|
5268
|
+
const userEsbuild = config.esbuild;
|
|
5269
|
+
const esbuild = userEsbuild && typeof userEsbuild === "object" ? { ...userEsbuild, jsx: "automatic", jsxImportSource: "react" } : { jsx: "automatic", jsxImportSource: "react" };
|
|
5270
|
+
return {
|
|
5271
|
+
resolve: resolve10,
|
|
5272
|
+
define: config.define,
|
|
5273
|
+
esbuild
|
|
5274
|
+
};
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5165
5277
|
// src/vite/router-discovery.ts
|
|
5166
5278
|
var debugDiscovery = createRangoDebugger(NS.discovery);
|
|
5167
5279
|
var debugRoutes = createRangoDebugger(NS.routes);
|
|
@@ -5184,14 +5296,23 @@ function ensureCloudflareProtocolLoaderRegistered() {
|
|
|
5184
5296
|
async function createTempRscServer(state, options = {}) {
|
|
5185
5297
|
ensureCloudflareProtocolLoaderRegistered();
|
|
5186
5298
|
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
5299
|
+
const runnerConfig = state.userRunnerConfig;
|
|
5300
|
+
const resolveConfig = runnerConfig?.resolve ?? {
|
|
5301
|
+
alias: state.userResolveAlias
|
|
5302
|
+
};
|
|
5303
|
+
const esbuildConfig = runnerConfig?.esbuild ?? {
|
|
5304
|
+
jsx: "automatic",
|
|
5305
|
+
jsxImportSource: "react"
|
|
5306
|
+
};
|
|
5187
5307
|
return createViteServer({
|
|
5188
5308
|
root: state.projectRoot,
|
|
5189
5309
|
configFile: false,
|
|
5190
5310
|
server: { middlewareMode: true },
|
|
5191
5311
|
appType: "custom",
|
|
5192
5312
|
logLevel: "silent",
|
|
5193
|
-
resolve:
|
|
5194
|
-
|
|
5313
|
+
resolve: resolveConfig,
|
|
5314
|
+
...runnerConfig?.define ? { define: runnerConfig.define } : {},
|
|
5315
|
+
esbuild: esbuildConfig,
|
|
5195
5316
|
...options.cacheDir && { cacheDir: options.cacheDir },
|
|
5196
5317
|
plugins: [
|
|
5197
5318
|
rsc({
|
|
@@ -5209,7 +5330,11 @@ async function createTempRscServer(state, options = {}) {
|
|
|
5209
5330
|
// Dev prerender must use dev-mode IDs (path-based) to match the workerd
|
|
5210
5331
|
// runtime. forceBuild produces hashed IDs for production bundle consistency.
|
|
5211
5332
|
exposeInternalIds(options.forceBuild ? { forceBuild: true } : void 0),
|
|
5212
|
-
exposeRouterId()
|
|
5333
|
+
exposeRouterId(),
|
|
5334
|
+
// Forwarded user resolution plugins (e.g. vite-tsconfig-paths). Stripped
|
|
5335
|
+
// to resolveId/load and placed last so framework resolution runs first;
|
|
5336
|
+
// Vite re-sorts by `enforce`, so `enforce: "pre"` resolvers still lead.
|
|
5337
|
+
...state.userResolvePlugins
|
|
5213
5338
|
]
|
|
5214
5339
|
});
|
|
5215
5340
|
}
|
|
@@ -5292,6 +5417,10 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
5292
5417
|
viteCommand = config.command;
|
|
5293
5418
|
viteMode = config.mode;
|
|
5294
5419
|
s.userResolveAlias = config.resolve.alias;
|
|
5420
|
+
s.userRunnerConfig = pickForwardedRunnerConfig(config);
|
|
5421
|
+
s.userResolvePlugins = selectForwardableResolvePlugins(
|
|
5422
|
+
config.plugins
|
|
5423
|
+
);
|
|
5295
5424
|
if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
|
|
5296
5425
|
s.resolvedEntryPath = opts.routerPathRef.path;
|
|
5297
5426
|
}
|
|
@@ -5987,7 +6116,8 @@ ${err.stack}` : null
|
|
|
5987
6116
|
].filter(Boolean).join("\n");
|
|
5988
6117
|
throw new Error(
|
|
5989
6118
|
`[rsc-router] Build-time router discovery failed:
|
|
5990
|
-
${details}
|
|
6119
|
+
${details}`,
|
|
6120
|
+
{ cause: err }
|
|
5991
6121
|
);
|
|
5992
6122
|
} finally {
|
|
5993
6123
|
delete globalThis.__rscRouterDiscoveryActive;
|
|
@@ -6213,7 +6343,15 @@ async function rango(options) {
|
|
|
6213
6343
|
esbuildOptions: sharedEsbuildOptions
|
|
6214
6344
|
},
|
|
6215
6345
|
resolve: {
|
|
6216
|
-
alias: rangoAliases
|
|
6346
|
+
alias: rangoAliases,
|
|
6347
|
+
// Force a single React/React-DOM copy across all three RSC
|
|
6348
|
+
// environments. RSC requires exactly one react/react-dom instance
|
|
6349
|
+
// per environment runtime; consumer install topologies (pnpm
|
|
6350
|
+
// strict layout, experimental React pins, third-party "use client"
|
|
6351
|
+
// packages) can otherwise resolve duplicate copies, causing
|
|
6352
|
+
// "Invalid hook call" / lost context. Child environments inherit
|
|
6353
|
+
// this root dedupe, and Vite merges it with any consumer dedupe.
|
|
6354
|
+
dedupe: ["react", "react-dom"]
|
|
6217
6355
|
},
|
|
6218
6356
|
build: {
|
|
6219
6357
|
rollupOptions: { onwarn }
|
|
@@ -6240,10 +6378,6 @@ async function rango(options) {
|
|
|
6240
6378
|
build: {
|
|
6241
6379
|
outDir: "./dist/rsc/ssr"
|
|
6242
6380
|
},
|
|
6243
|
-
resolve: {
|
|
6244
|
-
// Ensure single React instance in SSR child environment
|
|
6245
|
-
dedupe: ["react", "react-dom"]
|
|
6246
|
-
},
|
|
6247
6381
|
// Pre-bundle SSR entry and React for proper module linking with childEnvironments
|
|
6248
6382
|
// All deps must be listed to avoid late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
|
|
6249
6383
|
optimizeDeps: {
|
|
@@ -6339,7 +6473,15 @@ ${list}`);
|
|
|
6339
6473
|
rollupOptions: { onwarn }
|
|
6340
6474
|
},
|
|
6341
6475
|
resolve: {
|
|
6342
|
-
alias: rangoAliases
|
|
6476
|
+
alias: rangoAliases,
|
|
6477
|
+
// Force a single React/React-DOM copy across all three RSC
|
|
6478
|
+
// environments. RSC requires exactly one react/react-dom instance
|
|
6479
|
+
// per environment runtime; consumer install topologies (pnpm
|
|
6480
|
+
// strict layout, experimental React pins, third-party "use client"
|
|
6481
|
+
// packages) can otherwise resolve duplicate copies, causing
|
|
6482
|
+
// "Invalid hook call" / lost context. Child environments inherit
|
|
6483
|
+
// this root dedupe, and Vite merges it with any consumer dedupe.
|
|
6484
|
+
dedupe: ["react", "react-dom"]
|
|
6343
6485
|
},
|
|
6344
6486
|
environments: {
|
|
6345
6487
|
client: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.104",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -165,7 +165,8 @@
|
|
|
165
165
|
"peerDependencies": {
|
|
166
166
|
"@cloudflare/vite-plugin": "^1.25.0",
|
|
167
167
|
"@vitejs/plugin-rsc": "^0.5.23",
|
|
168
|
-
"react": "
|
|
168
|
+
"react": ">=19.2.6 <20",
|
|
169
|
+
"react-dom": ">=19.2.6 <20",
|
|
169
170
|
"vite": "^7.3.0"
|
|
170
171
|
},
|
|
171
172
|
"peerDependenciesMeta": {
|
|
@@ -22,9 +22,9 @@ import { createHostRouter } from "@rangojs/router/host";
|
|
|
22
22
|
|
|
23
23
|
const router = createHostRouter();
|
|
24
24
|
|
|
25
|
-
router.host(["."]).
|
|
26
|
-
router.host(["admin.*"]).
|
|
27
|
-
router.host(["api.*"]).
|
|
25
|
+
router.host(["."]).lazy(() => import("./apps/main"));
|
|
26
|
+
router.host(["admin.*"]).lazy(() => import("./apps/admin"));
|
|
27
|
+
router.host(["api.*"]).lazy(() => import("./apps/api"));
|
|
28
28
|
|
|
29
29
|
export default {
|
|
30
30
|
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
@@ -33,7 +33,31 @@ export default {
|
|
|
33
33
|
};
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
## Inline handlers (`.map`) vs lazy mounts (`.lazy`)
|
|
37
|
+
|
|
38
|
+
A host pattern maps to one of two things, and you pick the method by intent:
|
|
39
|
+
|
|
40
|
+
| Method | Argument | Use for |
|
|
41
|
+
| ------- | ------------------------------ | ------------------------------------------------------------ |
|
|
42
|
+
| `.map` | `(request, input) => Response` | An inline request handler that produces a response directly. |
|
|
43
|
+
| `.lazy` | `() => import("./sub-app")` | A lazily-imported handler or nested host router (a sub-app). |
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Lazy mount: the module's default export is a handler or a HostRouter.
|
|
47
|
+
router.host(["admin.*"]).lazy(() => import("./apps/admin"));
|
|
48
|
+
|
|
49
|
+
// Inline handler: returns a Response itself (sync or async).
|
|
50
|
+
router.host(["health.*"]).map(() => new Response("ok"));
|
|
51
|
+
router
|
|
52
|
+
.host(["echo.*"])
|
|
53
|
+
.map((request) => new Response(new URL(request.url).pathname));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Why two methods instead of one overloaded `.map()`:
|
|
57
|
+
|
|
58
|
+
- **Build-time discovery** invokes only `.lazy()` mounts (to trigger each sub-app's `createRouter()` registration). Inline `.map()` handlers are never invoked during discovery, so they can't crash it or pollute its errors.
|
|
59
|
+
- `.map(() => import("./sub-app"))` is a **type error** — a lazy import resolves to a module, not a `Response`. Use `.lazy()` for imports. (If the types are bypassed, e.g. from JS, a `.map()` handler that resolves to a module throws a clear `HostRouterError` at request time instead of returning the module.)
|
|
60
|
+
- A lazy loader may declare an ignored parameter (`.lazy((_request?) => import("./x"))`); `.lazy()` accepts it because intent is explicit, not inferred from the signature.
|
|
37
61
|
|
|
38
62
|
## Pattern Syntax
|
|
39
63
|
|
|
@@ -65,8 +89,8 @@ const hosts = defineHosts({
|
|
|
65
89
|
app: [".", "www.*"],
|
|
66
90
|
});
|
|
67
91
|
|
|
68
|
-
router.host(hosts.admin).
|
|
69
|
-
router.host(hosts.app).
|
|
92
|
+
router.host(hosts.admin).lazy(() => import("./apps/admin"));
|
|
93
|
+
router.host(hosts.app).lazy(() => import("./apps/main"));
|
|
70
94
|
```
|
|
71
95
|
|
|
72
96
|
Returns a frozen object — keys are autocompleted by TypeScript.
|
|
@@ -88,7 +112,7 @@ router.use(async (request, input, next) => {
|
|
|
88
112
|
router
|
|
89
113
|
.host(["admin.*"])
|
|
90
114
|
.use(requireAuth)
|
|
91
|
-
.
|
|
115
|
+
.lazy(() => import("./apps/admin"));
|
|
92
116
|
```
|
|
93
117
|
|
|
94
118
|
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
@@ -179,40 +203,41 @@ const request = createTestRequest({
|
|
|
179
203
|
});
|
|
180
204
|
|
|
181
205
|
// Test which route would match (without executing)
|
|
182
|
-
router.test("admin.example.com"); // { pattern, handler } | null
|
|
206
|
+
router.test("admin.example.com"); // { pattern, handler, kind } | null
|
|
183
207
|
```
|
|
184
208
|
|
|
185
209
|
## Error Types
|
|
186
210
|
|
|
187
211
|
All errors extend `HostRouterError`:
|
|
188
212
|
|
|
189
|
-
| Error | When
|
|
190
|
-
| ----------------------------- |
|
|
191
|
-
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces
|
|
192
|
-
| `HostOverrideNotAllowedError` | Cookie override from disallowed host
|
|
193
|
-
| `InvalidHostnameError` | Cookie value isn't a valid hostname
|
|
194
|
-
| `HostValidationError` | Custom `validate` function threw
|
|
195
|
-
| `NoRouteMatchError` | No host pattern matched the request
|
|
196
|
-
| `InvalidHandlerError` | Handler is not a function
|
|
213
|
+
| Error | When |
|
|
214
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
215
|
+
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
|
|
216
|
+
| `HostOverrideNotAllowedError` | Cookie override from disallowed host |
|
|
217
|
+
| `InvalidHostnameError` | Cookie value isn't a valid hostname |
|
|
218
|
+
| `HostValidationError` | Custom `validate` function threw |
|
|
219
|
+
| `NoRouteMatchError` | No host pattern matched the request |
|
|
220
|
+
| `InvalidHandlerError` | Handler is not a function, or a lazy mount resolved to a module without a usable `default` export |
|
|
221
|
+
| `HostRouterError` | A `.map()` inline handler resolved to a module namespace (a misused lazy import — use `.lazy()`) |
|
|
197
222
|
|
|
198
223
|
See the fallback section above for a `NoRouteMatchError` catch example.
|
|
199
224
|
|
|
200
225
|
## Nesting Host Routers
|
|
201
226
|
|
|
202
|
-
A lazy
|
|
227
|
+
A lazy mount can resolve to another `HostRouter`:
|
|
203
228
|
|
|
204
229
|
```typescript
|
|
205
230
|
// apps/regional.ts
|
|
206
231
|
import { createHostRouter } from "@rangojs/router/host";
|
|
207
232
|
|
|
208
233
|
const regional = createHostRouter();
|
|
209
|
-
regional.host(["us.*"]).
|
|
210
|
-
regional.host(["eu.*"]).
|
|
234
|
+
regional.host(["us.*"]).lazy(() => import("./regions/us"));
|
|
235
|
+
regional.host(["eu.*"]).lazy(() => import("./regions/eu"));
|
|
211
236
|
|
|
212
237
|
export default regional;
|
|
213
238
|
```
|
|
214
239
|
|
|
215
240
|
```typescript
|
|
216
241
|
// host-router.ts
|
|
217
|
-
router.host(["**.regional.example.com"]).
|
|
242
|
+
router.host(["**.regional.example.com"]).lazy(() => import("./apps/regional"));
|
|
218
243
|
```
|
package/src/host/index.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* const router = createHostRouter();
|
|
13
13
|
*
|
|
14
|
-
* router.host(['.']).
|
|
15
|
-
* router.host(['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) {
|