@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 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
- const listeners = this.core.eventListeners?.[name];
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
- eventListeners: {
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, ...args: any[]): Promise<void>;
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
- eventListeners = {};
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
- if (!this.eventListeners[event]) {
63
- this.eventListeners[event] = [];
64
- }
65
- this.eventListeners[event].push(listener);
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
- async emit(event, ...args) {
68
- if (this.eventListeners[event]) {
69
- for (const listener of this.eventListeners[event]) {
70
- try {
71
- await listener(...args);
72
- }
73
- catch (err) {
74
- this.logger.error(`Error in event listener for "${event}":`, err);
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
- else
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
- return input.replace(/\{\{(.*?)\}\}/g, (_, key) => this.get(key.trim(), langs));
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
@@ -14,6 +14,7 @@ export declare class Server {
14
14
  private staticDir;
15
15
  private httpServer;
16
16
  constructor(core: Core, config?: Partial<ServerConfig>);
17
+ getStaticDir(): string;
17
18
  private createSession;
18
19
  private parseCookies;
19
20
  private getClientIP;
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
- createSession(ip, cookies, res, req, pathname, extra = {}) {
63
- const session = new session_1.Session(ip, cookies, this, req, res, pathname);
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
- const session = this.createSession(ip, cookies, res, req, pathname, { protocol: 'http', header: req.headers });
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
- if (route && route.allowedMethods.includes(req.method ?? 'GET')) {
116
- const matched = await this.core.executeRoute(pathname, session, queryParams);
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
- else {
121
- if (!session.responseHandled) {
122
- let head = session.head;
123
- head['Set-Cookie'] = Object.entries(session.newCookie).map(([name, cookie]) => {
124
- let cookieString = `${name}=${cookie.value}`;
125
- if (cookie.options.expires) {
126
- cookieString += `; Expires=${cookie.options.expires.toUTCString()}`;
127
- }
128
- if (cookie.options.path) {
129
- cookieString += `; Path=${cookie.options.path}`;
130
- }
131
- if (cookie.options.domain) {
132
- cookieString += `; Domain=${cookie.options.domain}`;
133
- }
134
- if (cookie.options.secure) {
135
- cookieString += `; Secure`;
136
- }
137
- if (cookie.options.httpOnly) {
138
- cookieString += `; HttpOnly`;
139
- }
140
- if (cookie.options.sameSite) {
141
- cookieString += `; SameSite=${cookie.options.sameSite}`;
142
- }
143
- return cookieString;
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
- const matched = await this.core.executeRoute('root', session, queryParams);
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
- body: any;
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
- body;
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.body = content;
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.body = fs_1.default.readFileSync(path, 'utf-8');
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/core",
3
- "version": "2.0.13",
3
+ "version": "2.1.0",
4
4
  "description": "Core module for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",