@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/session.js CHANGED
@@ -6,96 +6,187 @@
6
6
  import crypto from 'crypto';
7
7
  import * as formidable from 'formidable';
8
8
  import fs from 'fs';
9
- export class Session {
9
+ /**
10
+ * 会话请求包装类
11
+ * 负责收集和整理所有来自客户端的请求信息
12
+ */
13
+ class SessionRequest {
14
+ /** 客户端的 IP 地址 */
10
15
  ip;
11
- cookie;
16
+ /** 请求中携带的原始 Cookie 键值对 */
17
+ cookies;
18
+ /** URL 中的查询字符串参数 */
12
19
  query;
20
+ /** 请求的资源路径(不含查询参数) */
21
+ pathname;
22
+ /** 所有的请求头信息 */
23
+ headers;
24
+ /** 客户端支持的语言列表,已按权重排序 */
25
+ languages = [];
26
+ /** 请求使用的协议 */
27
+ protocol = 'http';
28
+ /** 原始的 IncomingMessage 对象 */
29
+ raw;
30
+ constructor(data) {
31
+ this.ip = data.ip;
32
+ this.cookies = data.cookies;
33
+ this.query = data.query;
34
+ this.pathname = data.pathname || '/';
35
+ this.headers = data.headers;
36
+ this.raw = data.raw;
37
+ }
38
+ }
39
+ /**
40
+ * 会话响应包装类
41
+ * 负责管理和准备发往客户端的数据
42
+ */
43
+ class SessionResponse {
44
+ /** 待发送的 HTTP 状态码 */
45
+ status = 200;
46
+ /** 待发送的响应头 */
47
+ headers = { 'Content-Type': 'text/plain' };
48
+ /** 准备设置到客户端的 Cookie 集合 */
49
+ cookies = {};
50
+ /** 响应体的具体类型 */
51
+ type = "plain";
52
+ /** 响应的具体内容 */
53
+ body;
54
+ /** 标识该响应是否已经处理完毕 */
55
+ handled = false;
56
+ /** 原始的 ServerResponse 对象 */
57
+ raw;
58
+ constructor(res) {
59
+ this.raw = res;
60
+ }
61
+ }
62
+ /**
63
+ * 会话对象 (Session)
64
+ * 是插件开发中最核心的对象,封装了请求、响应及各种便捷工具
65
+ */
66
+ export class Session {
67
+ /** 封装后的请求对象 */
68
+ request;
69
+ /** 封装后的响应对象 */
70
+ response;
71
+ /** 当前会话的唯一标识符 (UUID) */
13
72
  sessionid;
73
+ /** 持久化的会话数据存储 */
14
74
  data = {};
15
- newCookie = {};
16
- head = {};
17
- status = 200;
18
- _restype = "plain";
19
- _body;
75
+ /** 供插件或中间件挂载的临时属性 */
20
76
  properties = {};
21
- client = null;
77
+ /** 对核心服务器实例的引用 */
22
78
  server;
23
- protocol = 'http';
24
- pathname;
25
- languages;
26
- responseHandled = false;
79
+ /** 当前插件的上下文环境 */
27
80
  pluginContext;
28
81
  /**
29
- * @constructor
30
- * @param ip 用户IP
31
- * @param cookie 会话cookie
32
- * @param query 请求字符串
82
+ * 初始化一个新的会话
83
+ * @param ip 客户端IP
84
+ * @param cookie 请求Cookie
85
+ * @param server 服务器实例
86
+ * @param req 原始请求对象
87
+ * @param res 原始响应对象
88
+ * @param pathname 请求路径
89
+ * @param query 查询参数
90
+ * @param pluginContext 插件上下文
33
91
  */
34
92
  constructor(ip, cookie, server, req, res, pathname, query, pluginContext) {
35
- this.ip = ip;
36
- this.cookie = cookie;
37
- this.query = query;
38
93
  this.server = server;
39
- this.pathname = pathname;
40
94
  this.pluginContext = pluginContext;
41
- let header = {};
42
- for (let key in req.headers) {
43
- const value = req.headers[key];
44
- if (typeof value === 'string') {
45
- header[key] = value;
46
- }
47
- else if (Array.isArray(value)) {
48
- header[key] = value.join(', ');
49
- }
50
- else if (value !== undefined) {
51
- header[key] = String(value);
95
+ const headers = {};
96
+ if (req) {
97
+ for (const key in req.headers) {
98
+ const value = req.headers[key];
99
+ headers[key] = Array.isArray(value) ? value.join(', ') : String(value || '');
52
100
  }
53
101
  }
54
- this.client = {
55
- req: req,
56
- res: res,
57
- headers: header
58
- };
59
- this.head['Content-Type'] = 'text/plain';
102
+ this.request = new SessionRequest({ ip, cookies: cookie, query, pathname, headers, raw: req });
103
+ this.response = new SessionResponse(res);
60
104
  if (cookie.sessionid) {
61
105
  this.sessionid = cookie.sessionid;
62
106
  }
63
107
  else {
64
- this.sessionid = this.generateId(this.ip);
108
+ this.sessionid = this.generateId();
65
109
  this.setCookie('sessionid', this.sessionid);
66
110
  }
67
111
  if (cookie.lang) {
68
- this.languages = cookie.lang.split(',');
112
+ this.request.languages = cookie.lang.split(',');
69
113
  }
70
- else if (req.headers['accept-language']) {
71
- this.languages = this.parseAcceptLanguages(req.headers['accept-language']);
72
- this.setCookie('lang', this.languages.join(','));
114
+ else if (headers['accept-language']) {
115
+ this.request.languages = this.parseAcceptLanguages(headers['accept-language']);
116
+ this.setCookie('lang', this.request.languages.join(','));
73
117
  }
74
118
  }
75
- response(body, type = 'text') {
76
- this._restype = type;
77
- this._body = body;
119
+ /** 获取客户端 IP 地址 */
120
+ get ip() { return this.request.ip; }
121
+ /** 获取请求中的 Cookie 集合 */
122
+ get cookie() { return this.request.cookies; }
123
+ /** 获取 URL 查询参数 */
124
+ get query() { return this.request.query; }
125
+ /** 获取请求路径 */
126
+ get pathname() { return this.request.pathname; }
127
+ /** 获取语言列表 */
128
+ get languages() { return this.request.languages; }
129
+ /** 获取请求协议 */
130
+ get protocol() { return this.request.protocol; }
131
+ set protocol(val) { this.request.protocol = val; }
132
+ /** 获取或设置响应状态码 */
133
+ get status() { return this.response.status; }
134
+ set status(val) { this.response.status = val; }
135
+ /** 获取待发送的新 Cookie */
136
+ get newCookie() { return this.response.cookies; }
137
+ /** 获取响应头对象 */
138
+ get head() { return this.response.headers; }
139
+ /** 获取当前响应内容类型 */
140
+ get _restype() { return this.response.type; }
141
+ /** 获取响应是否已处理的标记 */
142
+ get responseHandled() { return this.response.handled; }
143
+ set responseHandled(val) { this.response.handled = val; }
144
+ /** 组装并获取 Client 兼容对象 */
145
+ get client() {
146
+ return {
147
+ req: this.request.raw,
148
+ res: this.response.raw,
149
+ headers: this.request.headers
150
+ };
151
+ }
152
+ /**
153
+ * 设置响应体内容
154
+ * @param body 响应内容
155
+ * @param type 内容类型,默认为 'plain'
156
+ */
157
+ respond(body, type = 'plain') {
158
+ this.response.type = type;
159
+ this.response.body = body;
78
160
  }
161
+ /** 获取响应类型 */
79
162
  get restype() {
80
- return this._restype;
163
+ return this.response.type;
81
164
  }
165
+ /** 获取响应体内容 */
82
166
  get body() {
83
- return this._body;
167
+ return this.response.body;
84
168
  }
169
+ /** 设置响应体内容,自动推断类型 */
85
170
  set body(value) {
86
- this._body = value;
87
- this._restype = "plain"; // 自动把 restype 设成 plain
171
+ this.response.body = value;
172
+ this.response.type = typeof value === 'string' ? "plain" : "json";
88
173
  }
174
+ /**
175
+ * 设置响应 Cookie
176
+ * @param name Cookie 键名
177
+ * @param value Cookie 值
178
+ * @param options 配置项
179
+ */
89
180
  setCookie(name, value, options = {}) {
90
181
  if (options.path === undefined) {
91
182
  options.path = '/';
92
183
  }
93
- this.newCookie[name] = { value, options };
184
+ this.response.cookies[name] = { value, options };
94
185
  }
95
186
  /**
96
- * 解析 Accept-Language 字符串为排序后的语言数组
97
- * @param header Accept-Language 头,比如 "zh-CN,zh-TW;q=0.8,en-US;q=0.6"
98
- * @returns 按优先级排序的语言数组,自动补上父语言
187
+ * 解析 Accept-Language 请求头
188
+ * @param header 原始头字符串
189
+ * @returns 排序后的语言数组
99
190
  */
100
191
  parseAcceptLanguages(header) {
101
192
  if (!header)
@@ -136,18 +227,14 @@ export class Session {
136
227
  return result;
137
228
  }
138
229
  /**
139
- * 生成 sessionid
140
- * @param ip 用户IP
141
- * @param option 选项
230
+ * 生成会话 ID (UUID)
142
231
  */
143
- generateId(ip, option) {
144
- const sessionId = this.md5(ip + Date.now().toString() + Math.random().toString()); // 添加随机性
145
- return sessionId;
232
+ generateId() {
233
+ return crypto.randomUUID();
146
234
  }
147
235
  /**
148
- * 解析请求体
149
- * @param client 客户端
150
- * @returns Promise<ParsedParams>
236
+ * 异步解析请求体
237
+ * @returns 返回解析后的参数
151
238
  */
152
239
  async parseRequestBody(client = this.client) {
153
240
  const req = client.req;
@@ -191,90 +278,58 @@ export class Session {
191
278
  });
192
279
  }
193
280
  /**
194
- * 获取请求体
195
- * @param req 请求对象
196
- * @returns Promise<string>
281
+ * 获取请求体文本
197
282
  */
198
283
  async getReqBody(req) {
199
- let body = "";
200
- req.on('data', chunk => body += chunk.toString());
201
- return body;
284
+ return new Promise((resolve) => {
285
+ let body = "";
286
+ req.on('data', chunk => body += chunk.toString());
287
+ req.on('end', () => resolve(body));
288
+ });
202
289
  }
203
- // MD5 加密
290
+ /**
291
+ * MD5 加密
292
+ */
204
293
  md5(str) {
205
294
  const hash = crypto.createHash('md5');
206
295
  hash.update(str);
207
296
  return hash.digest('hex');
208
297
  }
209
- // 设置会话数据
210
- setData(key, value) {
211
- this.data[key] = value;
212
- }
213
- // 删除会话数据
214
- deleteData(key) {
215
- delete this.data[key];
216
- }
217
- // 清空会话数据
218
- clearData() {
219
- this.data = {};
220
- }
221
- // 销毁会话
222
- destroy() {
223
- this.clearData(); // 清理会话数据
224
- }
298
+ /** 设置会话数据 */
299
+ setData(key, value) { this.data[key] = value; }
300
+ /** 删除会话数据 */
301
+ deleteData(key) { delete this.data[key]; }
302
+ /** 清空会话数据 */
303
+ clearData() { this.data = {}; }
304
+ /** 销毁会话 */
305
+ destroy() { this.clearData(); }
306
+ /** 设置 MIME 类型 */
225
307
  setMime(mimeType) {
226
- switch (mimeType) {
227
- case 'png':
228
- this.head['Content-Type'] = 'image/png';
229
- break;
230
- case 'jpg':
231
- case 'jpeg':
232
- this.head['Content-Type'] = 'image/jpeg';
233
- break;
234
- case 'pdf':
235
- this.head['Content-Type'] = 'application/pdf';
236
- break;
237
- case 'plain':
238
- this.head['Content-Type'] = 'text/plain';
239
- break;
240
- case 'html':
241
- this.head['Content-Type'] = 'text/html';
242
- break;
243
- case 'json':
244
- this.head['Content-Type'] = 'application/json';
245
- break;
246
- case 'xml':
247
- this.head['Content-Type'] = 'application/xml';
248
- break;
249
- default:
250
- this.head['Content-Type'] = mimeType;
251
- break;
252
- }
253
- }
254
- send(data) {
255
- return this.client.res.write(data);
308
+ const map = {
309
+ 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
310
+ 'pdf': 'application/pdf', 'plain': 'text/plain', 'html': 'text/html',
311
+ 'json': 'application/json', 'xml': 'application/xml'
312
+ };
313
+ this.response.headers['Content-Type'] = map[mimeType] || mimeType;
256
314
  }
315
+ /** 向响应流写入数据 */
316
+ send(data) { return this.response.raw.write(data); }
317
+ /** 结束响应 */
257
318
  endsession(message) {
258
- this.responseHandled = true;
259
- return this.client.res.end(message);
319
+ this.response.handled = true;
320
+ return this.response.raw.end(message);
260
321
  }
261
- /**
262
- * 获取I18n文本
263
- * @param name 文本点名称
264
- */
322
+ /** 获取国际化文本 */
265
323
  text(name) {
266
- return this.server.core.i18n.get(name, this.languages);
324
+ return this.server.core.i18n.get(name, this.request.languages);
267
325
  }
268
- /**
269
- * 设置缓存标头
270
- * @param option 选项
271
- */
326
+ /** 设置缓存标头 */
272
327
  setCache(option) {
273
328
  if (option.etag)
274
- this.head['ETag'] = `${option.etag}`;
329
+ this.response.headers['ETag'] = `${option.etag}`;
275
330
  if (option.modified)
276
- this.head['Last-Modified'] = option.modified.toUTCString();
277
- let control = [];
331
+ this.response.headers['Last-Modified'] = option.modified.toUTCString();
332
+ const control = [];
278
333
  if (option.cacheControl !== undefined)
279
334
  control.push(option.cacheControl);
280
335
  if (option.maxAge !== undefined)
@@ -284,148 +339,90 @@ export class Session {
284
339
  if (option.isImmutable)
285
340
  control.push('immutable');
286
341
  if (control.length > 0)
287
- this.head['Cache-Control'] = control.join(', ');
342
+ this.response.headers['Cache-Control'] = control.join(', ');
288
343
  if (option.expires)
289
- this.head['Expires'] = option.expires.toUTCString();
344
+ this.response.headers['Expires'] = option.expires.toUTCString();
290
345
  }
291
- /**
292
- * 设置静态文件
293
- * @param content 文件内容
294
- * @param option 选项
295
- */
346
+ /** 发送静态内容并处理缓存 */
296
347
  static(content, option) {
297
- // 先查看用户是否在询问文件修改情况
298
- if (this.client.headers['If-Modified-Since']) {
299
- const modified = new Date(this.client.headers['If-Modified-Since']);
300
- if (modified.getTime() === option.modified?.getTime()) {
301
- this.status = 304;
348
+ if (this.request.headers['if-modified-since']) {
349
+ const modified = new Date(this.request.headers['if-modified-since']);
350
+ if (option.modified && modified.getTime() === option.modified.getTime()) {
351
+ this.response.status = 304;
302
352
  return;
303
353
  }
304
354
  }
305
- if (this.client.headers['If-None-Match']) {
306
- if (this.client.headers['If-None-Match'] === option.etag) {
307
- this.status = 304;
308
- return;
309
- }
355
+ if (this.request.headers['if-none-match'] === option.etag) {
356
+ this.response.status = 304;
357
+ return;
310
358
  }
311
359
  this.setCache(option);
312
- this.response(content);
360
+ this.respond(content);
313
361
  }
314
- /**
315
- * 发送静态文件
316
- * @param path 文件路径
317
- * @param option 缓存选项
318
- * @returns void
319
- */
362
+ /** 发送文件并处理缓存 */
320
363
  file(path, option) {
321
- if (this.client.headers['If-Modified-Since']) {
322
- const modified = new Date(this.client.headers['If-Modified-Since']);
323
- const moditime = fs.statSync(path).mtime;
324
- if (modified.getTime() === moditime.getTime()) {
325
- this.status = 304;
364
+ const stats = fs.statSync(path);
365
+ if (this.request.headers['if-modified-since']) {
366
+ const modified = new Date(this.request.headers['if-modified-since']);
367
+ if (modified.getTime() === stats.mtime.getTime()) {
368
+ this.response.status = 304;
326
369
  return;
327
370
  }
328
371
  }
329
372
  const etag = crypto.createHash(option.etagType || 'md5').update(fs.readFileSync(path)).digest('hex');
330
- if (this.client.headers['If-None-Match']) {
331
- if (option.etag) {
332
- if (this.client.headers['If-None-Match'] === etag) {
333
- this.status = 304;
334
- return;
335
- }
336
- }
373
+ if (this.request.headers['if-none-match'] === etag) {
374
+ this.response.status = 304;
375
+ return;
337
376
  }
338
377
  this.setCache({
339
- modified: fs.statSync(path).mtime,
340
- etag,
341
- maxAge: option.maxAge,
342
- smaxAge: option.smaxAge,
343
- cacheControl: 'public'
378
+ modified: stats.mtime, etag,
379
+ maxAge: option.maxAge, smaxAge: option.smaxAge, cacheControl: 'public'
344
380
  });
345
- this.response(fs.createReadStream(path), 'stream');
381
+ this.respond(fs.createReadStream(path), 'stream');
346
382
  }
347
- /**
348
- * 发送普通文件
349
- * @param path 文件路径
350
- * @param isStream 是否流式传输
351
- */
383
+ /** 直接发送文件 */
352
384
  sendFile(path, isStream = false) {
353
385
  if (isStream) {
354
386
  this.setMime('application/octet-stream');
355
- this.response(fs.createReadStream(path), 'stream');
387
+ this.respond(fs.createReadStream(path), 'stream');
356
388
  }
357
389
  else {
358
- this.response(fs.readFileSync(path), 'buffer');
390
+ this.respond(fs.readFileSync(path), 'buffer');
359
391
  }
360
392
  }
361
- /**
362
- * 渲染模板
363
- * @template 模板字符串
364
- * @data 数据
365
- * @missing 缺失值处理方式,keep-template: 保留模板,keep-key: 保留键,remove: 移除
366
- * @regex 模板匹配正则,默认为{{ xxx.xxx }}
367
- */
368
- render(template, data, missing = 'keep-template', regex = /\{\{\s*([\w.]+)\s*\}\}/g) {
369
- const getValue = (obj, path) => {
370
- return path.split('.').reduce((acc, key) => acc?.[key], obj);
371
- };
372
- return template.replace(regex, (_, key) => {
373
- const value = getValue(data, key);
374
- if (value !== undefined)
375
- return String(value);
376
- switch (missing) {
377
- case 'keep-key':
378
- return key;
379
- case 'remove':
380
- return '';
381
- case 'keep-template':
382
- default:
383
- return _;
384
- }
385
- });
386
- }
387
- /**
388
- * Renders a UI component using the plugin's declared renderer.
389
- * @param component The component object to render.
390
- * @param data The data/props to pass to the component.
391
- */
393
+ /** 渲染组件视图 */
392
394
  async renderView(component, data = {}) {
393
- if (!this.pluginContext) {
394
- throw new Error(`Cannot call 'renderView' because the session is not associated with a plugin context.`);
395
- }
396
- const renderer = this.pluginContext.renderer || null;
397
- const pluginName = this.pluginContext.pluginname;
395
+ if (!this.pluginContext)
396
+ throw new Error(`Plugin context missing.`);
397
+ const renderer = this.pluginContext.renderer;
398
398
  if (!renderer) {
399
- this.server.core.logger.error(`Renderer declared by plugin "${pluginName}" is not registered. Have you installed the renderer package (e.g., '@yumerijs/vue-renderer')?`);
400
- // throw new Error(`Renderer "${rendererName}" is not registered.`);
399
+ this.server.core.logger.error(`Renderer missing for ${this.pluginContext.pluginname}`);
400
+ return;
401
401
  }
402
- const renderOptions = {
403
- pluginName,
404
- };
405
402
  try {
406
- const html = await renderer.render(component, data, renderOptions);
403
+ const html = await renderer.render(component, data, { pluginName: this.pluginContext.pluginname });
407
404
  this.setMime('text/html');
408
- this.response(html, 'plain');
405
+ this.respond(html, 'plain');
409
406
  }
410
407
  catch (error) {
411
- this.server.core.logger.error(`Error while rendering view for plugin "${pluginName}":`, error);
408
+ this.server.core.logger.error(`Render error:`, error);
412
409
  throw error;
413
410
  }
414
411
  }
415
- async renderFile(filePath, data) {
416
- const renderer = this.pluginContext.renderer;
417
- const pluginName = this.pluginContext.pluginname;
412
+ /** 渲染文件 */
413
+ async render(filePath, data) {
414
+ const renderer = this.pluginContext?.renderer;
418
415
  if (!renderer) {
419
- this.server.core.logger.error(`Renderer declared by plugin "${pluginName}" is not registered. Have you installed the renderer package (e.g., '@yumerijs/vue-renderer')?`);
420
- // throw new Error(`Renderer "${rendererName}" is not registered.`);
416
+ this.server.core.logger.error(`Renderer missing.`);
417
+ return;
421
418
  }
422
419
  try {
423
420
  const html = await renderer.renderFile(filePath, data);
424
421
  this.setMime('text/html');
425
- this.response(html, 'plain');
422
+ this.respond(html, 'plain');
426
423
  }
427
424
  catch (error) {
428
- this.server.core.logger.error(`Error while rendering file "${filePath}":`, error);
425
+ this.server.core.logger.error(`File render error:`, error);
429
426
  throw error;
430
427
  }
431
428
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@yumerijs/core",
3
- "version": "2.2.2",
3
+ "version": "3.0.0-alpha.2",
4
4
  "description": "Core module for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "type": "module",
7
8
  "files": [
8
9
  "dist"
9
10
  ],