@yumerijs/core 2.2.2 → 3.0.0-alpha.2
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/context.d.ts +12 -12
- package/dist/context.js +21 -8
- package/dist/core.d.ts +27 -13
- package/dist/core.js +84 -12
- package/dist/route.d.ts +11 -2
- package/dist/route.js +245 -5
- package/dist/server.d.ts +22 -4
- package/dist/server.js +33 -30
- package/dist/session.d.ts +176 -75
- package/dist/session.js +226 -229
- package/package.json +2 -1
package/dist/context.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { Core } from './core.js';
|
|
1
|
+
import { Core, Plugin } from './core.js';
|
|
2
2
|
import { Route } from './route.js';
|
|
3
3
|
import { HookHandler } from './hook.js';
|
|
4
4
|
import { Middleware } from './middleware.js';
|
|
5
5
|
import { IRenderer } from '@yumerijs/types';
|
|
6
|
-
interface Plugin {
|
|
7
|
-
apply: (ctx: Context, config: any) => any;
|
|
8
|
-
disable: (ctx: Context) => Promise<void>;
|
|
9
|
-
depend: Array<string>;
|
|
10
|
-
provide: Array<string>;
|
|
11
|
-
}
|
|
12
6
|
export interface Components {
|
|
7
|
+
[key: string]: any;
|
|
13
8
|
}
|
|
14
9
|
/**
|
|
15
10
|
* 插件上下文对象
|
|
@@ -27,7 +22,7 @@ export declare class Context {
|
|
|
27
22
|
private i18ns;
|
|
28
23
|
component: Components;
|
|
29
24
|
renderer?: IRenderer;
|
|
30
|
-
|
|
25
|
+
module: any;
|
|
31
26
|
childpath: string;
|
|
32
27
|
/** 插件名称 */
|
|
33
28
|
pluginname: string;
|
|
@@ -36,7 +31,7 @@ export declare class Context {
|
|
|
36
31
|
* @param core Core 实例
|
|
37
32
|
* @param pluginname 插件名称
|
|
38
33
|
*/
|
|
39
|
-
constructor(core: Core, pluginname: string,
|
|
34
|
+
constructor(core: Core, pluginname: string, module?: any, injections?: Record<string, any>);
|
|
40
35
|
/**
|
|
41
36
|
* 注入依赖
|
|
42
37
|
* @param name 依赖名称
|
|
@@ -101,10 +96,16 @@ export declare class Context {
|
|
|
101
96
|
fork(name?: string, path?: string): Context;
|
|
102
97
|
/**
|
|
103
98
|
* 注册子插件
|
|
104
|
-
* @param
|
|
99
|
+
* @param module 插件模块
|
|
100
|
+
* @param config 插件配置
|
|
101
|
+
*/
|
|
102
|
+
apply(module: Plugin, config: any): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* 快速注册子插件(自动 fork)
|
|
105
|
+
* @param module 插件模块
|
|
105
106
|
* @param config 插件配置
|
|
106
107
|
*/
|
|
107
|
-
|
|
108
|
+
plugin(module: Plugin, config?: any): Promise<Context>;
|
|
108
109
|
/**
|
|
109
110
|
* 注册 i18n
|
|
110
111
|
* @param content 内容(可以是嵌套对象或单个key)
|
|
@@ -116,4 +117,3 @@ export declare class Context {
|
|
|
116
117
|
*/
|
|
117
118
|
dispose(): Promise<void>;
|
|
118
119
|
}
|
|
119
|
-
export {};
|
package/dist/context.js
CHANGED
|
@@ -16,7 +16,7 @@ export class Context {
|
|
|
16
16
|
i18ns = [];
|
|
17
17
|
component;
|
|
18
18
|
renderer;
|
|
19
|
-
|
|
19
|
+
module;
|
|
20
20
|
childpath = '/';
|
|
21
21
|
/** 插件名称 */
|
|
22
22
|
pluginname;
|
|
@@ -25,9 +25,9 @@ export class Context {
|
|
|
25
25
|
* @param core Core 实例
|
|
26
26
|
* @param pluginname 插件名称
|
|
27
27
|
*/
|
|
28
|
-
constructor(core, pluginname,
|
|
28
|
+
constructor(core, pluginname, module, injections = {}) {
|
|
29
29
|
this.core = core;
|
|
30
|
-
this.
|
|
30
|
+
this.module = module;
|
|
31
31
|
this.pluginname = pluginname;
|
|
32
32
|
this.childPlugins = new Map();
|
|
33
33
|
this.component = injections;
|
|
@@ -145,15 +145,28 @@ export class Context {
|
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
147
|
* 注册子插件
|
|
148
|
-
* @param
|
|
148
|
+
* @param module 插件模块
|
|
149
149
|
* @param config 插件配置
|
|
150
150
|
*/
|
|
151
|
-
async apply(
|
|
152
|
-
if (!
|
|
151
|
+
async apply(module, config) {
|
|
152
|
+
if (!module)
|
|
153
153
|
return;
|
|
154
154
|
const ctx = this.fork();
|
|
155
|
-
await plugin
|
|
156
|
-
this.childPlugins.set(ctx,
|
|
155
|
+
await this.core.plugin(module, ctx, config);
|
|
156
|
+
this.childPlugins.set(ctx, module);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 快速注册子插件(自动 fork)
|
|
160
|
+
* @param module 插件模块
|
|
161
|
+
* @param config 插件配置
|
|
162
|
+
*/
|
|
163
|
+
async plugin(module, config = {}) {
|
|
164
|
+
if (!module)
|
|
165
|
+
return;
|
|
166
|
+
const ctx = this.fork();
|
|
167
|
+
await this.core.plugin(module, ctx, config);
|
|
168
|
+
this.childPlugins.set(ctx, module);
|
|
169
|
+
return ctx;
|
|
157
170
|
}
|
|
158
171
|
/**
|
|
159
172
|
* 注册 i18n
|
package/dist/core.d.ts
CHANGED
|
@@ -9,19 +9,32 @@ import { HookHandler, Hook } from './hook.js';
|
|
|
9
9
|
import { Server as CoreServer } from './server.js';
|
|
10
10
|
import { I18n } from './i18n.js';
|
|
11
11
|
import { IRenderer } from '@yumerijs/types';
|
|
12
|
-
interface Plugin {
|
|
13
|
-
apply
|
|
14
|
-
disable
|
|
15
|
-
depend
|
|
16
|
-
provide
|
|
12
|
+
export interface Plugin {
|
|
13
|
+
apply?: (ctx: Context, config: Config) => Promise<void> | void;
|
|
14
|
+
disable?: (ctx: Context) => Promise<void> | void;
|
|
15
|
+
depend?: Array<string>;
|
|
16
|
+
provide?: Array<string>;
|
|
17
|
+
render?: string;
|
|
18
|
+
config?: Schema<any>;
|
|
17
19
|
}
|
|
20
|
+
type PluginConstructor = new (ctx: Context, config: Config) => Plugin;
|
|
21
|
+
type PluginModuleLike = Plugin | PluginConstructor | ((ctx: Context, config: Config) => Promise<void> | void) | {
|
|
22
|
+
default?: Plugin | PluginConstructor | ((ctx: Context, config: Config) => Promise<void> | void);
|
|
23
|
+
apply?: ((ctx: Context, config: Config) => Promise<void> | void);
|
|
24
|
+
disable?: (ctx: Context) => Promise<void> | void;
|
|
25
|
+
depend?: Array<string>;
|
|
26
|
+
provide?: Array<string>;
|
|
27
|
+
render?: string;
|
|
28
|
+
config?: Schema<any>;
|
|
29
|
+
};
|
|
30
|
+
export declare function resolvePluginModule(module: PluginModuleLike, context: Context, config: Config): Plugin;
|
|
18
31
|
export interface CoreOptions {
|
|
19
|
-
port
|
|
20
|
-
host
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
port?: number;
|
|
33
|
+
host?: string;
|
|
34
|
+
enableCors?: boolean;
|
|
35
|
+
enableWs?: boolean;
|
|
36
|
+
lang?: string[];
|
|
37
|
+
skipcheckUpdates?: boolean;
|
|
25
38
|
}
|
|
26
39
|
export declare const coreConfigSchema: Schema<CoreOptions>;
|
|
27
40
|
export declare const enum PluginStatus {
|
|
@@ -44,12 +57,13 @@ export declare class Core {
|
|
|
44
57
|
loader: any;
|
|
45
58
|
renderers: Map<string, IRenderer>;
|
|
46
59
|
pluginRenderers: Map<string, string>;
|
|
47
|
-
constructor(loader?: any, coreConfig?: CoreOptions,
|
|
60
|
+
constructor(loader?: any, coreConfig?: CoreOptions, loggersetCore?: boolean, splash?: boolean);
|
|
61
|
+
checkUpdate(): Promise<void>;
|
|
48
62
|
addRenderer(renderer: IRenderer): void;
|
|
49
63
|
getRendererForPlugin(pluginName: string): string | undefined;
|
|
50
64
|
runCore(): Promise<void>;
|
|
51
65
|
getShortPluginName(pluginName: string): string;
|
|
52
|
-
plugin(
|
|
66
|
+
plugin(module: PluginModuleLike, context: Context, config: Config): Promise<void>;
|
|
53
67
|
registerComponent(name: string, component: any): void;
|
|
54
68
|
getComponent(name: string): any;
|
|
55
69
|
unregisterComponent(name: string): void;
|
package/dist/core.js
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import { Schema } from './config.js';
|
|
2
|
+
import { Schema, fallback } from './config.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
import { Route } from './route.js';
|
|
5
5
|
import { Hook } from './hook.js';
|
|
6
6
|
import { Server as CoreServer } from './server.js';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
const version = JSON.parse(await fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
9
|
+
function isClassPlugin(value) {
|
|
10
|
+
if (typeof value !== 'function')
|
|
11
|
+
return false;
|
|
12
|
+
const source = Function.prototype.toString.call(value);
|
|
13
|
+
return source.startsWith('class ');
|
|
14
|
+
}
|
|
15
|
+
function mergePluginMeta(target, source) {
|
|
16
|
+
if (!source || typeof source !== 'object')
|
|
17
|
+
return target;
|
|
18
|
+
if (target.depend == null && Array.isArray(source.depend))
|
|
19
|
+
target.depend = source.depend;
|
|
20
|
+
if (target.provide == null && Array.isArray(source.provide))
|
|
21
|
+
target.provide = source.provide;
|
|
22
|
+
if (target.render == null && typeof source.render === 'string')
|
|
23
|
+
target.render = source.render;
|
|
24
|
+
if (target.config == null && source.config)
|
|
25
|
+
target.config = source.config;
|
|
26
|
+
if (target.disable == null && typeof source.disable === 'function')
|
|
27
|
+
target.disable = source.disable.bind(source);
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
export function resolvePluginModule(module, context, config) {
|
|
31
|
+
const candidate = module?.default ?? module?.apply ?? module;
|
|
32
|
+
let plugin;
|
|
33
|
+
if (isClassPlugin(candidate)) {
|
|
34
|
+
plugin = new candidate(context, config);
|
|
35
|
+
}
|
|
36
|
+
else if (typeof candidate === 'function') {
|
|
37
|
+
plugin = { apply: candidate };
|
|
38
|
+
}
|
|
39
|
+
else if (candidate && typeof candidate === 'object') {
|
|
40
|
+
plugin = candidate;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
throw new TypeError('Invalid plugin module. Expected class, function, or plugin object.');
|
|
44
|
+
}
|
|
45
|
+
mergePluginMeta(plugin, module);
|
|
46
|
+
if (candidate !== module) {
|
|
47
|
+
mergePluginMeta(plugin, candidate);
|
|
48
|
+
}
|
|
49
|
+
return plugin;
|
|
50
|
+
}
|
|
7
51
|
export const coreConfigSchema = Schema.object({
|
|
8
52
|
port: Schema.number('监听端口').default(14510),
|
|
9
53
|
host: Schema.string('监听地址').default('0.0.0.0'),
|
|
10
|
-
staticDir: Schema.string('静态目录').default('public'),
|
|
11
54
|
enableCors: Schema.boolean('启用跨域').default(false),
|
|
12
55
|
enableWs: Schema.boolean('启用 WebSocket').default(false),
|
|
13
56
|
lang: Schema.array(Schema.string(), '语言列表').default(['zh', 'en']),
|
|
57
|
+
skipcheckUpdates: Schema.boolean('启动时检查更新').default(false)
|
|
14
58
|
});
|
|
15
59
|
export class Core {
|
|
16
60
|
emitter = new EventEmitter();
|
|
@@ -25,12 +69,32 @@ export class Core {
|
|
|
25
69
|
loader;
|
|
26
70
|
renderers = new Map();
|
|
27
71
|
pluginRenderers = new Map(); // Stores which plugin uses which renderer
|
|
28
|
-
constructor(loader, coreConfig,
|
|
29
|
-
this.coreConfig = coreConfig
|
|
72
|
+
constructor(loader, coreConfig = {}, loggersetCore = true, splash = true) {
|
|
73
|
+
this.coreConfig = fallback(coreConfigSchema, coreConfig);
|
|
30
74
|
this.loader = loader;
|
|
31
|
-
if (
|
|
75
|
+
if (splash)
|
|
76
|
+
this.logger.info('Welcome to use Yumeri ver.' + version);
|
|
77
|
+
if (!this.coreConfig.skipcheckUpdates)
|
|
78
|
+
this.checkUpdate();
|
|
79
|
+
if (loggersetCore)
|
|
32
80
|
Logger.setCore(this);
|
|
33
81
|
}
|
|
82
|
+
async checkUpdate() {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch('https://registry.npmjs.org/yumeri/latest');
|
|
85
|
+
if (!response.ok)
|
|
86
|
+
return;
|
|
87
|
+
const { version: latest } = await response.json();
|
|
88
|
+
if (latest !== version) {
|
|
89
|
+
this.logger.info(`There is new version for Yumeri: ${version} -> ${latest}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.logger.error('Error while checking updates: ', error.message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
34
98
|
addRenderer(renderer) {
|
|
35
99
|
if (this.renderers.has(renderer.name)) {
|
|
36
100
|
this.logger.warn(`Renderer "${renderer.name}" is already registered and will be overwritten.`);
|
|
@@ -46,7 +110,6 @@ export class Core {
|
|
|
46
110
|
host: this.coreConfig.host || '0.0.0.0',
|
|
47
111
|
enableCors: this.coreConfig.enableCors || false,
|
|
48
112
|
enableWs: this.coreConfig.enableWs || false,
|
|
49
|
-
staticDir: this.coreConfig.staticDir || 'public',
|
|
50
113
|
});
|
|
51
114
|
await this.server.start();
|
|
52
115
|
}
|
|
@@ -60,12 +123,21 @@ export class Core {
|
|
|
60
123
|
}
|
|
61
124
|
return pluginName;
|
|
62
125
|
}
|
|
63
|
-
async plugin(
|
|
126
|
+
async plugin(module, context, config) {
|
|
127
|
+
const plugin = resolvePluginModule(module, context, config);
|
|
64
128
|
const shortName = this.getShortPluginName(context.pluginname);
|
|
65
|
-
context.
|
|
129
|
+
context.module = plugin;
|
|
130
|
+
// 自动依赖注入
|
|
131
|
+
const depend = plugin.depend || [];
|
|
132
|
+
for (const name of depend) {
|
|
133
|
+
const component = this.getComponent(name);
|
|
134
|
+
if (component) {
|
|
135
|
+
context.inject(name, component);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
66
138
|
this.logger.info(`apply plugin ${shortName}`);
|
|
67
|
-
if (
|
|
68
|
-
await
|
|
139
|
+
if (plugin.apply) {
|
|
140
|
+
await plugin.apply(context, config);
|
|
69
141
|
}
|
|
70
142
|
}
|
|
71
143
|
registerComponent(name, component) {
|
|
@@ -123,7 +195,7 @@ export class Core {
|
|
|
123
195
|
async executeRoute(pathname, session, queryParams) {
|
|
124
196
|
for (const routePath in this.routes) {
|
|
125
197
|
const route = this.routes[routePath];
|
|
126
|
-
const result = route.match(pathname);
|
|
198
|
+
const result = route.match(pathname, session.client?.headers?.host);
|
|
127
199
|
if (result) {
|
|
128
200
|
try {
|
|
129
201
|
this.emit('request:start', {
|
|
@@ -151,7 +223,7 @@ export class Core {
|
|
|
151
223
|
}
|
|
152
224
|
else {
|
|
153
225
|
const start = Date.now();
|
|
154
|
-
await route.executeHandler(session, queryParams, result.pathParams);
|
|
226
|
+
await route.executeHandler(session, queryParams, result.pathParams, result.hostParams);
|
|
155
227
|
this.emit('route:end', {
|
|
156
228
|
path: pathname,
|
|
157
229
|
route: routePath,
|
package/dist/route.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare class Route {
|
|
|
25
25
|
allowedMethods: string[];
|
|
26
26
|
ws: WebSocketServer;
|
|
27
27
|
context: Context;
|
|
28
|
+
private routehost;
|
|
28
29
|
/**
|
|
29
30
|
* 创建路由
|
|
30
31
|
* @param path 路由路径
|
|
@@ -37,6 +38,12 @@ export declare class Route {
|
|
|
37
38
|
* @returns this
|
|
38
39
|
*/
|
|
39
40
|
action(handler: RouteHandler): this;
|
|
41
|
+
/**
|
|
42
|
+
* 设置允许的host
|
|
43
|
+
* @param host host列表
|
|
44
|
+
* @returns this
|
|
45
|
+
*/
|
|
46
|
+
host(host: string[] | string): this;
|
|
40
47
|
/**
|
|
41
48
|
* 挂载中间件
|
|
42
49
|
* @param middleware 中间件
|
|
@@ -46,11 +53,13 @@ export declare class Route {
|
|
|
46
53
|
/**
|
|
47
54
|
* 匹配路由
|
|
48
55
|
* @param pathname 请求路径
|
|
56
|
+
* @param host 请求host
|
|
49
57
|
* @returns 匹配结果
|
|
50
58
|
*/
|
|
51
|
-
match(pathname: string): {
|
|
59
|
+
match(pathname: string, host?: string): {
|
|
52
60
|
params: Record<string, string | undefined>;
|
|
53
61
|
pathParams: string[];
|
|
62
|
+
hostParams: Record<string, string>;
|
|
54
63
|
} | null;
|
|
55
64
|
/**
|
|
56
65
|
* 执行路由处理器
|
|
@@ -58,7 +67,7 @@ export declare class Route {
|
|
|
58
67
|
* @param params 查询参数
|
|
59
68
|
* @param pathParams 路由参数
|
|
60
69
|
*/
|
|
61
|
-
executeHandler(session: Session, params: URLSearchParams, pathParams: string[]): Promise<void>;
|
|
70
|
+
executeHandler(session: Session, params: URLSearchParams, pathParams: string[], hostParams: Record<string, string>): Promise<void>;
|
|
62
71
|
/**
|
|
63
72
|
* 设置可用方法
|
|
64
73
|
* @param methods 可用方法
|
package/dist/route.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
2
3
|
function parsePatternToSegments(pattern) {
|
|
3
4
|
// Normalize: remove leading/trailing slashes for consistent splitting
|
|
4
5
|
const norm = pattern.replace(/^\/+|\/+$/g, '');
|
|
@@ -22,6 +23,196 @@ function parsePatternToSegments(pattern) {
|
|
|
22
23
|
}
|
|
23
24
|
return { segments, params };
|
|
24
25
|
}
|
|
26
|
+
function parseSegmentToken(part) {
|
|
27
|
+
if (!part.startsWith(':')) {
|
|
28
|
+
return { type: 'static', value: part };
|
|
29
|
+
}
|
|
30
|
+
let name = part.slice(1);
|
|
31
|
+
let modifier = '';
|
|
32
|
+
const lastChar = name[name.length - 1];
|
|
33
|
+
if (lastChar === '?' || lastChar === '*' || lastChar === '+') {
|
|
34
|
+
modifier = lastChar;
|
|
35
|
+
name = name.slice(0, -1);
|
|
36
|
+
}
|
|
37
|
+
return { type: 'param', name, modifier };
|
|
38
|
+
}
|
|
39
|
+
function splitHostAndPortPattern(pattern) {
|
|
40
|
+
const normalized = pattern.trim().toLowerCase();
|
|
41
|
+
if (normalized.startsWith('[')) {
|
|
42
|
+
const end = normalized.indexOf(']');
|
|
43
|
+
if (end === -1)
|
|
44
|
+
return { hostPart: normalized };
|
|
45
|
+
const hostPart = normalized.slice(1, end);
|
|
46
|
+
const rest = normalized.slice(end + 1);
|
|
47
|
+
if (rest.startsWith(':')) {
|
|
48
|
+
return { hostPart, portPart: rest.slice(1) };
|
|
49
|
+
}
|
|
50
|
+
return { hostPart };
|
|
51
|
+
}
|
|
52
|
+
const colonCount = (normalized.match(/:/g) || []).length;
|
|
53
|
+
const lastColon = normalized.lastIndexOf(':');
|
|
54
|
+
const hasDots = normalized.includes('.');
|
|
55
|
+
if (lastColon === -1) {
|
|
56
|
+
return { hostPart: normalized };
|
|
57
|
+
}
|
|
58
|
+
const suffix = normalized.slice(lastColon + 1);
|
|
59
|
+
const prefix = normalized.slice(0, lastColon);
|
|
60
|
+
const isPortToken = /^\d+$/.test(suffix) || /^:[a-z_][a-z0-9_]*[?*+]?$/.test(`:${suffix}`);
|
|
61
|
+
if (!isPortToken) {
|
|
62
|
+
return { hostPart: normalized };
|
|
63
|
+
}
|
|
64
|
+
if (normalized.startsWith(':') && !hasDots) {
|
|
65
|
+
const paramBody = prefix.slice(1);
|
|
66
|
+
if (/^[a-z_][a-z0-9_]*$/.test(paramBody)) {
|
|
67
|
+
return { hostPart: prefix, portPart: suffix };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (colonCount === 1 && !normalized.startsWith(':')) {
|
|
71
|
+
return { hostPart: prefix, portPart: suffix };
|
|
72
|
+
}
|
|
73
|
+
if (hasDots) {
|
|
74
|
+
return { hostPart: prefix, portPart: suffix };
|
|
75
|
+
}
|
|
76
|
+
return { hostPart: normalized };
|
|
77
|
+
}
|
|
78
|
+
function parseHostPattern(hostPattern) {
|
|
79
|
+
const { hostPart, portPart } = splitHostAndPortPattern(hostPattern);
|
|
80
|
+
const matchWholeHost = !hostPart.includes('.') || hostPart.includes(':');
|
|
81
|
+
const rawParts = matchWholeHost ? [hostPart] : hostPart.split('.');
|
|
82
|
+
return {
|
|
83
|
+
hostSegments: rawParts.map(parseSegmentToken),
|
|
84
|
+
portSegment: portPart ? parseSegmentToken(portPart) : undefined,
|
|
85
|
+
matchWholeHost,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseAuthority(host) {
|
|
89
|
+
const normalized = host.trim().toLowerCase();
|
|
90
|
+
if (normalized.startsWith('[')) {
|
|
91
|
+
const end = normalized.indexOf(']');
|
|
92
|
+
if (end !== -1) {
|
|
93
|
+
const hostname = normalized.slice(1, end);
|
|
94
|
+
const rest = normalized.slice(end + 1);
|
|
95
|
+
const port = rest.startsWith(':') ? rest.slice(1) : undefined;
|
|
96
|
+
return { hostname, port, kind: 'ipv6' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (isIP(normalized) === 6) {
|
|
100
|
+
return { hostname: normalized, kind: 'ipv6' };
|
|
101
|
+
}
|
|
102
|
+
const lastColon = normalized.lastIndexOf(':');
|
|
103
|
+
if (lastColon !== -1 && normalized.indexOf(':') === lastColon) {
|
|
104
|
+
const maybePort = normalized.slice(lastColon + 1);
|
|
105
|
+
if (/^\d+$/.test(maybePort)) {
|
|
106
|
+
const hostname = normalized.slice(0, lastColon);
|
|
107
|
+
if (isIP(hostname) === 4) {
|
|
108
|
+
return { hostname, port: maybePort, kind: 'ipv4' };
|
|
109
|
+
}
|
|
110
|
+
return { hostname, port: maybePort, kind: 'domain' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (isIP(normalized) === 4) {
|
|
114
|
+
return { hostname: normalized, kind: 'ipv4' };
|
|
115
|
+
}
|
|
116
|
+
return { hostname: normalized, kind: 'domain' };
|
|
117
|
+
}
|
|
118
|
+
function matchSingleSegment(seg, value, params) {
|
|
119
|
+
if (seg.type === 'static') {
|
|
120
|
+
return value === seg.value;
|
|
121
|
+
}
|
|
122
|
+
switch (seg.modifier) {
|
|
123
|
+
case '':
|
|
124
|
+
case '+':
|
|
125
|
+
case '*':
|
|
126
|
+
if (value == null || value === '') {
|
|
127
|
+
if (seg.modifier === '*') {
|
|
128
|
+
params[seg.name] = undefined;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
params[seg.name] = value;
|
|
134
|
+
return true;
|
|
135
|
+
case '?':
|
|
136
|
+
params[seg.name] = value || undefined;
|
|
137
|
+
return true;
|
|
138
|
+
default:
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function matchHostSegments(segments, parts, joiner, params) {
|
|
143
|
+
let hi = 0;
|
|
144
|
+
let hj = 0;
|
|
145
|
+
while (hi < segments.length) {
|
|
146
|
+
const seg = segments[hi];
|
|
147
|
+
const nextSegIsStatic = !!segments[hi + 1] && segments[hi + 1].type === 'static';
|
|
148
|
+
if (seg.type === 'static') {
|
|
149
|
+
if (hj >= parts.length || parts[hj] !== seg.value)
|
|
150
|
+
return false;
|
|
151
|
+
hi++;
|
|
152
|
+
hj++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
switch (seg.modifier) {
|
|
156
|
+
case '':
|
|
157
|
+
if (hj >= parts.length)
|
|
158
|
+
return false;
|
|
159
|
+
params[seg.name] = parts[hj];
|
|
160
|
+
hi++;
|
|
161
|
+
hj++;
|
|
162
|
+
break;
|
|
163
|
+
case '?':
|
|
164
|
+
if (hj < parts.length) {
|
|
165
|
+
if (nextSegIsStatic && parts[hj] === segments[hi + 1].value) {
|
|
166
|
+
params[seg.name] = undefined;
|
|
167
|
+
hi++;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
params[seg.name] = parts[hj];
|
|
171
|
+
hi++;
|
|
172
|
+
hj++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
params[seg.name] = undefined;
|
|
177
|
+
hi++;
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
case '+': {
|
|
181
|
+
if (hj >= parts.length)
|
|
182
|
+
return false;
|
|
183
|
+
let end = parts.length;
|
|
184
|
+
if (nextSegIsStatic) {
|
|
185
|
+
const nextStatic = segments[hi + 1].value;
|
|
186
|
+
const found = parts.indexOf(nextStatic, hj);
|
|
187
|
+
if (found === -1 || found === hj)
|
|
188
|
+
return false;
|
|
189
|
+
end = found;
|
|
190
|
+
}
|
|
191
|
+
params[seg.name] = parts.slice(hj, end).join(joiner);
|
|
192
|
+
hj = end;
|
|
193
|
+
hi++;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case '*': {
|
|
197
|
+
let end = parts.length;
|
|
198
|
+
if (nextSegIsStatic) {
|
|
199
|
+
const nextStatic = segments[hi + 1].value;
|
|
200
|
+
const found = parts.indexOf(nextStatic, hj);
|
|
201
|
+
if (found !== -1)
|
|
202
|
+
end = found;
|
|
203
|
+
}
|
|
204
|
+
const value = parts.slice(hj, end).join(joiner);
|
|
205
|
+
params[seg.name] = value || undefined;
|
|
206
|
+
hj = end;
|
|
207
|
+
hi++;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
default:
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return hj === parts.length;
|
|
215
|
+
}
|
|
25
216
|
/**
|
|
26
217
|
* Route class using segment-based matching (no fragile capture-group-index mapping).
|
|
27
218
|
* Behavior matches the SimpleRouter in the webpage:
|
|
@@ -39,6 +230,7 @@ export class Route {
|
|
|
39
230
|
allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'];
|
|
40
231
|
ws = null;
|
|
41
232
|
context;
|
|
233
|
+
routehost = null;
|
|
42
234
|
/**
|
|
43
235
|
* 创建路由
|
|
44
236
|
* @param path 路由路径
|
|
@@ -60,6 +252,20 @@ export class Route {
|
|
|
60
252
|
this.handler = handler;
|
|
61
253
|
return this;
|
|
62
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* 设置允许的host
|
|
257
|
+
* @param host host列表
|
|
258
|
+
* @returns this
|
|
259
|
+
*/
|
|
260
|
+
host(host) {
|
|
261
|
+
if (typeof host === 'string') {
|
|
262
|
+
this.routehost = [host];
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
this.routehost = host;
|
|
266
|
+
}
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
63
269
|
/**
|
|
64
270
|
* 挂载中间件
|
|
65
271
|
* @param middleware 中间件
|
|
@@ -72,9 +278,41 @@ export class Route {
|
|
|
72
278
|
/**
|
|
73
279
|
* 匹配路由
|
|
74
280
|
* @param pathname 请求路径
|
|
281
|
+
* @param host 请求host
|
|
75
282
|
* @returns 匹配结果
|
|
76
283
|
*/
|
|
77
|
-
match(pathname) {
|
|
284
|
+
match(pathname, host) {
|
|
285
|
+
let hostParams = {};
|
|
286
|
+
// Host 匹配逻辑开始
|
|
287
|
+
if (this.routehost && this.routehost.length > 0) {
|
|
288
|
+
if (!host)
|
|
289
|
+
return null; // 如果设置了 host 限制但没有传入 host,直接失败
|
|
290
|
+
const actual = parseAuthority(host);
|
|
291
|
+
let hostMatched = false;
|
|
292
|
+
for (const pattern of this.routehost) {
|
|
293
|
+
const parsedPattern = parseHostPattern(pattern);
|
|
294
|
+
const hostParts = parsedPattern.matchWholeHost || actual.kind === 'ipv6'
|
|
295
|
+
? [actual.hostname]
|
|
296
|
+
: actual.hostname.split('.');
|
|
297
|
+
const joiner = parsedPattern.matchWholeHost || actual.kind === 'ipv6' ? ':' : '.';
|
|
298
|
+
let tempParams = {};
|
|
299
|
+
const hostOk = matchHostSegments(parsedPattern.hostSegments, hostParts, joiner, tempParams);
|
|
300
|
+
if (!hostOk)
|
|
301
|
+
continue;
|
|
302
|
+
if (parsedPattern.portSegment) {
|
|
303
|
+
const portOk = matchSingleSegment(parsedPattern.portSegment, actual.port, tempParams);
|
|
304
|
+
if (!portOk)
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
hostMatched = true;
|
|
308
|
+
hostParams = tempParams;
|
|
309
|
+
break; // 只要匹配到一个 host pattern 就行
|
|
310
|
+
}
|
|
311
|
+
if (!hostMatched)
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
// Host 匹配逻辑结束
|
|
315
|
+
// 路由匹配逻辑开始
|
|
78
316
|
if (pathname.startsWith('/')) {
|
|
79
317
|
// normalize pathname: remove leading/trailing slashes
|
|
80
318
|
const normPath = pathname.replace(/^\/+|\/+$/g, '');
|
|
@@ -198,10 +436,12 @@ export class Route {
|
|
|
198
436
|
// after finishing pattern, path must be fully consumed (no extra segments)
|
|
199
437
|
if (j !== parts.length)
|
|
200
438
|
return null;
|
|
201
|
-
|
|
439
|
+
const result = { params, pathParams, hostParams };
|
|
440
|
+
return result;
|
|
202
441
|
}
|
|
203
442
|
else if (pathname.startsWith('root') && this.path == pathname) {
|
|
204
|
-
|
|
443
|
+
const result = { params: { pathname }, pathParams: [pathname], hostParams };
|
|
444
|
+
return result;
|
|
205
445
|
}
|
|
206
446
|
return null;
|
|
207
447
|
}
|
|
@@ -211,9 +451,9 @@ export class Route {
|
|
|
211
451
|
* @param params 查询参数
|
|
212
452
|
* @param pathParams 路由参数
|
|
213
453
|
*/
|
|
214
|
-
async executeHandler(session, params, pathParams) {
|
|
454
|
+
async executeHandler(session, params, pathParams, hostParams) {
|
|
215
455
|
if (this.handler) {
|
|
216
|
-
await this.handler(session, params, ...pathParams);
|
|
456
|
+
await this.handler(session, params, ...pathParams, ...Object.values(hostParams));
|
|
217
457
|
}
|
|
218
458
|
}
|
|
219
459
|
/**
|