@yumerijs/core 2.0.13 → 2.1.0
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 -1
- package/dist/context.js +17 -7
- package/dist/core.d.ts +10 -5
- package/dist/core.js +76 -20
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +3 -2
- package/dist/route.d.ts +4 -1
- package/dist/route.js +4 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +71 -72
- package/dist/session.d.ts +49 -2
- package/dist/session.js +105 -4
- package/package.json +1 -1
package/dist/context.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ interface Plugin {
|
|
|
9
9
|
depend: Array<string>;
|
|
10
10
|
provide: Array<string>;
|
|
11
11
|
}
|
|
12
|
+
export interface Components {
|
|
13
|
+
}
|
|
12
14
|
/**
|
|
13
15
|
* 插件上下文对象
|
|
14
16
|
* 每个插件一个 Context,用于管理插件注册的命令、路由、事件、组件和中间件
|
|
@@ -23,6 +25,8 @@ export declare class Context {
|
|
|
23
25
|
private childContexts;
|
|
24
26
|
private childPlugins;
|
|
25
27
|
private i18ns;
|
|
28
|
+
component: Components;
|
|
29
|
+
instance: any;
|
|
26
30
|
/** 插件名称 */
|
|
27
31
|
pluginname: string;
|
|
28
32
|
/**
|
|
@@ -30,7 +34,13 @@ export declare class Context {
|
|
|
30
34
|
* @param core Core 实例
|
|
31
35
|
* @param pluginname 插件名称
|
|
32
36
|
*/
|
|
33
|
-
constructor(core: Core, pluginname: string);
|
|
37
|
+
constructor(core: Core, pluginname: string, instance?: any, injections?: Record<string, any>);
|
|
38
|
+
/**
|
|
39
|
+
* 注入依赖
|
|
40
|
+
* @param name 依赖名称
|
|
41
|
+
* @param value 依赖值
|
|
42
|
+
*/
|
|
43
|
+
inject(name: string, value: any): void;
|
|
34
44
|
/**
|
|
35
45
|
* 注册路由
|
|
36
46
|
* @param path 路由路径
|
|
@@ -72,6 +82,7 @@ export declare class Context {
|
|
|
72
82
|
emit(event: string, ...args: any[]): Promise<void>;
|
|
73
83
|
/**
|
|
74
84
|
* 获取组件实例
|
|
85
|
+
* @deprecated
|
|
75
86
|
* @param name 组件名称
|
|
76
87
|
*/
|
|
77
88
|
getComponent(name: string): any;
|
package/dist/context.js
CHANGED
|
@@ -16,6 +16,8 @@ class Context {
|
|
|
16
16
|
childContexts = [];
|
|
17
17
|
childPlugins = new Map();
|
|
18
18
|
i18ns = [];
|
|
19
|
+
component;
|
|
20
|
+
instance;
|
|
19
21
|
/** 插件名称 */
|
|
20
22
|
pluginname;
|
|
21
23
|
/**
|
|
@@ -23,10 +25,20 @@ class Context {
|
|
|
23
25
|
* @param core Core 实例
|
|
24
26
|
* @param pluginname 插件名称
|
|
25
27
|
*/
|
|
26
|
-
constructor(core, pluginname) {
|
|
28
|
+
constructor(core, pluginname, instance, injections = {}) {
|
|
27
29
|
this.core = core;
|
|
30
|
+
this.instance = instance;
|
|
28
31
|
this.pluginname = pluginname;
|
|
29
32
|
this.childPlugins = new Map();
|
|
33
|
+
this.component = injections;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 注入依赖
|
|
37
|
+
* @param name 依赖名称
|
|
38
|
+
* @param value 依赖值
|
|
39
|
+
*/
|
|
40
|
+
inject(name, value) {
|
|
41
|
+
this.component[name] = value;
|
|
30
42
|
}
|
|
31
43
|
/**
|
|
32
44
|
* 注册路由
|
|
@@ -36,10 +48,10 @@ class Context {
|
|
|
36
48
|
route(path) {
|
|
37
49
|
if (this.core.routes[path]) {
|
|
38
50
|
this.core.logger.warn(`Plugin "${this.pluginname}" attempt to register route "${path}", but it has already been registered.`);
|
|
39
|
-
return new route_1.Route(path);
|
|
51
|
+
return new route_1.Route(path, this);
|
|
40
52
|
}
|
|
41
53
|
this.routes.push(path);
|
|
42
|
-
return this.core.route(path);
|
|
54
|
+
return this.core.route(path, this);
|
|
43
55
|
}
|
|
44
56
|
/**
|
|
45
57
|
* 注册事件
|
|
@@ -99,6 +111,7 @@ class Context {
|
|
|
99
111
|
}
|
|
100
112
|
/**
|
|
101
113
|
* 获取组件实例
|
|
114
|
+
* @deprecated
|
|
102
115
|
* @param name 组件名称
|
|
103
116
|
*/
|
|
104
117
|
getComponent(name) {
|
|
@@ -184,10 +197,7 @@ class Context {
|
|
|
184
197
|
this.middlewares.forEach((middleware) => delete this.core.globalMiddlewares[middleware]);
|
|
185
198
|
// 删除事件监听器
|
|
186
199
|
this.eventlisteners.forEach(({ name, listener }) => {
|
|
187
|
-
|
|
188
|
-
if (listeners) {
|
|
189
|
-
this.core.eventListeners[name] = listeners.filter(l => l !== listener);
|
|
190
|
-
}
|
|
200
|
+
this.core.off(name, listener);
|
|
191
201
|
});
|
|
192
202
|
// 删除钩子
|
|
193
203
|
for (const hook in this.hooks) {
|
package/dist/core.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
1
2
|
import { Config } from './config';
|
|
2
3
|
import { Logger } from './logger';
|
|
3
4
|
import { Session } from './session';
|
|
@@ -7,6 +8,7 @@ import { Context } from './context';
|
|
|
7
8
|
import { HookHandler, Hook } from './hook';
|
|
8
9
|
import { Server as CoreServer } from './server';
|
|
9
10
|
import { I18n } from './i18n';
|
|
11
|
+
import { IRenderer } from '@yumerijs/types';
|
|
10
12
|
interface Plugin {
|
|
11
13
|
apply: (ctx: Context, config: Config) => Promise<void>;
|
|
12
14
|
disable: (ctx: Context) => Promise<void>;
|
|
@@ -27,9 +29,7 @@ export declare const enum PluginStatus {
|
|
|
27
29
|
PENDING = "pending"
|
|
28
30
|
}
|
|
29
31
|
export declare class Core {
|
|
30
|
-
|
|
31
|
-
[event: string]: ((...args: any[]) => Promise<void>)[];
|
|
32
|
-
};
|
|
32
|
+
emitter: EventEmitter<[never]>;
|
|
33
33
|
components: {
|
|
34
34
|
[name: string]: any;
|
|
35
35
|
};
|
|
@@ -41,7 +41,11 @@ export declare class Core {
|
|
|
41
41
|
server: CoreServer;
|
|
42
42
|
i18n: I18n;
|
|
43
43
|
loader: any;
|
|
44
|
+
renderers: Map<string, IRenderer>;
|
|
45
|
+
pluginRenderers: Map<string, string>;
|
|
44
46
|
constructor(loader?: any, coreConfig?: CoreOptions, setCore?: boolean);
|
|
47
|
+
addRenderer(renderer: IRenderer): void;
|
|
48
|
+
getRendererForPlugin(pluginName: string): string | undefined;
|
|
45
49
|
runCore(): Promise<void>;
|
|
46
50
|
getShortPluginName(pluginName: string): string;
|
|
47
51
|
plugin(pluginInstance: Plugin, context: Context, config: Config): Promise<void>;
|
|
@@ -49,9 +53,10 @@ export declare class Core {
|
|
|
49
53
|
getComponent(name: string): any;
|
|
50
54
|
unregisterComponent(name: string): void;
|
|
51
55
|
on(event: string, listener: (...args: any[]) => Promise<void>): void;
|
|
52
|
-
emit(event: string, ...
|
|
56
|
+
emit(event: string, ...payload: any): void;
|
|
57
|
+
off(event: string, listener: (...args: any[]) => Promise<void>): void;
|
|
53
58
|
use(name: string, middleware: Middleware): Core;
|
|
54
|
-
route(path: string): Route;
|
|
59
|
+
route(path: string, context: Context): Route;
|
|
55
60
|
hook(name: string, hookname: string, callback: HookHandler): any;
|
|
56
61
|
unhook(name: string, hookname: string): any;
|
|
57
62
|
hookExecute(name: string, ...args: any[]): Promise<any[]>;
|
package/dist/core.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Core = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
4
5
|
const logger_1 = require("./logger");
|
|
5
6
|
const route_1 = require("./route");
|
|
6
7
|
const hook_1 = require("./hook");
|
|
7
8
|
const server_1 = require("./server");
|
|
8
9
|
class Core {
|
|
9
|
-
|
|
10
|
+
emitter = new events_1.EventEmitter();
|
|
10
11
|
components = {};
|
|
11
12
|
routes = {};
|
|
12
13
|
logger = new logger_1.Logger('core');
|
|
@@ -16,12 +17,23 @@ class Core {
|
|
|
16
17
|
server;
|
|
17
18
|
i18n;
|
|
18
19
|
loader;
|
|
20
|
+
renderers = new Map();
|
|
21
|
+
pluginRenderers = new Map(); // Stores which plugin uses which renderer
|
|
19
22
|
constructor(loader, coreConfig, setCore = true) {
|
|
20
23
|
this.coreConfig = coreConfig || {};
|
|
21
24
|
this.loader = loader;
|
|
22
25
|
if (setCore)
|
|
23
26
|
logger_1.Logger.setCore(this);
|
|
24
27
|
}
|
|
28
|
+
addRenderer(renderer) {
|
|
29
|
+
if (this.renderers.has(renderer.name)) {
|
|
30
|
+
this.logger.warn(`Renderer "${renderer.name}" is already registered and will be overwritten.`);
|
|
31
|
+
}
|
|
32
|
+
this.renderers.set(renderer.name, renderer);
|
|
33
|
+
}
|
|
34
|
+
getRendererForPlugin(pluginName) {
|
|
35
|
+
return this.pluginRenderers.get(pluginName);
|
|
36
|
+
}
|
|
25
37
|
async runCore() {
|
|
26
38
|
this.server = new server_1.Server(this, {
|
|
27
39
|
port: this.coreConfig.port || 14510,
|
|
@@ -44,6 +56,7 @@ class Core {
|
|
|
44
56
|
}
|
|
45
57
|
async plugin(pluginInstance, context, config) {
|
|
46
58
|
const shortName = this.getShortPluginName(context.pluginname);
|
|
59
|
+
context.instance = pluginInstance;
|
|
47
60
|
this.logger.info(`apply plugin ${shortName}`);
|
|
48
61
|
if (pluginInstance.apply) {
|
|
49
62
|
await pluginInstance.apply(context, config);
|
|
@@ -59,29 +72,28 @@ class Core {
|
|
|
59
72
|
delete this.components[name];
|
|
60
73
|
}
|
|
61
74
|
on(event, listener) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
this.emitter.on(event, (...args) => {
|
|
76
|
+
// 包一层保证 async 可以被捕获
|
|
77
|
+
Promise.resolve(listener(...args)).catch((err) => {
|
|
78
|
+
console.error(`Error in event listener for "${event}":`, err);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
82
|
+
emit(event, ...payload) {
|
|
83
|
+
this.emitter.emit(event, ...payload);
|
|
84
|
+
}
|
|
85
|
+
// 删除监听器
|
|
86
|
+
off(event, listener) {
|
|
87
|
+
// 原生 EventEmitter 必须删“同一个函数引用”
|
|
88
|
+
// 所以必须包装一致,这里我们直接用 listener 本体删
|
|
89
|
+
this.emitter.off(event, listener);
|
|
78
90
|
}
|
|
79
91
|
use(name, middleware) {
|
|
80
92
|
this.globalMiddlewares[name] = middleware;
|
|
81
93
|
return this;
|
|
82
94
|
}
|
|
83
|
-
route(path) {
|
|
84
|
-
const route = new route_1.Route(path);
|
|
95
|
+
route(path, context) {
|
|
96
|
+
const route = new route_1.Route(path, context);
|
|
85
97
|
this.routes[path] = route;
|
|
86
98
|
return route;
|
|
87
99
|
}
|
|
@@ -108,18 +120,62 @@ class Core {
|
|
|
108
120
|
const result = route.match(pathname);
|
|
109
121
|
if (result) {
|
|
110
122
|
try {
|
|
123
|
+
this.emit('request:start', {
|
|
124
|
+
path: pathname,
|
|
125
|
+
route: routePath,
|
|
126
|
+
method: session?.client?.req?.method,
|
|
127
|
+
plugin: route.context?.pluginname,
|
|
128
|
+
start: Date.now(),
|
|
129
|
+
sessionId: session?.sessionid,
|
|
130
|
+
});
|
|
111
131
|
const middlewares = [...Object.values(this.globalMiddlewares), ...(route.middlewares || [])];
|
|
112
132
|
let index = 0;
|
|
113
133
|
const runner = async () => {
|
|
114
|
-
if (index < middlewares.length)
|
|
134
|
+
if (index < middlewares.length) {
|
|
135
|
+
const name = Object.keys(this.globalMiddlewares)[index] || `mw-${index}`;
|
|
136
|
+
const start = Date.now();
|
|
115
137
|
await middlewares[index++](session, runner);
|
|
116
|
-
|
|
138
|
+
this.emit('middleware:end', {
|
|
139
|
+
name,
|
|
140
|
+
path: pathname,
|
|
141
|
+
plugin: route.context?.pluginname,
|
|
142
|
+
duration: Date.now() - start,
|
|
143
|
+
sessionId: session?.sessionid,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const start = Date.now();
|
|
117
148
|
await route.executeHandler(session, queryParams, result.pathParams);
|
|
149
|
+
this.emit('route:end', {
|
|
150
|
+
path: pathname,
|
|
151
|
+
route: routePath,
|
|
152
|
+
plugin: route.context?.pluginname,
|
|
153
|
+
duration: Date.now() - start,
|
|
154
|
+
sessionId: session?.sessionid,
|
|
155
|
+
status: session?.status,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
118
158
|
};
|
|
119
159
|
await runner();
|
|
160
|
+
this.emit('request:end', {
|
|
161
|
+
path: pathname,
|
|
162
|
+
route: routePath,
|
|
163
|
+
method: session?.client?.req?.method,
|
|
164
|
+
plugin: route.context?.pluginname,
|
|
165
|
+
duration: Date.now() - session._startAt || undefined,
|
|
166
|
+
status: session?.status,
|
|
167
|
+
sessionId: session?.sessionid,
|
|
168
|
+
});
|
|
120
169
|
}
|
|
121
170
|
catch (error) {
|
|
122
171
|
this.logger.error(`Unhandled error in route execution for path "${pathname}":`, error);
|
|
172
|
+
this.emit('request:error', {
|
|
173
|
+
path: pathname,
|
|
174
|
+
route: routePath,
|
|
175
|
+
plugin: route.context?.pluginname,
|
|
176
|
+
error: String(error),
|
|
177
|
+
sessionId: session?.sessionid,
|
|
178
|
+
});
|
|
123
179
|
if (session && session.client.res && !session.client.res.writableEnded) {
|
|
124
180
|
session.client.res.statusCode = 500;
|
|
125
181
|
session.client.res.end('Internal Server Error');
|
package/dist/i18n.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class I18n {
|
|
|
23
23
|
* 替换模板字符串中的文本点
|
|
24
24
|
* e.g. "Hello {{app.title}}" -> "Hello 世界"
|
|
25
25
|
*/
|
|
26
|
-
replaceAll(input: string, langs?: string[]): string;
|
|
26
|
+
replaceAll(input: string, langs?: string[], customRegex?: RegExp): string;
|
|
27
27
|
all(): I18nData;
|
|
28
28
|
}
|
|
29
29
|
export {};
|
package/dist/i18n.js
CHANGED
|
@@ -69,8 +69,9 @@ class I18n {
|
|
|
69
69
|
* 替换模板字符串中的文本点
|
|
70
70
|
* e.g. "Hello {{app.title}}" -> "Hello 世界"
|
|
71
71
|
*/
|
|
72
|
-
replaceAll(input, langs) {
|
|
73
|
-
|
|
72
|
+
replaceAll(input, langs, customRegex) {
|
|
73
|
+
const regex = customRegex || /\{\{\s*([\w.]+)\s*\}\}/g;
|
|
74
|
+
return input.replace(regex, (_, key) => this.get(key.trim(), langs));
|
|
74
75
|
}
|
|
75
76
|
all() {
|
|
76
77
|
return this.data;
|
package/dist/route.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Session } from './session';
|
|
7
7
|
import { Middleware } from './middleware';
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { Context } from './context';
|
|
9
10
|
export type RouteHandler = (session: Session, queryParams: URLSearchParams, ...pathParams: string[]) => Promise<void> | void;
|
|
10
11
|
/**
|
|
11
12
|
* Route class using segment-based matching (no fragile capture-group-index mapping).
|
|
@@ -23,11 +24,13 @@ export declare class Route {
|
|
|
23
24
|
middlewares: Middleware[];
|
|
24
25
|
allowedMethods: string[];
|
|
25
26
|
ws: WebSocketServer;
|
|
27
|
+
context: Context;
|
|
26
28
|
/**
|
|
27
29
|
* 创建路由
|
|
28
30
|
* @param path 路由路径
|
|
31
|
+
* @param context 插件上下文
|
|
29
32
|
*/
|
|
30
|
-
constructor(path: string);
|
|
33
|
+
constructor(path: string, context: Context);
|
|
31
34
|
/**
|
|
32
35
|
* 设置路由处理器
|
|
33
36
|
* @param handler 路由处理器
|
package/dist/route.js
CHANGED
|
@@ -41,12 +41,15 @@ class Route {
|
|
|
41
41
|
middlewares = [];
|
|
42
42
|
allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'];
|
|
43
43
|
ws = null;
|
|
44
|
+
context;
|
|
44
45
|
/**
|
|
45
46
|
* 创建路由
|
|
46
47
|
* @param path 路由路径
|
|
48
|
+
* @param context 插件上下文
|
|
47
49
|
*/
|
|
48
|
-
constructor(path) {
|
|
50
|
+
constructor(path, context) {
|
|
49
51
|
this.path = path;
|
|
52
|
+
this.context = context;
|
|
50
53
|
const { segments, params } = parsePatternToSegments(path);
|
|
51
54
|
this.segments = segments;
|
|
52
55
|
this.paramsInfo = params;
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -44,7 +44,11 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const url_1 = require("url");
|
|
46
46
|
const mime = __importStar(require("mime-types"));
|
|
47
|
+
const types_1 = require("@yumerijs/types");
|
|
47
48
|
const logger = new logger_1.Logger('server');
|
|
49
|
+
function isStream(value) {
|
|
50
|
+
return value && typeof value.pipe === "function";
|
|
51
|
+
}
|
|
48
52
|
class Server {
|
|
49
53
|
core;
|
|
50
54
|
port;
|
|
@@ -59,8 +63,11 @@ class Server {
|
|
|
59
63
|
this.enableCors = config.enableCors ?? true;
|
|
60
64
|
this.staticDir = config.staticDir ?? 'static';
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
getStaticDir() {
|
|
67
|
+
return this.staticDir;
|
|
68
|
+
}
|
|
69
|
+
createSession(ip, cookies, res, req, pathname, pluginContext, extra = {}) {
|
|
70
|
+
const session = new session_1.Session(ip, cookies, this, req, res, pathname, undefined, pluginContext);
|
|
64
71
|
Object.assign(session.properties, extra);
|
|
65
72
|
session.protocol = extra.protocol ?? 'http';
|
|
66
73
|
return session;
|
|
@@ -109,84 +116,75 @@ class Server {
|
|
|
109
116
|
const queryParams = url.searchParams;
|
|
110
117
|
const ip = this.getClientIP(req);
|
|
111
118
|
const cookies = this.parseCookies(req);
|
|
112
|
-
|
|
119
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
120
|
+
const virtualAsset = await (0, types_1.resolveVirtualAsset)(pathname);
|
|
121
|
+
if (virtualAsset) {
|
|
122
|
+
const headers = virtualAsset.headers || {};
|
|
123
|
+
headers['Content-Type'] = headers['Content-Type'] || virtualAsset.contentType || 'application/octet-stream';
|
|
124
|
+
res.writeHead(200, headers);
|
|
125
|
+
res.end(virtualAsset.body);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
113
129
|
const route = this.core.getRoute(pathname);
|
|
114
130
|
const rootroute = this.core.getRoute('root');
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
const pluginContext = route ? route.context : (rootroute ? rootroute.context : undefined);
|
|
132
|
+
const session = this.createSession(ip, cookies, res, req, pathname, pluginContext, { protocol: 'http', header: req.headers });
|
|
133
|
+
session._startAt = Date.now();
|
|
134
|
+
const handleRoute = async (routePath) => {
|
|
135
|
+
const matched = await this.core.executeRoute(routePath, session, queryParams);
|
|
117
136
|
if (!matched) {
|
|
118
137
|
this.serveStaticFile(pathname, res);
|
|
138
|
+
return;
|
|
119
139
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (this.enableCors) {
|
|
146
|
-
head['Access-Control-Allow-Origin'] = '*';
|
|
147
|
-
}
|
|
148
|
-
res.writeHead(session.status ?? 200, head);
|
|
140
|
+
if (session.responseHandled)
|
|
141
|
+
return;
|
|
142
|
+
const head = { ...session.head };
|
|
143
|
+
head['Set-Cookie'] = Object.entries(session.newCookie).map(([name, cookie]) => {
|
|
144
|
+
let cookieString = `${name}=${cookie.value}`;
|
|
145
|
+
if (cookie.options.expires)
|
|
146
|
+
cookieString += `; Expires=${cookie.options.expires.toUTCString()}`;
|
|
147
|
+
if (cookie.options.path)
|
|
148
|
+
cookieString += `; Path=${cookie.options.path}`;
|
|
149
|
+
if (cookie.options.domain)
|
|
150
|
+
cookieString += `; Domain=${cookie.options.domain}`;
|
|
151
|
+
if (cookie.options.secure)
|
|
152
|
+
cookieString += `; Secure`;
|
|
153
|
+
if (cookie.options.httpOnly)
|
|
154
|
+
cookieString += `; HttpOnly`;
|
|
155
|
+
if (cookie.options.sameSite)
|
|
156
|
+
cookieString += `; SameSite=${cookie.options.sameSite}`;
|
|
157
|
+
return cookieString;
|
|
158
|
+
});
|
|
159
|
+
if (this.enableCors) {
|
|
160
|
+
head['Access-Control-Allow-Origin'] = '*';
|
|
161
|
+
}
|
|
162
|
+
res.writeHead(session.status ?? 200, head);
|
|
163
|
+
switch (session.restype) {
|
|
164
|
+
case 'plain':
|
|
149
165
|
res.end(session.body);
|
|
150
|
-
|
|
166
|
+
break;
|
|
167
|
+
case 'buffer':
|
|
168
|
+
res.end(session.body);
|
|
169
|
+
break;
|
|
170
|
+
case 'json':
|
|
171
|
+
res.end(JSON.stringify(session.body));
|
|
172
|
+
break;
|
|
173
|
+
case 'stream':
|
|
174
|
+
if (isStream(session.body)) {
|
|
175
|
+
const stream = session.body;
|
|
176
|
+
stream.pipe(res);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
res.end(session.body); // fallback
|
|
151
181
|
}
|
|
182
|
+
};
|
|
183
|
+
if (route && route.allowedMethods.includes(req.method ?? 'GET')) {
|
|
184
|
+
await handleRoute(pathname);
|
|
152
185
|
}
|
|
153
186
|
else if (rootroute && rootroute.allowedMethods.includes(req.method ?? 'GET')) {
|
|
154
|
-
|
|
155
|
-
if (!matched) {
|
|
156
|
-
this.serveStaticFile(pathname, res);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
if (!session.responseHandled) {
|
|
160
|
-
let head = session.head;
|
|
161
|
-
head['Set-Cookie'] = Object.entries(session.newCookie).map(([name, cookie]) => {
|
|
162
|
-
let cookieString = `${name}=${cookie.value}`;
|
|
163
|
-
if (cookie.options.expires) {
|
|
164
|
-
cookieString += `; Expires=${cookie.options.expires.toUTCString()}`;
|
|
165
|
-
}
|
|
166
|
-
if (cookie.options.path) {
|
|
167
|
-
cookieString += `; Path=${cookie.options.path}`;
|
|
168
|
-
}
|
|
169
|
-
if (cookie.options.domain) {
|
|
170
|
-
cookieString += `; Domain=${cookie.options.domain}`;
|
|
171
|
-
}
|
|
172
|
-
if (cookie.options.secure) {
|
|
173
|
-
cookieString += `; Secure`;
|
|
174
|
-
}
|
|
175
|
-
if (cookie.options.httpOnly) {
|
|
176
|
-
cookieString += `; HttpOnly`;
|
|
177
|
-
}
|
|
178
|
-
if (cookie.options.sameSite) {
|
|
179
|
-
cookieString += `; SameSite=${cookie.options.sameSite}`;
|
|
180
|
-
}
|
|
181
|
-
return cookieString;
|
|
182
|
-
});
|
|
183
|
-
if (this.enableCors) {
|
|
184
|
-
head['Access-Control-Allow-Origin'] = '*';
|
|
185
|
-
}
|
|
186
|
-
res.writeHead(session.status ?? 200, head);
|
|
187
|
-
res.end(session.body);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
187
|
+
await handleRoute('root');
|
|
190
188
|
}
|
|
191
189
|
else {
|
|
192
190
|
this.serveStaticFile(pathname, res);
|
|
@@ -198,8 +196,9 @@ class Server {
|
|
|
198
196
|
const queryParams = url.searchParams;
|
|
199
197
|
const ip = this.getClientIP(req);
|
|
200
198
|
const cookies = this.parseCookies(req);
|
|
201
|
-
const session = this.createSession(ip, cookies, null, req, pathname, { protocol: 'http', header: req.headers });
|
|
202
199
|
const route = this.core.getRoute(pathname);
|
|
200
|
+
const pluginContext = route ? route.context : undefined;
|
|
201
|
+
const session = this.createSession(ip, cookies, null, req, pathname, pluginContext, { protocol: 'http', header: req.headers });
|
|
203
202
|
if (route && route.ws != null) {
|
|
204
203
|
const matched = await this.core.executeRoute(pathname, session, queryParams);
|
|
205
204
|
if (!matched) {
|
package/dist/session.d.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
**/
|
|
6
6
|
import { Server } from './server';
|
|
7
7
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
8
|
+
import { Stream } from "stream";
|
|
9
|
+
import { Context } from './context';
|
|
8
10
|
type ParsedParams = Record<string, string | string[] | undefined>;
|
|
11
|
+
type MissingMode = 'keep-template' | 'keep-key' | 'remove';
|
|
9
12
|
export interface Client {
|
|
10
13
|
req: IncomingMessage;
|
|
11
14
|
res: ServerResponse;
|
|
@@ -34,6 +37,13 @@ export interface StaticCacheOptions {
|
|
|
34
37
|
smaxAge?: number;
|
|
35
38
|
etagType?: 'md5' | 'sha1' | 'sha256' | 'sha512';
|
|
36
39
|
}
|
|
40
|
+
type ResType = "plain" | "json" | "stream" | "buffer";
|
|
41
|
+
interface BodyMap {
|
|
42
|
+
plain: string;
|
|
43
|
+
json: Record<string, any>;
|
|
44
|
+
stream: Stream;
|
|
45
|
+
buffer: Buffer;
|
|
46
|
+
}
|
|
37
47
|
export declare class Session {
|
|
38
48
|
ip: string;
|
|
39
49
|
cookie: Record<string, string>;
|
|
@@ -46,7 +56,8 @@ export declare class Session {
|
|
|
46
56
|
}>;
|
|
47
57
|
head: Record<string, any>;
|
|
48
58
|
status: number;
|
|
49
|
-
|
|
59
|
+
private _restype;
|
|
60
|
+
private _body;
|
|
50
61
|
properties?: Record<string, any>;
|
|
51
62
|
client: Client;
|
|
52
63
|
server: Server;
|
|
@@ -54,13 +65,18 @@ export declare class Session {
|
|
|
54
65
|
pathname: string;
|
|
55
66
|
languages: string[];
|
|
56
67
|
responseHandled: boolean;
|
|
68
|
+
pluginContext: Context | undefined;
|
|
57
69
|
/**
|
|
58
70
|
* @constructor
|
|
59
71
|
* @param ip 用户IP
|
|
60
72
|
* @param cookie 会话cookie
|
|
61
73
|
* @param query 请求字符串
|
|
62
74
|
*/
|
|
63
|
-
constructor(ip: string, cookie: Record<string, string>, server: Server, req?: IncomingMessage, res?: ServerResponse, pathname?: string, query?: Record<string, string
|
|
75
|
+
constructor(ip: string, cookie: Record<string, string>, server: Server, req?: IncomingMessage, res?: ServerResponse, pathname?: string, query?: Record<string, string>, pluginContext?: Context);
|
|
76
|
+
response<T extends ResType>(body: BodyMap[T], type?: T): void;
|
|
77
|
+
get restype(): ResType;
|
|
78
|
+
get body(): string;
|
|
79
|
+
set body(value: string);
|
|
64
80
|
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
65
81
|
/**
|
|
66
82
|
* 解析 Accept-Language 字符串为排序后的语言数组
|
|
@@ -104,7 +120,38 @@ export declare class Session {
|
|
|
104
120
|
* @param option 选项
|
|
105
121
|
*/
|
|
106
122
|
setCache(option: CacheOptions): void;
|
|
123
|
+
/**
|
|
124
|
+
* 设置静态文件
|
|
125
|
+
* @param content 文件内容
|
|
126
|
+
* @param option 选项
|
|
127
|
+
*/
|
|
107
128
|
static(content: string, option: CacheOptions): void;
|
|
129
|
+
/**
|
|
130
|
+
* 发送静态文件
|
|
131
|
+
* @param path 文件路径
|
|
132
|
+
* @param option 缓存选项
|
|
133
|
+
* @returns void
|
|
134
|
+
*/
|
|
108
135
|
file(path: string, option: StaticCacheOptions): void;
|
|
136
|
+
/**
|
|
137
|
+
* 发送普通文件
|
|
138
|
+
* @param path 文件路径
|
|
139
|
+
* @param isStream 是否流式传输
|
|
140
|
+
*/
|
|
141
|
+
sendFile(path: string, isStream?: boolean): void;
|
|
142
|
+
/**
|
|
143
|
+
* 渲染模板
|
|
144
|
+
* @template 模板字符串
|
|
145
|
+
* @data 数据
|
|
146
|
+
* @missing 缺失值处理方式,keep-template: 保留模板,keep-key: 保留键,remove: 移除
|
|
147
|
+
* @regex 模板匹配正则,默认为{{ xxx.xxx }}
|
|
148
|
+
*/
|
|
149
|
+
render(template: string, data: Record<string, any>, missing?: MissingMode, regex?: RegExp): string;
|
|
150
|
+
/**
|
|
151
|
+
* Renders a UI component using the plugin's declared renderer.
|
|
152
|
+
* @param component The component object to render.
|
|
153
|
+
* @param data The data/props to pass to the component.
|
|
154
|
+
*/
|
|
155
|
+
renderView(component: any, data?: Record<string, any>): Promise<void>;
|
|
109
156
|
}
|
|
110
157
|
export {};
|
package/dist/session.js
CHANGED
|
@@ -54,7 +54,8 @@ class Session {
|
|
|
54
54
|
newCookie = {};
|
|
55
55
|
head = {};
|
|
56
56
|
status = 200;
|
|
57
|
-
|
|
57
|
+
_restype = "plain";
|
|
58
|
+
_body;
|
|
58
59
|
properties = {};
|
|
59
60
|
client = null;
|
|
60
61
|
server;
|
|
@@ -62,18 +63,20 @@ class Session {
|
|
|
62
63
|
pathname;
|
|
63
64
|
languages;
|
|
64
65
|
responseHandled = false;
|
|
66
|
+
pluginContext;
|
|
65
67
|
/**
|
|
66
68
|
* @constructor
|
|
67
69
|
* @param ip 用户IP
|
|
68
70
|
* @param cookie 会话cookie
|
|
69
71
|
* @param query 请求字符串
|
|
70
72
|
*/
|
|
71
|
-
constructor(ip, cookie, server, req, res, pathname, query) {
|
|
73
|
+
constructor(ip, cookie, server, req, res, pathname, query, pluginContext) {
|
|
72
74
|
this.ip = ip;
|
|
73
75
|
this.cookie = cookie;
|
|
74
76
|
this.query = query;
|
|
75
77
|
this.server = server;
|
|
76
78
|
this.pathname = pathname;
|
|
79
|
+
this.pluginContext = pluginContext;
|
|
77
80
|
let header = {};
|
|
78
81
|
for (let key in req.headers) {
|
|
79
82
|
const value = req.headers[key];
|
|
@@ -108,6 +111,20 @@ class Session {
|
|
|
108
111
|
this.setCookie('lang', this.languages.join(','));
|
|
109
112
|
}
|
|
110
113
|
}
|
|
114
|
+
response(body, type = 'text') {
|
|
115
|
+
this._restype = type;
|
|
116
|
+
this._body = body;
|
|
117
|
+
}
|
|
118
|
+
get restype() {
|
|
119
|
+
return this._restype;
|
|
120
|
+
}
|
|
121
|
+
get body() {
|
|
122
|
+
return this._body;
|
|
123
|
+
}
|
|
124
|
+
set body(value) {
|
|
125
|
+
this._body = value;
|
|
126
|
+
this._restype = "plain"; // 自动把 restype 设成 plain
|
|
127
|
+
}
|
|
111
128
|
setCookie(name, value, options = {}) {
|
|
112
129
|
if (options.path === undefined) {
|
|
113
130
|
options.path = '/';
|
|
@@ -310,6 +327,11 @@ class Session {
|
|
|
310
327
|
if (option.expires)
|
|
311
328
|
this.head['Expires'] = option.expires.toUTCString();
|
|
312
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* 设置静态文件
|
|
332
|
+
* @param content 文件内容
|
|
333
|
+
* @param option 选项
|
|
334
|
+
*/
|
|
313
335
|
static(content, option) {
|
|
314
336
|
// 先查看用户是否在询问文件修改情况
|
|
315
337
|
if (this.client.headers['If-Modified-Since']) {
|
|
@@ -326,8 +348,14 @@ class Session {
|
|
|
326
348
|
}
|
|
327
349
|
}
|
|
328
350
|
this.setCache(option);
|
|
329
|
-
this.
|
|
351
|
+
this.response(content);
|
|
330
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* 发送静态文件
|
|
355
|
+
* @param path 文件路径
|
|
356
|
+
* @param option 缓存选项
|
|
357
|
+
* @returns void
|
|
358
|
+
*/
|
|
331
359
|
file(path, option) {
|
|
332
360
|
if (this.client.headers['If-Modified-Since']) {
|
|
333
361
|
const modified = new Date(this.client.headers['If-Modified-Since']);
|
|
@@ -353,7 +381,80 @@ class Session {
|
|
|
353
381
|
smaxAge: option.smaxAge,
|
|
354
382
|
cacheControl: 'public'
|
|
355
383
|
});
|
|
356
|
-
this.
|
|
384
|
+
this.response(fs_1.default.createReadStream(path), 'stream');
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 发送普通文件
|
|
388
|
+
* @param path 文件路径
|
|
389
|
+
* @param isStream 是否流式传输
|
|
390
|
+
*/
|
|
391
|
+
sendFile(path, isStream = false) {
|
|
392
|
+
if (isStream) {
|
|
393
|
+
this.setMime('application/octet-stream');
|
|
394
|
+
this.response(fs_1.default.createReadStream(path), 'stream');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this.response(fs_1.default.readFileSync(path), 'buffer');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 渲染模板
|
|
402
|
+
* @template 模板字符串
|
|
403
|
+
* @data 数据
|
|
404
|
+
* @missing 缺失值处理方式,keep-template: 保留模板,keep-key: 保留键,remove: 移除
|
|
405
|
+
* @regex 模板匹配正则,默认为{{ xxx.xxx }}
|
|
406
|
+
*/
|
|
407
|
+
render(template, data, missing = 'keep-template', regex = /\{\{\s*([\w.]+)\s*\}\}/g) {
|
|
408
|
+
const getValue = (obj, path) => {
|
|
409
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
410
|
+
};
|
|
411
|
+
return template.replace(regex, (_, key) => {
|
|
412
|
+
const value = getValue(data, key);
|
|
413
|
+
if (value !== undefined)
|
|
414
|
+
return String(value);
|
|
415
|
+
switch (missing) {
|
|
416
|
+
case 'keep-key':
|
|
417
|
+
return key;
|
|
418
|
+
case 'remove':
|
|
419
|
+
return '';
|
|
420
|
+
case 'keep-template':
|
|
421
|
+
default:
|
|
422
|
+
return _;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Renders a UI component using the plugin's declared renderer.
|
|
428
|
+
* @param component The component object to render.
|
|
429
|
+
* @param data The data/props to pass to the component.
|
|
430
|
+
*/
|
|
431
|
+
async renderView(component, data = {}) {
|
|
432
|
+
if (!this.pluginContext) {
|
|
433
|
+
throw new Error(`Cannot call 'renderView' because the session is not associated with a plugin context.`);
|
|
434
|
+
}
|
|
435
|
+
const rendererName = this.pluginContext.instance.render;
|
|
436
|
+
const pluginName = this.pluginContext.pluginname;
|
|
437
|
+
if (!rendererName) {
|
|
438
|
+
this.server.core.logger.error(`Plugin "${pluginName}" uses 'renderView' but did not declare a renderer. Please add 'export const render = "your-renderer-name";' to your plugin's entry file.`);
|
|
439
|
+
// throw new Error(`Plugin "${pluginName}" did not declare a renderer.`);
|
|
440
|
+
}
|
|
441
|
+
const renderer = this.server.core.renderers.get(rendererName);
|
|
442
|
+
if (!renderer) {
|
|
443
|
+
this.server.core.logger.error(`Renderer "${rendererName}" declared by plugin "${pluginName}" is not registered. Have you installed the renderer package (e.g., '@yumerijs/vue-renderer')?`);
|
|
444
|
+
// throw new Error(`Renderer "${rendererName}" is not registered.`);
|
|
445
|
+
}
|
|
446
|
+
const renderOptions = {
|
|
447
|
+
pluginName,
|
|
448
|
+
};
|
|
449
|
+
try {
|
|
450
|
+
const html = await renderer.render(component, data, renderOptions);
|
|
451
|
+
this.setMime('text/html');
|
|
452
|
+
this.response(html, 'plain');
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
this.server.core.logger.error(`Error while rendering view for plugin "${pluginName}":`, error);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
357
458
|
}
|
|
358
459
|
}
|
|
359
460
|
exports.Session = Session;
|