@yumerijs/core 2.0.14 → 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
@@ -26,6 +26,7 @@ export declare class Context {
26
26
  private childPlugins;
27
27
  private i18ns;
28
28
  component: Components;
29
+ instance: any;
29
30
  /** 插件名称 */
30
31
  pluginname: string;
31
32
  /**
@@ -33,7 +34,7 @@ export declare class Context {
33
34
  * @param core Core 实例
34
35
  * @param pluginname 插件名称
35
36
  */
36
- constructor(core: Core, pluginname: string, injections?: Record<string, any>);
37
+ constructor(core: Core, pluginname: string, instance?: any, injections?: Record<string, any>);
37
38
  /**
38
39
  * 注入依赖
39
40
  * @param name 依赖名称
package/dist/context.js CHANGED
@@ -17,6 +17,7 @@ class Context {
17
17
  childPlugins = new Map();
18
18
  i18ns = [];
19
19
  component;
20
+ instance;
20
21
  /** 插件名称 */
21
22
  pluginname;
22
23
  /**
@@ -24,8 +25,9 @@ class Context {
24
25
  * @param core Core 实例
25
26
  * @param pluginname 插件名称
26
27
  */
27
- constructor(core, pluginname, injections = {}) {
28
+ constructor(core, pluginname, instance, injections = {}) {
28
29
  this.core = core;
30
+ this.instance = instance;
29
31
  this.pluginname = pluginname;
30
32
  this.childPlugins = new Map();
31
33
  this.component = injections;
@@ -46,10 +48,10 @@ class Context {
46
48
  route(path) {
47
49
  if (this.core.routes[path]) {
48
50
  this.core.logger.warn(`Plugin "${this.pluginname}" attempt to register route "${path}", but it has already been registered.`);
49
- return new route_1.Route(path);
51
+ return new route_1.Route(path, this);
50
52
  }
51
53
  this.routes.push(path);
52
- return this.core.route(path);
54
+ return this.core.route(path, this);
53
55
  }
54
56
  /**
55
57
  * 注册事件
@@ -195,10 +197,7 @@ class Context {
195
197
  this.middlewares.forEach((middleware) => delete this.core.globalMiddlewares[middleware]);
196
198
  // 删除事件监听器
197
199
  this.eventlisteners.forEach(({ name, listener }) => {
198
- const listeners = this.core.eventListeners?.[name];
199
- if (listeners) {
200
- this.core.eventListeners[name] = listeners.filter(l => l !== listener);
201
- }
200
+ this.core.off(name, listener);
202
201
  });
203
202
  // 删除钩子
204
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,6 +44,7 @@ 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');
48
49
  function isStream(value) {
49
50
  return value && typeof value.pipe === "function";
@@ -62,8 +63,11 @@ class Server {
62
63
  this.enableCors = config.enableCors ?? true;
63
64
  this.staticDir = config.staticDir ?? 'static';
64
65
  }
65
- createSession(ip, cookies, res, req, pathname, extra = {}) {
66
- 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);
67
71
  Object.assign(session.properties, extra);
68
72
  session.protocol = extra.protocol ?? 'http';
69
73
  return session;
@@ -112,9 +116,21 @@ class Server {
112
116
  const queryParams = url.searchParams;
113
117
  const ip = this.getClientIP(req);
114
118
  const cookies = this.parseCookies(req);
115
- 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
+ }
116
129
  const route = this.core.getRoute(pathname);
117
130
  const rootroute = this.core.getRoute('root');
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();
118
134
  const handleRoute = async (routePath) => {
119
135
  const matched = await this.core.executeRoute(routePath, session, queryParams);
120
136
  if (!matched) {
@@ -180,8 +196,9 @@ class Server {
180
196
  const queryParams = url.searchParams;
181
197
  const ip = this.getClientIP(req);
182
198
  const cookies = this.parseCookies(req);
183
- const session = this.createSession(ip, cookies, null, req, pathname, { protocol: 'http', header: req.headers });
184
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 });
185
202
  if (route && route.ws != null) {
186
203
  const matched = await this.core.executeRoute(pathname, session, queryParams);
187
204
  if (!matched) {
package/dist/session.d.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  import { Server } from './server';
7
7
  import { IncomingMessage, ServerResponse } from 'http';
8
8
  import { Stream } from "stream";
9
+ import { Context } from './context';
9
10
  type ParsedParams = Record<string, string | string[] | undefined>;
11
+ type MissingMode = 'keep-template' | 'keep-key' | 'remove';
10
12
  export interface Client {
11
13
  req: IncomingMessage;
12
14
  res: ServerResponse;
@@ -63,13 +65,14 @@ export declare class Session {
63
65
  pathname: string;
64
66
  languages: string[];
65
67
  responseHandled: boolean;
68
+ pluginContext: Context | undefined;
66
69
  /**
67
70
  * @constructor
68
71
  * @param ip 用户IP
69
72
  * @param cookie 会话cookie
70
73
  * @param query 请求字符串
71
74
  */
72
- 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);
73
76
  response<T extends ResType>(body: BodyMap[T], type?: T): void;
74
77
  get restype(): ResType;
75
78
  get body(): string;
@@ -136,5 +139,19 @@ export declare class Session {
136
139
  * @param isStream 是否流式传输
137
140
  */
138
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>;
139
156
  }
140
157
  export {};
package/dist/session.js CHANGED
@@ -63,18 +63,20 @@ class Session {
63
63
  pathname;
64
64
  languages;
65
65
  responseHandled = false;
66
+ pluginContext;
66
67
  /**
67
68
  * @constructor
68
69
  * @param ip 用户IP
69
70
  * @param cookie 会话cookie
70
71
  * @param query 请求字符串
71
72
  */
72
- constructor(ip, cookie, server, req, res, pathname, query) {
73
+ constructor(ip, cookie, server, req, res, pathname, query, pluginContext) {
73
74
  this.ip = ip;
74
75
  this.cookie = cookie;
75
76
  this.query = query;
76
77
  this.server = server;
77
78
  this.pathname = pathname;
79
+ this.pluginContext = pluginContext;
78
80
  let header = {};
79
81
  for (let key in req.headers) {
80
82
  const value = req.headers[key];
@@ -395,5 +397,64 @@ class Session {
395
397
  this.response(fs_1.default.readFileSync(path), 'buffer');
396
398
  }
397
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
+ }
458
+ }
398
459
  }
399
460
  exports.Session = Session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/core",
3
- "version": "2.0.14",
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",