@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.8ff1c5d
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/dist/vite/index.js +8 -55
- package/package.json +1 -10
- package/src/vite/index.ts +7 -60
- package/src/host/cookie-handler.ts +0 -159
- package/src/host/errors.ts +0 -97
- package/src/host/index.ts +0 -56
- package/src/host/pattern-matcher.ts +0 -214
- package/src/host/router.ts +0 -330
- package/src/host/testing.ts +0 -79
- package/src/host/types.ts +0 -138
- package/src/host/utils.ts +0 -25
package/dist/vite/index.js
CHANGED
|
@@ -719,7 +719,7 @@ import { resolve } from "node:path";
|
|
|
719
719
|
// package.json
|
|
720
720
|
var package_default = {
|
|
721
721
|
name: "@rangojs/router",
|
|
722
|
-
version: "0.0.0-experimental.
|
|
722
|
+
version: "0.0.0-experimental.8ff1c5d",
|
|
723
723
|
type: "module",
|
|
724
724
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
725
725
|
author: "Ivo Todorov",
|
|
@@ -821,15 +821,6 @@ var package_default = {
|
|
|
821
821
|
"./build": {
|
|
822
822
|
types: "./src/build/index.ts",
|
|
823
823
|
import: "./src/build/index.ts"
|
|
824
|
-
},
|
|
825
|
-
"./host": {
|
|
826
|
-
"react-server": "./src/host/index.ts",
|
|
827
|
-
types: "./src/host/index.ts",
|
|
828
|
-
default: "./src/host/index.ts"
|
|
829
|
-
},
|
|
830
|
-
"./host/testing": {
|
|
831
|
-
types: "./src/host/testing.ts",
|
|
832
|
-
default: "./src/host/testing.ts"
|
|
833
824
|
}
|
|
834
825
|
},
|
|
835
826
|
files: [
|
|
@@ -1066,45 +1057,13 @@ function createRouterDiscoveryPlugin(entryPath) {
|
|
|
1066
1057
|
async function discoverRouters(rscEnv) {
|
|
1067
1058
|
await rscEnv.runner.import(entryPath);
|
|
1068
1059
|
const serverMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
1069
|
-
|
|
1060
|
+
const registry = serverMod.RouterRegistry;
|
|
1070
1061
|
if (!registry || registry.size === 0) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
`[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
|
|
1077
|
-
);
|
|
1078
|
-
for (const [, entry] of hostRegistry) {
|
|
1079
|
-
for (const route of entry.routes) {
|
|
1080
|
-
if (typeof route.handler === "function") {
|
|
1081
|
-
try {
|
|
1082
|
-
await route.handler();
|
|
1083
|
-
} catch {
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
if (entry.fallback && typeof entry.fallback.handler === "function") {
|
|
1088
|
-
try {
|
|
1089
|
-
await entry.fallback.handler();
|
|
1090
|
-
} catch {
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
const freshServerMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
1095
|
-
const freshRegistry = freshServerMod.RouterRegistry;
|
|
1096
|
-
if (freshRegistry && freshRegistry.size > 0) {
|
|
1097
|
-
Object.assign(serverMod, freshServerMod);
|
|
1098
|
-
registry = freshRegistry;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
} catch {
|
|
1102
|
-
}
|
|
1103
|
-
if (!registry || registry.size === 0) {
|
|
1104
|
-
throw new Error(
|
|
1105
|
-
`[rsc-router] No routers found in registry after importing ${entryPath}`
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1062
|
+
console.warn(
|
|
1063
|
+
"[rsc-router] No routers found in registry after importing",
|
|
1064
|
+
entryPath
|
|
1065
|
+
);
|
|
1066
|
+
return serverMod;
|
|
1108
1067
|
}
|
|
1109
1068
|
const buildMod = await rscEnv.runner.import("@rangojs/router/build");
|
|
1110
1069
|
const generateManifest = buildMod.generateManifest;
|
|
@@ -1203,11 +1162,7 @@ function createRouterDiscoveryPlugin(entryPath) {
|
|
|
1203
1162
|
}
|
|
1204
1163
|
await discoverRouters(rscEnv);
|
|
1205
1164
|
} catch (err) {
|
|
1206
|
-
|
|
1207
|
-
if (tempServer) {
|
|
1208
|
-
await tempServer.close();
|
|
1209
|
-
}
|
|
1210
|
-
throw new Error(
|
|
1165
|
+
console.warn(
|
|
1211
1166
|
`[rsc-router] Build-time router discovery failed: ${err.message}`
|
|
1212
1167
|
);
|
|
1213
1168
|
} finally {
|
|
@@ -1539,7 +1494,6 @@ async function rscRouter(options) {
|
|
|
1539
1494
|
}
|
|
1540
1495
|
return plugins;
|
|
1541
1496
|
}
|
|
1542
|
-
var rango = rscRouter;
|
|
1543
1497
|
function createCjsToEsmPlugin() {
|
|
1544
1498
|
return {
|
|
1545
1499
|
name: "@rangojs/router:cjs-to-esm",
|
|
@@ -1598,6 +1552,5 @@ export {
|
|
|
1598
1552
|
exposeHandleId,
|
|
1599
1553
|
exposeLoaderId,
|
|
1600
1554
|
exposeLocationStateId,
|
|
1601
|
-
rango,
|
|
1602
1555
|
rscRouter
|
|
1603
1556
|
};
|
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.8ff1c5d",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
6
6
|
"author": "Ivo Todorov",
|
|
@@ -102,15 +102,6 @@
|
|
|
102
102
|
"./build": {
|
|
103
103
|
"types": "./src/build/index.ts",
|
|
104
104
|
"import": "./src/build/index.ts"
|
|
105
|
-
},
|
|
106
|
-
"./host": {
|
|
107
|
-
"react-server": "./src/host/index.ts",
|
|
108
|
-
"types": "./src/host/index.ts",
|
|
109
|
-
"default": "./src/host/index.ts"
|
|
110
|
-
},
|
|
111
|
-
"./host/testing": {
|
|
112
|
-
"types": "./src/host/testing.ts",
|
|
113
|
-
"default": "./src/host/testing.ts"
|
|
114
105
|
}
|
|
115
106
|
},
|
|
116
107
|
"files": [
|
package/src/vite/index.ts
CHANGED
|
@@ -356,59 +356,14 @@ function createRouterDiscoveryPlugin(entryPath: string): Plugin {
|
|
|
356
356
|
|
|
357
357
|
// Import the router package to access the registry
|
|
358
358
|
const serverMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
359
|
-
|
|
359
|
+
const registry: Map<string, any> = serverMod.RouterRegistry;
|
|
360
360
|
|
|
361
361
|
if (!registry || registry.size === 0) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (hostRegistry && hostRegistry.size > 0) {
|
|
369
|
-
console.log(
|
|
370
|
-
`[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
for (const [, entry] of hostRegistry) {
|
|
374
|
-
for (const route of entry.routes) {
|
|
375
|
-
if (typeof route.handler === 'function') {
|
|
376
|
-
try {
|
|
377
|
-
await route.handler();
|
|
378
|
-
} catch {
|
|
379
|
-
// Lazy handler may fail in temp server context, that's OK
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (entry.fallback && typeof entry.fallback.handler === 'function') {
|
|
384
|
-
try {
|
|
385
|
-
await entry.fallback.handler();
|
|
386
|
-
} catch {
|
|
387
|
-
// Fallback handler may fail in temp server context
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Re-read RouterRegistry - sub-app createRouter() calls should have populated it
|
|
393
|
-
const freshServerMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
394
|
-
const freshRegistry: Map<string, any> = freshServerMod.RouterRegistry;
|
|
395
|
-
|
|
396
|
-
if (freshRegistry && freshRegistry.size > 0) {
|
|
397
|
-
// Update references so the manifest generation below uses the fresh data
|
|
398
|
-
Object.assign(serverMod, freshServerMod);
|
|
399
|
-
registry = freshRegistry;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
} catch {
|
|
403
|
-
// @rangojs/router/host not available or import failed, skip
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// If still no routers after host router resolution, fail
|
|
407
|
-
if (!registry || registry.size === 0) {
|
|
408
|
-
throw new Error(
|
|
409
|
-
`[rsc-router] No routers found in registry after importing ${entryPath}`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
362
|
+
console.warn(
|
|
363
|
+
"[rsc-router] No routers found in registry after importing",
|
|
364
|
+
entryPath
|
|
365
|
+
);
|
|
366
|
+
return serverMod;
|
|
412
367
|
}
|
|
413
368
|
|
|
414
369
|
// Import build utilities for manifest generation
|
|
@@ -545,12 +500,7 @@ function createRouterDiscoveryPlugin(entryPath: string): Plugin {
|
|
|
545
500
|
|
|
546
501
|
await discoverRouters(rscEnv);
|
|
547
502
|
} catch (err: any) {
|
|
548
|
-
|
|
549
|
-
delete (globalThis as any).__rscRouterDiscoveryActive;
|
|
550
|
-
if (tempServer) {
|
|
551
|
-
await tempServer.close();
|
|
552
|
-
}
|
|
553
|
-
throw new Error(
|
|
503
|
+
console.warn(
|
|
554
504
|
`[rsc-router] Build-time router discovery failed: ${err.message}`
|
|
555
505
|
);
|
|
556
506
|
} finally {
|
|
@@ -1037,9 +987,6 @@ export async function rscRouter(
|
|
|
1037
987
|
return plugins;
|
|
1038
988
|
}
|
|
1039
989
|
|
|
1040
|
-
/** Alias for backwards compatibility */
|
|
1041
|
-
export const rango = rscRouter;
|
|
1042
|
-
|
|
1043
990
|
/**
|
|
1044
991
|
* Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
|
|
1045
992
|
* The react-server-dom vendor files are shipped as CJS which doesn't work in browsers.
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cookie Override Handler
|
|
3
|
-
*
|
|
4
|
-
* Manages cookie-based host override for development environments.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { HostOverrideConfig } from './types.js';
|
|
8
|
-
import { matchPattern, parseRequest } from './pattern-matcher.js';
|
|
9
|
-
import {
|
|
10
|
-
HostOverrideNotAllowedError,
|
|
11
|
-
InvalidHostnameError,
|
|
12
|
-
HostValidationError,
|
|
13
|
-
} from './errors.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse cookies from request
|
|
17
|
-
*/
|
|
18
|
-
export function parseCookies(request: Request): Record<string, string> {
|
|
19
|
-
const cookieHeader = request.headers.get('cookie');
|
|
20
|
-
if (!cookieHeader) {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const cookies: Record<string, string> = {};
|
|
25
|
-
const pairs = cookieHeader.split(';');
|
|
26
|
-
|
|
27
|
-
for (const pair of pairs) {
|
|
28
|
-
const [key, value] = pair.trim().split('=');
|
|
29
|
-
if (key && value) {
|
|
30
|
-
cookies[key] = decodeURIComponent(value);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return cookies;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get cookie value from request
|
|
39
|
-
*/
|
|
40
|
-
export function getCookie(request: Request, name: string): string | undefined {
|
|
41
|
-
const cookies = parseCookies(request);
|
|
42
|
-
return cookies[name];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create Set-Cookie header to delete a cookie
|
|
47
|
-
*/
|
|
48
|
-
export function createDeleteCookieHeader(name: string): string {
|
|
49
|
-
return `${name}=; Max-Age=0; Path=/; Secure; HttpOnly`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create error response with cookie deletion
|
|
54
|
-
*/
|
|
55
|
-
export function createCookieErrorResponse(
|
|
56
|
-
cookieName: string,
|
|
57
|
-
message: string
|
|
58
|
-
): Response {
|
|
59
|
-
return new Response(
|
|
60
|
-
JSON.stringify({
|
|
61
|
-
error: message,
|
|
62
|
-
message: `The ${cookieName} cookie has been cleared`,
|
|
63
|
-
}),
|
|
64
|
-
{
|
|
65
|
-
status: 400,
|
|
66
|
-
headers: {
|
|
67
|
-
'Content-Type': 'application/json',
|
|
68
|
-
'Set-Cookie': createDeleteCookieHeader(cookieName),
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Check if current host is allowed to use override
|
|
76
|
-
*/
|
|
77
|
-
export function isHostAllowed(
|
|
78
|
-
request: Request,
|
|
79
|
-
allowedHosts: string[]
|
|
80
|
-
): boolean {
|
|
81
|
-
const { hostname, pathname, parts } = parseRequest(request);
|
|
82
|
-
|
|
83
|
-
for (const pattern of allowedHosts) {
|
|
84
|
-
if (matchPattern(pattern, hostname, pathname, parts)) {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Handle cookie override logic
|
|
94
|
-
*
|
|
95
|
-
* Returns overridden hostname if valid, original hostname if no override.
|
|
96
|
-
* Throws errors for invalid overrides.
|
|
97
|
-
*/
|
|
98
|
-
export function handleCookieOverride(
|
|
99
|
-
request: Request,
|
|
100
|
-
config: HostOverrideConfig | undefined,
|
|
101
|
-
context: any
|
|
102
|
-
): string {
|
|
103
|
-
if (!config) {
|
|
104
|
-
const { hostname } = parseRequest(request);
|
|
105
|
-
return hostname;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const { cookieName, allowedHosts, validate } = config;
|
|
109
|
-
const cookieValue = getCookie(request, cookieName);
|
|
110
|
-
const { hostname: originalHostname } = parseRequest(request);
|
|
111
|
-
|
|
112
|
-
// No cookie - return original hostname
|
|
113
|
-
if (!cookieValue) {
|
|
114
|
-
return originalHostname;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check if current host is allowed
|
|
118
|
-
const allowed = isHostAllowed(request, allowedHosts);
|
|
119
|
-
|
|
120
|
-
// If not allowed, throw error
|
|
121
|
-
if (!allowed) {
|
|
122
|
-
throw new HostOverrideNotAllowedError(originalHostname, cookieName, {
|
|
123
|
-
cause: { cookieValue, currentHost: originalHostname },
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// If allowed and has custom validation, run it
|
|
128
|
-
if (validate) {
|
|
129
|
-
try {
|
|
130
|
-
const validatedHostname = validate(request, cookieValue, context);
|
|
131
|
-
return validatedHostname;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
// Wrap in HostValidationError
|
|
134
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
-
throw new HostValidationError(message, error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Default validation - verify it's a valid hostname using URL constructor
|
|
140
|
-
try {
|
|
141
|
-
// Try to construct a URL with the hostname to validate it
|
|
142
|
-
const testUrl = new URL(`https://${cookieValue}`);
|
|
143
|
-
|
|
144
|
-
// Ensure the hostname matches what we provided (URL constructor normalizes it)
|
|
145
|
-
if (testUrl.hostname !== cookieValue) {
|
|
146
|
-
throw new InvalidHostnameError(cookieValue, {
|
|
147
|
-
cause: { original: cookieValue, normalized: testUrl.hostname },
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
// If URL constructor failed, throw InvalidHostnameError with cause
|
|
152
|
-
if (error instanceof InvalidHostnameError) {
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
throw new InvalidHostnameError(cookieValue, { cause: error });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return cookieValue;
|
|
159
|
-
}
|
package/src/host/errors.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Error Classes for Host Router
|
|
3
|
-
*
|
|
4
|
-
* All host router errors extend HostRouterError for easy instance checking.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Error options with cause
|
|
9
|
-
*/
|
|
10
|
-
interface ErrorOptions {
|
|
11
|
-
cause?: unknown;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Base error class for all host router errors
|
|
16
|
-
*/
|
|
17
|
-
export class HostRouterError extends Error {
|
|
18
|
-
cause?: unknown;
|
|
19
|
-
|
|
20
|
-
constructor(message: string, options?: ErrorOptions) {
|
|
21
|
-
super(message);
|
|
22
|
-
if (options?.cause) {
|
|
23
|
-
this.cause = options.cause;
|
|
24
|
-
}
|
|
25
|
-
this.name = 'HostRouterError';
|
|
26
|
-
Object.setPrototypeOf(this, HostRouterError.prototype);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Error thrown when pattern validation fails
|
|
32
|
-
*/
|
|
33
|
-
export class InvalidPatternError extends HostRouterError {
|
|
34
|
-
constructor(pattern: string, reason: string, options?: ErrorOptions) {
|
|
35
|
-
super(`Invalid pattern "${pattern}": ${reason}`, options);
|
|
36
|
-
this.name = 'InvalidPatternError';
|
|
37
|
-
Object.setPrototypeOf(this, InvalidPatternError.prototype);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Error thrown when cookie override is not allowed
|
|
43
|
-
*/
|
|
44
|
-
export class HostOverrideNotAllowedError extends HostRouterError {
|
|
45
|
-
constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
|
|
46
|
-
super(
|
|
47
|
-
`Host override not allowed on "${currentHost}" (cookie: ${cookieName})`,
|
|
48
|
-
options
|
|
49
|
-
);
|
|
50
|
-
this.name = 'HostOverrideNotAllowedError';
|
|
51
|
-
Object.setPrototypeOf(this, HostOverrideNotAllowedError.prototype);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Error thrown when cookie hostname is invalid
|
|
57
|
-
*/
|
|
58
|
-
export class InvalidHostnameError extends HostRouterError {
|
|
59
|
-
constructor(hostname: string, options?: ErrorOptions) {
|
|
60
|
-
super(`Invalid hostname format: "${hostname}"`, options);
|
|
61
|
-
this.name = 'InvalidHostnameError';
|
|
62
|
-
Object.setPrototypeOf(this, InvalidHostnameError.prototype);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Error thrown when custom validation fails
|
|
68
|
-
*/
|
|
69
|
-
export class HostValidationError extends HostRouterError {
|
|
70
|
-
constructor(message: string, cause?: unknown) {
|
|
71
|
-
super(message, { cause });
|
|
72
|
-
this.name = 'HostValidationError';
|
|
73
|
-
Object.setPrototypeOf(this, HostValidationError.prototype);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Error thrown when no route matches
|
|
79
|
-
*/
|
|
80
|
-
export class NoRouteMatchError extends HostRouterError {
|
|
81
|
-
constructor(hostname: string, pathname: string, options?: ErrorOptions) {
|
|
82
|
-
super(`No route matched for ${hostname}${pathname}`, options);
|
|
83
|
-
this.name = 'NoRouteMatchError';
|
|
84
|
-
Object.setPrototypeOf(this, NoRouteMatchError.prototype);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Error thrown when handler type is invalid
|
|
90
|
-
*/
|
|
91
|
-
export class InvalidHandlerError extends HostRouterError {
|
|
92
|
-
constructor(handler: unknown, options?: ErrorOptions) {
|
|
93
|
-
super(`Invalid handler type: ${typeof handler}`, options);
|
|
94
|
-
this.name = 'InvalidHandlerError';
|
|
95
|
-
Object.setPrototypeOf(this, InvalidHandlerError.prototype);
|
|
96
|
-
}
|
|
97
|
-
}
|
package/src/host/index.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host Router
|
|
3
|
-
*
|
|
4
|
-
* A routing system for managing multi-application hosting based on
|
|
5
|
-
* domain/subdomain patterns with support for cookie-based host override
|
|
6
|
-
* for development environments.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { createHostRouter } from '@rangojs/router/host';
|
|
11
|
-
*
|
|
12
|
-
* const router = createHostRouter();
|
|
13
|
-
*
|
|
14
|
-
* router.host(['.']).map(() => import('./apps/main'));
|
|
15
|
-
* router.host(['admin.*']).map(() => import('./apps/admin'));
|
|
16
|
-
*
|
|
17
|
-
* export default {
|
|
18
|
-
* fetch(request) {
|
|
19
|
-
* return router.match(request);
|
|
20
|
-
* }
|
|
21
|
-
* };
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
// Core router
|
|
26
|
-
export { createHostRouter } from './router.js';
|
|
27
|
-
|
|
28
|
-
// Host router registry for build-time discovery
|
|
29
|
-
export { HostRouterRegistry, type HostRouterRegistryEntry } from './router.js';
|
|
30
|
-
|
|
31
|
-
// Utilities
|
|
32
|
-
export { defineHosts } from './utils.js';
|
|
33
|
-
|
|
34
|
-
// Errors
|
|
35
|
-
export {
|
|
36
|
-
HostRouterError,
|
|
37
|
-
InvalidPatternError,
|
|
38
|
-
HostOverrideNotAllowedError,
|
|
39
|
-
InvalidHostnameError,
|
|
40
|
-
HostValidationError,
|
|
41
|
-
NoRouteMatchError,
|
|
42
|
-
InvalidHandlerError,
|
|
43
|
-
} from './errors.js';
|
|
44
|
-
|
|
45
|
-
// Types
|
|
46
|
-
export type {
|
|
47
|
-
HostRouter,
|
|
48
|
-
HostRouteBuilder,
|
|
49
|
-
HostRouterOptions,
|
|
50
|
-
Handler,
|
|
51
|
-
LazyHandler,
|
|
52
|
-
Middleware,
|
|
53
|
-
HostPattern,
|
|
54
|
-
HostMatchResult,
|
|
55
|
-
HostOverrideConfig,
|
|
56
|
-
} from './types.js';
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pattern Matching Engine
|
|
3
|
-
*
|
|
4
|
-
* Handles matching of hostnames and paths against various patterns:
|
|
5
|
-
* - `.` or `*` - any apex domain
|
|
6
|
-
* - `**` - any domain (apex + subdomains)
|
|
7
|
-
* - `*.` - any single-level subdomain
|
|
8
|
-
* - `**.` - any multi-level subdomain
|
|
9
|
-
* - `example.com` - exact domain
|
|
10
|
-
* - `*.com` - any apex .com domain
|
|
11
|
-
* - `*.example.com` - subdomain of example.com
|
|
12
|
-
* - `**.example.com` - any depth subdomain
|
|
13
|
-
* - `admin.*` - admin subdomain of any apex
|
|
14
|
-
* - `example.com/admin` - specific domain with path prefix
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { InvalidPatternError } from './errors.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Normalize a pattern by removing trailing slashes from paths
|
|
21
|
-
*/
|
|
22
|
-
export function normalizePattern(pattern: string): string {
|
|
23
|
-
// If pattern has a path component, remove trailing slash
|
|
24
|
-
const slashIndex = pattern.indexOf('/');
|
|
25
|
-
if (slashIndex !== -1) {
|
|
26
|
-
const domain = pattern.slice(0, slashIndex);
|
|
27
|
-
const path = pattern.slice(slashIndex).replace(/\/$/, '');
|
|
28
|
-
return domain + path;
|
|
29
|
-
}
|
|
30
|
-
return pattern;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parse hostname and path from request URL
|
|
35
|
-
*/
|
|
36
|
-
export function parseRequest(request: Request): {
|
|
37
|
-
hostname: string;
|
|
38
|
-
pathname: string;
|
|
39
|
-
parts: string[];
|
|
40
|
-
} {
|
|
41
|
-
const url = new URL(request.url);
|
|
42
|
-
const hostname = url.hostname;
|
|
43
|
-
const pathname = url.pathname;
|
|
44
|
-
const parts = hostname.split('.');
|
|
45
|
-
|
|
46
|
-
return { hostname, pathname, parts };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Count subdomain levels (0 for apex, 1+ for subdomains)
|
|
51
|
-
*/
|
|
52
|
-
function getSubdomainLevel(parts: string[]): number {
|
|
53
|
-
// Apex domain has 2 parts (example.com)
|
|
54
|
-
// Single subdomain has 3 parts (www.example.com)
|
|
55
|
-
// Multi-level has 4+ parts (a.b.example.com)
|
|
56
|
-
return Math.max(0, parts.length - 2);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Check if hostname is an apex domain (no subdomains)
|
|
61
|
-
*/
|
|
62
|
-
function isApexDomain(parts: string[]): boolean {
|
|
63
|
-
return parts.length === 2;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Match a single pattern against hostname and path
|
|
68
|
-
*/
|
|
69
|
-
export function matchPattern(
|
|
70
|
-
pattern: string,
|
|
71
|
-
hostname: string,
|
|
72
|
-
pathname: string,
|
|
73
|
-
parts: string[]
|
|
74
|
-
): boolean {
|
|
75
|
-
const normalized = normalizePattern(pattern);
|
|
76
|
-
|
|
77
|
-
// Check if pattern has path component
|
|
78
|
-
const slashIndex = normalized.indexOf('/');
|
|
79
|
-
const hasPath = slashIndex !== -1;
|
|
80
|
-
const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
|
|
81
|
-
const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
|
|
82
|
-
|
|
83
|
-
// First match domain
|
|
84
|
-
const domainMatch = matchDomainPattern(domainPattern, hostname, parts);
|
|
85
|
-
if (!domainMatch) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Then match path (prefix match)
|
|
90
|
-
if (pathPattern) {
|
|
91
|
-
return pathname === pathPattern || pathname.startsWith(pathPattern + '/');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Match domain pattern against hostname
|
|
99
|
-
*/
|
|
100
|
-
function matchDomainPattern(
|
|
101
|
-
pattern: string,
|
|
102
|
-
hostname: string,
|
|
103
|
-
parts: string[]
|
|
104
|
-
): boolean {
|
|
105
|
-
// Exact match
|
|
106
|
-
if (pattern === hostname) {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// `.` or `*` - any apex domain
|
|
111
|
-
if (pattern === '.' || pattern === '*') {
|
|
112
|
-
return isApexDomain(parts);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// `**` - any domain (apex + all subdomains)
|
|
116
|
-
if (pattern === '**') {
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// `*.` - any single-level subdomain
|
|
121
|
-
if (pattern === '*.') {
|
|
122
|
-
return getSubdomainLevel(parts) === 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// `**.` - any multi-level subdomain (2+ levels)
|
|
126
|
-
if (pattern === '**.') {
|
|
127
|
-
return getSubdomainLevel(parts) >= 2;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// `*.tld` - any apex domain with specific TLD (e.g., *.com)
|
|
131
|
-
if (pattern.startsWith('*.') && !pattern.includes('.', 2)) {
|
|
132
|
-
const tld = pattern.slice(2);
|
|
133
|
-
return isApexDomain(parts) && hostname.endsWith('.' + tld);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// `*.example.com` - single subdomain of specific domain
|
|
137
|
-
if (pattern.startsWith('*.')) {
|
|
138
|
-
const baseDomain = pattern.slice(2);
|
|
139
|
-
if (hostname.endsWith('.' + baseDomain)) {
|
|
140
|
-
// Count parts: if pattern is *.example.com (3 parts),
|
|
141
|
-
// hostname should have exactly 4 parts (www.example.com)
|
|
142
|
-
const patternParts = baseDomain.split('.');
|
|
143
|
-
return parts.length === patternParts.length + 1;
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// `**.example.com` - any depth subdomain of specific domain
|
|
149
|
-
if (pattern.startsWith('**.')) {
|
|
150
|
-
const baseDomain = pattern.slice(3);
|
|
151
|
-
if (hostname.endsWith('.' + baseDomain)) {
|
|
152
|
-
const patternParts = baseDomain.split('.');
|
|
153
|
-
// Must have more parts than the base domain (i.e., has subdomains)
|
|
154
|
-
return parts.length > patternParts.length;
|
|
155
|
-
}
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// `subdomain.*` - specific subdomain of any apex domain
|
|
160
|
-
// e.g., admin.* matches admin.example.com, admin.google.com
|
|
161
|
-
if (pattern.endsWith('.*')) {
|
|
162
|
-
const subdomain = pattern.slice(0, -2);
|
|
163
|
-
// Must be single-level subdomain (3 parts total)
|
|
164
|
-
if (parts.length === 3 && parts[0] === subdomain) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// `subdomain.**` - specific subdomain of any domain (including multi-level)
|
|
171
|
-
// e.g., admin.** matches admin.example.com, admin.sub.example.com
|
|
172
|
-
if (pattern.endsWith('.**')) {
|
|
173
|
-
const subdomain = pattern.slice(0, -3);
|
|
174
|
-
if (parts.length >= 3 && parts[0] === subdomain) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// `subdomain.` - specific subdomain of any apex domain (no wildcard)
|
|
181
|
-
// e.g., admin. matches admin.example.com, admin.google.com
|
|
182
|
-
if (pattern.endsWith('.') && !pattern.includes('*')) {
|
|
183
|
-
const subdomain = pattern.slice(0, -1);
|
|
184
|
-
// Must be exactly 3 parts (subdomain.domain.tld)
|
|
185
|
-
if (parts.length === 3 && parts[0] === subdomain) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Validate pattern format
|
|
196
|
-
*/
|
|
197
|
-
export function validatePattern(pattern: string): void {
|
|
198
|
-
if (!pattern || typeof pattern !== 'string') {
|
|
199
|
-
throw new InvalidPatternError(
|
|
200
|
-
pattern,
|
|
201
|
-
'Pattern must be a non-empty string',
|
|
202
|
-
{ cause: { type: typeof pattern, value: pattern } }
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Check for invalid characters (spaces, etc.)
|
|
207
|
-
if (/\s/.test(pattern)) {
|
|
208
|
-
throw new InvalidPatternError(pattern, 'contains whitespace', {
|
|
209
|
-
cause: { pattern },
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Additional validation can be added here
|
|
214
|
-
}
|
package/src/host/router.ts
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host Router Implementation
|
|
3
|
-
*
|
|
4
|
-
* Main router that handles host-based routing with middleware and cookie override.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
HostRouter,
|
|
9
|
-
HostRouteBuilder,
|
|
10
|
-
HostRouterOptions,
|
|
11
|
-
Handler,
|
|
12
|
-
LazyHandler,
|
|
13
|
-
Middleware,
|
|
14
|
-
HostPattern,
|
|
15
|
-
RouteEntry,
|
|
16
|
-
HostMatchResult,
|
|
17
|
-
} from './types.js';
|
|
18
|
-
import {
|
|
19
|
-
matchPattern,
|
|
20
|
-
parseRequest,
|
|
21
|
-
normalizePattern,
|
|
22
|
-
validatePattern,
|
|
23
|
-
} from './pattern-matcher.js';
|
|
24
|
-
import {
|
|
25
|
-
handleCookieOverride,
|
|
26
|
-
createCookieErrorResponse,
|
|
27
|
-
} from './cookie-handler.js';
|
|
28
|
-
import {
|
|
29
|
-
HostRouterError,
|
|
30
|
-
NoRouteMatchError,
|
|
31
|
-
InvalidHandlerError,
|
|
32
|
-
} from './errors.js';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Registry entry for a host router instance.
|
|
36
|
-
* Stores references to the live routes array and fallback, so the discovery
|
|
37
|
-
* plugin can iterate handlers registered after createHostRouter() returns.
|
|
38
|
-
*/
|
|
39
|
-
export interface HostRouterRegistryEntry {
|
|
40
|
-
routes: RouteEntry[];
|
|
41
|
-
fallback: RouteEntry | null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Global registry for host routers (parallel to RouterRegistry for RSC routers).
|
|
46
|
-
* Populated by createHostRouter() so the build-time discovery plugin can find
|
|
47
|
-
* host routers and resolve their lazy handlers to trigger sub-app createRouter() calls.
|
|
48
|
-
*/
|
|
49
|
-
export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> = new Map();
|
|
50
|
-
|
|
51
|
-
let hostRouterAutoId = 0;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Create a host router
|
|
55
|
-
*/
|
|
56
|
-
export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
|
|
57
|
-
const routes: RouteEntry[] = [];
|
|
58
|
-
const globalMiddleware: Middleware[] = [];
|
|
59
|
-
let fallbackRoute: RouteEntry | null = null;
|
|
60
|
-
|
|
61
|
-
const { debug = false, hostOverride } = options;
|
|
62
|
-
|
|
63
|
-
function log(message: string, ...args: any[]): void {
|
|
64
|
-
if (debug) {
|
|
65
|
-
console.log(`[HostRouter] ${message}`, ...args);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Create a route builder for chaining
|
|
71
|
-
*/
|
|
72
|
-
function createRouteBuilder(
|
|
73
|
-
patterns: string[],
|
|
74
|
-
isFallback = false
|
|
75
|
-
): HostRouteBuilder {
|
|
76
|
-
const middleware: Middleware[] = [];
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
use(...mw: Middleware[]): HostRouteBuilder {
|
|
80
|
-
middleware.push(...mw);
|
|
81
|
-
return this;
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
map(handler: Handler | LazyHandler): HostRouter {
|
|
85
|
-
const entry: RouteEntry = {
|
|
86
|
-
patterns,
|
|
87
|
-
middleware,
|
|
88
|
-
handler,
|
|
89
|
-
isFallback,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
if (isFallback) {
|
|
93
|
-
fallbackRoute = entry;
|
|
94
|
-
} else {
|
|
95
|
-
routes.push(entry);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
log(
|
|
99
|
-
`Registered ${isFallback ? 'fallback' : 'route'}:`,
|
|
100
|
-
patterns.join(', ')
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
return router;
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Find matching route for hostname and path
|
|
110
|
-
*/
|
|
111
|
-
function findMatchingRoute(
|
|
112
|
-
hostname: string,
|
|
113
|
-
pathname: string
|
|
114
|
-
): RouteEntry | null {
|
|
115
|
-
const parts = hostname.split('.');
|
|
116
|
-
|
|
117
|
-
for (const route of routes) {
|
|
118
|
-
for (const pattern of route.patterns) {
|
|
119
|
-
if (matchPattern(pattern, hostname, pathname, parts)) {
|
|
120
|
-
log(`Matched pattern: "${pattern}"`);
|
|
121
|
-
return route;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Execute middleware chain
|
|
131
|
-
*/
|
|
132
|
-
async function executeMiddleware(
|
|
133
|
-
middleware: Middleware[],
|
|
134
|
-
request: Request,
|
|
135
|
-
context: any,
|
|
136
|
-
finalHandler: () => Promise<Response>
|
|
137
|
-
): Promise<Response> {
|
|
138
|
-
let index = 0;
|
|
139
|
-
|
|
140
|
-
async function next(): Promise<Response> {
|
|
141
|
-
if (index >= middleware.length) {
|
|
142
|
-
return finalHandler();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const mw = middleware[index++];
|
|
146
|
-
if (!mw) {
|
|
147
|
-
return finalHandler();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return mw(request, context, next);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return next();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Execute handler (lazy or direct)
|
|
158
|
-
*/
|
|
159
|
-
async function executeHandler(
|
|
160
|
-
handler: Handler | LazyHandler,
|
|
161
|
-
request: Request,
|
|
162
|
-
context: any
|
|
163
|
-
): Promise<Response> {
|
|
164
|
-
// Check if it's a lazy handler (function that returns promise)
|
|
165
|
-
if (typeof handler === 'function') {
|
|
166
|
-
const result = handler(request, context);
|
|
167
|
-
|
|
168
|
-
// If it returns a promise with default export
|
|
169
|
-
if (result && typeof result === 'object' && 'then' in result) {
|
|
170
|
-
const module = await result;
|
|
171
|
-
if (
|
|
172
|
-
typeof module === 'object' &&
|
|
173
|
-
module !== null &&
|
|
174
|
-
'default' in module
|
|
175
|
-
) {
|
|
176
|
-
const defaultExport = (module as { default: Handler | HostRouter })
|
|
177
|
-
.default;
|
|
178
|
-
|
|
179
|
-
// If default export is a router with match method
|
|
180
|
-
if (
|
|
181
|
-
typeof defaultExport === 'object' &&
|
|
182
|
-
defaultExport !== null &&
|
|
183
|
-
'match' in defaultExport
|
|
184
|
-
) {
|
|
185
|
-
return (defaultExport as HostRouter).match(request, context);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Otherwise treat as handler
|
|
189
|
-
return (defaultExport as Handler)(request, context);
|
|
190
|
-
}
|
|
191
|
-
// If promise resolves to Response
|
|
192
|
-
return result as Promise<Response>;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Direct handler
|
|
196
|
-
return result as Response | Promise<Response>;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
throw new InvalidHandlerError(handler, {
|
|
200
|
-
cause: { handlerType: typeof handler },
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Router instance
|
|
206
|
-
*/
|
|
207
|
-
const router: HostRouter = {
|
|
208
|
-
host(patterns: HostPattern): HostRouteBuilder {
|
|
209
|
-
const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
|
|
210
|
-
|
|
211
|
-
// Validate and normalize patterns
|
|
212
|
-
const normalized = patternsArray.map((p) => {
|
|
213
|
-
validatePattern(p);
|
|
214
|
-
return normalizePattern(p);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return createRouteBuilder(normalized, false);
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
use(...middleware: Middleware[]): HostRouter {
|
|
221
|
-
globalMiddleware.push(...middleware);
|
|
222
|
-
log(`Registered global middleware (${middleware.length})`);
|
|
223
|
-
return router;
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
fallback(): HostRouteBuilder {
|
|
227
|
-
return createRouteBuilder([], true);
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
test(hostname: string): HostMatchResult | null {
|
|
231
|
-
const parts = hostname.split('.');
|
|
232
|
-
const pathname = '/';
|
|
233
|
-
|
|
234
|
-
for (const route of routes) {
|
|
235
|
-
for (const pattern of route.patterns) {
|
|
236
|
-
if (matchPattern(pattern, hostname, pathname, parts)) {
|
|
237
|
-
return {
|
|
238
|
-
pattern,
|
|
239
|
-
handler: route.handler,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return null;
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
async match(request: Request, context: any = {}): Promise<Response> {
|
|
249
|
-
log(`Request: ${request.url}`);
|
|
250
|
-
|
|
251
|
-
let effectiveHostname: string;
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
// Handle cookie override (may throw HostRouterError)
|
|
255
|
-
effectiveHostname = handleCookieOverride(
|
|
256
|
-
request,
|
|
257
|
-
hostOverride,
|
|
258
|
-
context
|
|
259
|
-
);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
// If it's a HostRouterError from cookie override
|
|
262
|
-
if (error instanceof HostRouterError) {
|
|
263
|
-
log(`Cookie override error: ${error.message}`);
|
|
264
|
-
|
|
265
|
-
// If fallback exists, use it
|
|
266
|
-
if (fallbackRoute) {
|
|
267
|
-
context.error = error;
|
|
268
|
-
const allMiddleware = [
|
|
269
|
-
...globalMiddleware,
|
|
270
|
-
...fallbackRoute.middleware,
|
|
271
|
-
];
|
|
272
|
-
|
|
273
|
-
return executeMiddleware(allMiddleware, request, context, () =>
|
|
274
|
-
executeHandler(fallbackRoute!.handler, request, context)
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Otherwise return error response with cookie deletion
|
|
279
|
-
if (hostOverride) {
|
|
280
|
-
return createCookieErrorResponse(
|
|
281
|
-
hostOverride.cookieName,
|
|
282
|
-
error.message
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Re-throw non-HostRouterErrors
|
|
288
|
-
throw error;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const { pathname } = parseRequest(request);
|
|
292
|
-
|
|
293
|
-
if (effectiveHostname !== parseRequest(request).hostname) {
|
|
294
|
-
log(`Cookie override: ${effectiveHostname}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Find matching route
|
|
298
|
-
const matchedRoute = findMatchingRoute(effectiveHostname, pathname);
|
|
299
|
-
|
|
300
|
-
if (!matchedRoute) {
|
|
301
|
-
log(`No route matched`);
|
|
302
|
-
throw new NoRouteMatchError(effectiveHostname, pathname, {
|
|
303
|
-
cause: {
|
|
304
|
-
hostname: effectiveHostname,
|
|
305
|
-
pathname,
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Combine global and route-specific middleware
|
|
311
|
-
const allMiddleware = [...globalMiddleware, ...matchedRoute.middleware];
|
|
312
|
-
|
|
313
|
-
// Execute middleware chain and handler
|
|
314
|
-
return executeMiddleware(allMiddleware, request, context, () =>
|
|
315
|
-
executeHandler(matchedRoute.handler, request, context)
|
|
316
|
-
);
|
|
317
|
-
},
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
// Register in the global HostRouterRegistry for build-time discovery.
|
|
321
|
-
// The routes array and fallbackRoute ref are live - they reflect routes
|
|
322
|
-
// added via .host().map() after this point.
|
|
323
|
-
const registryId = `host-router-${hostRouterAutoId++}`;
|
|
324
|
-
HostRouterRegistry.set(registryId, {
|
|
325
|
-
get routes() { return routes; },
|
|
326
|
-
get fallback() { return fallbackRoute; },
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
return router;
|
|
330
|
-
}
|
package/src/host/testing.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Testing Utilities for Host Router
|
|
3
|
-
*
|
|
4
|
-
* Helper functions for testing host routing.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { matchPattern } from './pattern-matcher.js';
|
|
8
|
-
|
|
9
|
-
export interface CreateTestRequestOptions {
|
|
10
|
-
host: string;
|
|
11
|
-
path?: string;
|
|
12
|
-
method?: string;
|
|
13
|
-
cookies?: Record<string, string>;
|
|
14
|
-
headers?: Record<string, string>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Create a test request with specific host and cookies
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* const request = createTestRequest({
|
|
23
|
-
* host: 'admin.example.com',
|
|
24
|
-
* path: '/dashboard',
|
|
25
|
-
* cookies: { 'x-requested-host': 'api.example.com' }
|
|
26
|
-
* });
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export function createTestRequest(options: CreateTestRequestOptions): Request {
|
|
30
|
-
const {
|
|
31
|
-
host,
|
|
32
|
-
path = '/',
|
|
33
|
-
method = 'GET',
|
|
34
|
-
cookies = {},
|
|
35
|
-
headers = {},
|
|
36
|
-
} = options;
|
|
37
|
-
|
|
38
|
-
const url = `http://${host}${path}`;
|
|
39
|
-
const requestHeaders = new Headers(headers);
|
|
40
|
-
|
|
41
|
-
// Add cookies if provided
|
|
42
|
-
if (Object.keys(cookies).length > 0) {
|
|
43
|
-
const cookieString = Object.entries(cookies)
|
|
44
|
-
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
45
|
-
.join('; ');
|
|
46
|
-
requestHeaders.set('cookie', cookieString);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return new Request(url, {
|
|
50
|
-
method,
|
|
51
|
-
headers: requestHeaders,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Test if a pattern matches a hostname
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* expect(testPattern('admin.*', 'admin.example.com')).toBe(true);
|
|
61
|
-
* expect(testPattern(['*', 'www.*'], 'example.com')).toBe(true);
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export function testPattern(
|
|
65
|
-
pattern: string | string[],
|
|
66
|
-
hostname: string
|
|
67
|
-
): boolean {
|
|
68
|
-
const patterns = Array.isArray(pattern) ? pattern : [pattern];
|
|
69
|
-
const parts = hostname.split('.');
|
|
70
|
-
const pathname = '/';
|
|
71
|
-
|
|
72
|
-
for (const p of patterns) {
|
|
73
|
-
if (matchPattern(p, hostname, pathname, parts)) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return false;
|
|
79
|
-
}
|
package/src/host/types.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host Router Types
|
|
3
|
-
*
|
|
4
|
-
* Type definitions for the host-based routing system.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Handler function that processes a request and returns a response
|
|
9
|
-
*/
|
|
10
|
-
export type Handler = (
|
|
11
|
-
request: Request,
|
|
12
|
-
context: any
|
|
13
|
-
) => Response | Promise<Response>;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Lazy handler that dynamically imports a module with a default handler or router
|
|
17
|
-
*/
|
|
18
|
-
export type LazyHandler = () => Promise<{ default: Handler | HostRouter }>;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Middleware function that can intercept and modify requests/responses
|
|
22
|
-
*/
|
|
23
|
-
export type Middleware = (
|
|
24
|
-
request: Request,
|
|
25
|
-
context: any,
|
|
26
|
-
next: () => Promise<Response>
|
|
27
|
-
) => Promise<Response>;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Host pattern - can be a string or array of strings
|
|
31
|
-
*/
|
|
32
|
-
export type HostPattern = string | string[];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Result from testing a hostname against patterns
|
|
36
|
-
*/
|
|
37
|
-
export interface HostMatchResult {
|
|
38
|
-
pattern: string;
|
|
39
|
-
handler: Handler | LazyHandler;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Host route builder for chaining middleware and handler
|
|
44
|
-
*/
|
|
45
|
-
export interface HostRouteBuilder {
|
|
46
|
-
/**
|
|
47
|
-
* Add middleware to this host pattern
|
|
48
|
-
*/
|
|
49
|
-
use(...middleware: Middleware[]): HostRouteBuilder;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Map to a handler or lazy import
|
|
53
|
-
*/
|
|
54
|
-
map(handler: Handler | LazyHandler): HostRouter;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Main host router interface
|
|
59
|
-
*/
|
|
60
|
-
export interface HostRouter {
|
|
61
|
-
/**
|
|
62
|
-
* Register a host pattern
|
|
63
|
-
*/
|
|
64
|
-
host(patterns: HostPattern): HostRouteBuilder;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Register global middleware
|
|
68
|
-
*/
|
|
69
|
-
use(...middleware: Middleware[]): HostRouter;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Match an incoming request
|
|
73
|
-
*/
|
|
74
|
-
match(request: Request, context?: any): Promise<Response>;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Register fallback handler for allowed hosts without valid cookie
|
|
78
|
-
*/
|
|
79
|
-
fallback(): HostRouteBuilder;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Test which handler would match a hostname
|
|
83
|
-
*/
|
|
84
|
-
test(hostname: string): HostMatchResult | null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Host override configuration
|
|
89
|
-
*/
|
|
90
|
-
export interface HostOverrideConfig {
|
|
91
|
-
/**
|
|
92
|
-
* Cookie name to read for host override
|
|
93
|
-
*/
|
|
94
|
-
cookieName: string;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Hosts that are allowed to use override
|
|
98
|
-
*/
|
|
99
|
-
allowedHosts: string[];
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Optional validation function
|
|
103
|
-
*/
|
|
104
|
-
validate?: (request: Request, cookieValue: string, context: any) => string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Host router options
|
|
109
|
-
*/
|
|
110
|
-
export interface HostRouterOptions {
|
|
111
|
-
/**
|
|
112
|
-
* Enable debug logging
|
|
113
|
-
*/
|
|
114
|
-
debug?: boolean;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Cookie-based host override configuration
|
|
118
|
-
*/
|
|
119
|
-
hostOverride?: HostOverrideConfig;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Internal route entry
|
|
124
|
-
*/
|
|
125
|
-
export interface RouteEntry {
|
|
126
|
-
patterns: string[];
|
|
127
|
-
middleware: Middleware[];
|
|
128
|
-
handler: Handler | LazyHandler;
|
|
129
|
-
isFallback?: boolean;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Pattern match result (internal)
|
|
134
|
-
*/
|
|
135
|
-
export interface PatternMatchResult {
|
|
136
|
-
matched: boolean;
|
|
137
|
-
routeEntry?: RouteEntry;
|
|
138
|
-
}
|
package/src/host/utils.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host Router Utilities
|
|
3
|
-
*
|
|
4
|
-
* Helper functions for type-safe pattern definitions.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Define hosts with type safety
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* const hosts = defineHosts({
|
|
13
|
-
* admin: 'admin.*',
|
|
14
|
-
* api: 'api.*',
|
|
15
|
-
* app: ['*', 'www.*']
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* router.host(hosts.admin).map(...); // Type-safe!
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export function defineHosts<T extends Record<string, string | string[]>>(
|
|
22
|
-
hosts: T
|
|
23
|
-
): Readonly<T> {
|
|
24
|
-
return Object.freeze(hosts);
|
|
25
|
-
}
|