@yumerijs/core 3.0.0-alpha.1 → 3.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.d.ts CHANGED
@@ -1,14 +1,8 @@
1
- import { Core } from './core.js';
1
+ import { Core, Plugin } from './core.js';
2
2
  import { Route } from './route.js';
3
3
  import { HookHandler } from './hook.js';
4
4
  import { Middleware } from './middleware.js';
5
5
  import { IRenderer } from '@yumerijs/types';
6
- interface Plugin {
7
- apply: (ctx: Context, config: any) => any;
8
- disable: (ctx: Context) => Promise<void>;
9
- depend: Array<string>;
10
- provide: Array<string>;
11
- }
12
6
  export interface Components {
13
7
  [key: string]: any;
14
8
  }
@@ -123,4 +117,3 @@ export declare class Context {
123
117
  */
124
118
  dispose(): Promise<void>;
125
119
  }
126
- export {};
package/dist/core.d.ts CHANGED
@@ -9,12 +9,25 @@ import { HookHandler, Hook } from './hook.js';
9
9
  import { Server as CoreServer } from './server.js';
10
10
  import { I18n } from './i18n.js';
11
11
  import { IRenderer } from '@yumerijs/types';
12
- interface Plugin {
13
- apply: (ctx: Context, config: Config) => Promise<void>;
14
- disable: (ctx: Context) => Promise<void>;
15
- depend: Array<string>;
16
- provide: Array<string>;
12
+ export interface Plugin {
13
+ apply?: (ctx: Context, config: Config) => Promise<void> | void;
14
+ disable?: (ctx: Context) => Promise<void> | void;
15
+ depend?: Array<string>;
16
+ provide?: Array<string>;
17
+ render?: string;
18
+ config?: Schema<any>;
17
19
  }
20
+ type PluginConstructor = new (ctx: Context, config: Config) => Plugin;
21
+ type PluginModuleLike = Plugin | PluginConstructor | ((ctx: Context, config: Config) => Promise<void> | void) | {
22
+ default?: Plugin | PluginConstructor | ((ctx: Context, config: Config) => Promise<void> | void);
23
+ apply?: ((ctx: Context, config: Config) => Promise<void> | void);
24
+ disable?: (ctx: Context) => Promise<void> | void;
25
+ depend?: Array<string>;
26
+ provide?: Array<string>;
27
+ render?: string;
28
+ config?: Schema<any>;
29
+ };
30
+ export declare function resolvePluginModule(module: PluginModuleLike, context: Context, config: Config): Plugin;
18
31
  export interface CoreOptions {
19
32
  port?: number;
20
33
  host?: string;
@@ -50,7 +63,7 @@ export declare class Core {
50
63
  getRendererForPlugin(pluginName: string): string | undefined;
51
64
  runCore(): Promise<void>;
52
65
  getShortPluginName(pluginName: string): string;
53
- plugin(module: Plugin, context: Context, config: Config): Promise<void>;
66
+ plugin(module: PluginModuleLike, context: Context, config: Config): Promise<void>;
54
67
  registerComponent(name: string, component: any): void;
55
68
  getComponent(name: string): any;
56
69
  unregisterComponent(name: string): void;
package/dist/core.js CHANGED
@@ -6,6 +6,48 @@ import { Hook } from './hook.js';
6
6
  import { Server as CoreServer } from './server.js';
7
7
  import * as fs from 'fs';
8
8
  const version = JSON.parse(await fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf-8')).version;
9
+ function isClassPlugin(value) {
10
+ if (typeof value !== 'function')
11
+ return false;
12
+ const source = Function.prototype.toString.call(value);
13
+ return source.startsWith('class ');
14
+ }
15
+ function mergePluginMeta(target, source) {
16
+ if (!source || typeof source !== 'object')
17
+ return target;
18
+ if (target.depend == null && Array.isArray(source.depend))
19
+ target.depend = source.depend;
20
+ if (target.provide == null && Array.isArray(source.provide))
21
+ target.provide = source.provide;
22
+ if (target.render == null && typeof source.render === 'string')
23
+ target.render = source.render;
24
+ if (target.config == null && source.config)
25
+ target.config = source.config;
26
+ if (target.disable == null && typeof source.disable === 'function')
27
+ target.disable = source.disable.bind(source);
28
+ return target;
29
+ }
30
+ export function resolvePluginModule(module, context, config) {
31
+ const candidate = module?.default ?? module?.apply ?? module;
32
+ let plugin;
33
+ if (isClassPlugin(candidate)) {
34
+ plugin = new candidate(context, config);
35
+ }
36
+ else if (typeof candidate === 'function') {
37
+ plugin = { apply: candidate };
38
+ }
39
+ else if (candidate && typeof candidate === 'object') {
40
+ plugin = candidate;
41
+ }
42
+ else {
43
+ throw new TypeError('Invalid plugin module. Expected class, function, or plugin object.');
44
+ }
45
+ mergePluginMeta(plugin, module);
46
+ if (candidate !== module) {
47
+ mergePluginMeta(plugin, candidate);
48
+ }
49
+ return plugin;
50
+ }
9
51
  export const coreConfigSchema = Schema.object({
10
52
  port: Schema.number('监听端口').default(14510),
11
53
  host: Schema.string('监听地址').default('0.0.0.0'),
@@ -82,10 +124,11 @@ export class Core {
82
124
  return pluginName;
83
125
  }
84
126
  async plugin(module, context, config) {
127
+ const plugin = resolvePluginModule(module, context, config);
85
128
  const shortName = this.getShortPluginName(context.pluginname);
86
- context.module = module;
129
+ context.module = plugin;
87
130
  // 自动依赖注入
88
- const depend = module.depend || [];
131
+ const depend = plugin.depend || [];
89
132
  for (const name of depend) {
90
133
  const component = this.getComponent(name);
91
134
  if (component) {
@@ -93,8 +136,8 @@ export class Core {
93
136
  }
94
137
  }
95
138
  this.logger.info(`apply plugin ${shortName}`);
96
- if (module.apply) {
97
- await module.apply(context, config);
139
+ if (plugin.apply) {
140
+ await plugin.apply(context, config);
98
141
  }
99
142
  }
100
143
  registerComponent(name, component) {
package/dist/route.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { WebSocketServer } from 'ws';
2
+ import { isIP } from 'node:net';
2
3
  function parsePatternToSegments(pattern) {
3
4
  // Normalize: remove leading/trailing slashes for consistent splitting
4
5
  const norm = pattern.replace(/^\/+|\/+$/g, '');
@@ -22,22 +23,195 @@ function parsePatternToSegments(pattern) {
22
23
  }
23
24
  return { segments, params };
24
25
  }
25
- function parseHostToSegments(hostPattern) {
26
- // 移除可能的空格,按 . 分割
27
- const parts = hostPattern.split('.');
28
- return parts.map(part => {
29
- // 简单的参数解析逻辑
30
- if (part.startsWith(':')) {
31
- const lastChar = part[part.length - 1];
32
- const hasModifier = ['?', '+', '*'].includes(lastChar);
33
- return {
34
- type: 'param',
35
- name: hasModifier ? part.slice(1, -1) : part.slice(1),
36
- modifier: hasModifier ? lastChar : '',
37
- };
38
- }
26
+ function parseSegmentToken(part) {
27
+ if (!part.startsWith(':')) {
39
28
  return { type: 'static', value: part };
40
- });
29
+ }
30
+ let name = part.slice(1);
31
+ let modifier = '';
32
+ const lastChar = name[name.length - 1];
33
+ if (lastChar === '?' || lastChar === '*' || lastChar === '+') {
34
+ modifier = lastChar;
35
+ name = name.slice(0, -1);
36
+ }
37
+ return { type: 'param', name, modifier };
38
+ }
39
+ function splitHostAndPortPattern(pattern) {
40
+ const normalized = pattern.trim().toLowerCase();
41
+ if (normalized.startsWith('[')) {
42
+ const end = normalized.indexOf(']');
43
+ if (end === -1)
44
+ return { hostPart: normalized };
45
+ const hostPart = normalized.slice(1, end);
46
+ const rest = normalized.slice(end + 1);
47
+ if (rest.startsWith(':')) {
48
+ return { hostPart, portPart: rest.slice(1) };
49
+ }
50
+ return { hostPart };
51
+ }
52
+ const colonCount = (normalized.match(/:/g) || []).length;
53
+ const lastColon = normalized.lastIndexOf(':');
54
+ const hasDots = normalized.includes('.');
55
+ if (lastColon === -1) {
56
+ return { hostPart: normalized };
57
+ }
58
+ const suffix = normalized.slice(lastColon + 1);
59
+ const prefix = normalized.slice(0, lastColon);
60
+ const isPortToken = /^\d+$/.test(suffix) || /^:[a-z_][a-z0-9_]*[?*+]?$/.test(`:${suffix}`);
61
+ if (!isPortToken) {
62
+ return { hostPart: normalized };
63
+ }
64
+ if (normalized.startsWith(':') && !hasDots) {
65
+ const paramBody = prefix.slice(1);
66
+ if (/^[a-z_][a-z0-9_]*$/.test(paramBody)) {
67
+ return { hostPart: prefix, portPart: suffix };
68
+ }
69
+ }
70
+ if (colonCount === 1 && !normalized.startsWith(':')) {
71
+ return { hostPart: prefix, portPart: suffix };
72
+ }
73
+ if (hasDots) {
74
+ return { hostPart: prefix, portPart: suffix };
75
+ }
76
+ return { hostPart: normalized };
77
+ }
78
+ function parseHostPattern(hostPattern) {
79
+ const { hostPart, portPart } = splitHostAndPortPattern(hostPattern);
80
+ const matchWholeHost = !hostPart.includes('.') || hostPart.includes(':');
81
+ const rawParts = matchWholeHost ? [hostPart] : hostPart.split('.');
82
+ return {
83
+ hostSegments: rawParts.map(parseSegmentToken),
84
+ portSegment: portPart ? parseSegmentToken(portPart) : undefined,
85
+ matchWholeHost,
86
+ };
87
+ }
88
+ function parseAuthority(host) {
89
+ const normalized = host.trim().toLowerCase();
90
+ if (normalized.startsWith('[')) {
91
+ const end = normalized.indexOf(']');
92
+ if (end !== -1) {
93
+ const hostname = normalized.slice(1, end);
94
+ const rest = normalized.slice(end + 1);
95
+ const port = rest.startsWith(':') ? rest.slice(1) : undefined;
96
+ return { hostname, port, kind: 'ipv6' };
97
+ }
98
+ }
99
+ if (isIP(normalized) === 6) {
100
+ return { hostname: normalized, kind: 'ipv6' };
101
+ }
102
+ const lastColon = normalized.lastIndexOf(':');
103
+ if (lastColon !== -1 && normalized.indexOf(':') === lastColon) {
104
+ const maybePort = normalized.slice(lastColon + 1);
105
+ if (/^\d+$/.test(maybePort)) {
106
+ const hostname = normalized.slice(0, lastColon);
107
+ if (isIP(hostname) === 4) {
108
+ return { hostname, port: maybePort, kind: 'ipv4' };
109
+ }
110
+ return { hostname, port: maybePort, kind: 'domain' };
111
+ }
112
+ }
113
+ if (isIP(normalized) === 4) {
114
+ return { hostname: normalized, kind: 'ipv4' };
115
+ }
116
+ return { hostname: normalized, kind: 'domain' };
117
+ }
118
+ function matchSingleSegment(seg, value, params) {
119
+ if (seg.type === 'static') {
120
+ return value === seg.value;
121
+ }
122
+ switch (seg.modifier) {
123
+ case '':
124
+ case '+':
125
+ case '*':
126
+ if (value == null || value === '') {
127
+ if (seg.modifier === '*') {
128
+ params[seg.name] = undefined;
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+ params[seg.name] = value;
134
+ return true;
135
+ case '?':
136
+ params[seg.name] = value || undefined;
137
+ return true;
138
+ default:
139
+ return false;
140
+ }
141
+ }
142
+ function matchHostSegments(segments, parts, joiner, params) {
143
+ let hi = 0;
144
+ let hj = 0;
145
+ while (hi < segments.length) {
146
+ const seg = segments[hi];
147
+ const nextSegIsStatic = !!segments[hi + 1] && segments[hi + 1].type === 'static';
148
+ if (seg.type === 'static') {
149
+ if (hj >= parts.length || parts[hj] !== seg.value)
150
+ return false;
151
+ hi++;
152
+ hj++;
153
+ continue;
154
+ }
155
+ switch (seg.modifier) {
156
+ case '':
157
+ if (hj >= parts.length)
158
+ return false;
159
+ params[seg.name] = parts[hj];
160
+ hi++;
161
+ hj++;
162
+ break;
163
+ case '?':
164
+ if (hj < parts.length) {
165
+ if (nextSegIsStatic && parts[hj] === segments[hi + 1].value) {
166
+ params[seg.name] = undefined;
167
+ hi++;
168
+ }
169
+ else {
170
+ params[seg.name] = parts[hj];
171
+ hi++;
172
+ hj++;
173
+ }
174
+ }
175
+ else {
176
+ params[seg.name] = undefined;
177
+ hi++;
178
+ }
179
+ break;
180
+ case '+': {
181
+ if (hj >= parts.length)
182
+ return false;
183
+ let end = parts.length;
184
+ if (nextSegIsStatic) {
185
+ const nextStatic = segments[hi + 1].value;
186
+ const found = parts.indexOf(nextStatic, hj);
187
+ if (found === -1 || found === hj)
188
+ return false;
189
+ end = found;
190
+ }
191
+ params[seg.name] = parts.slice(hj, end).join(joiner);
192
+ hj = end;
193
+ hi++;
194
+ break;
195
+ }
196
+ case '*': {
197
+ let end = parts.length;
198
+ if (nextSegIsStatic) {
199
+ const nextStatic = segments[hi + 1].value;
200
+ const found = parts.indexOf(nextStatic, hj);
201
+ if (found !== -1)
202
+ end = found;
203
+ }
204
+ const value = parts.slice(hj, end).join(joiner);
205
+ params[seg.name] = value || undefined;
206
+ hj = end;
207
+ hi++;
208
+ break;
209
+ }
210
+ default:
211
+ return false;
212
+ }
213
+ }
214
+ return hj === parts.length;
41
215
  }
42
216
  /**
43
217
  * Route class using segment-based matching (no fragile capture-group-index mapping).
@@ -113,102 +287,26 @@ export class Route {
113
287
  if (this.routehost && this.routehost.length > 0) {
114
288
  if (!host)
115
289
  return null; // 如果设置了 host 限制但没有传入 host,直接失败
116
- const actualHost = host.toLowerCase().split(':')[0]; // 移除端口并转小写
117
- const hostParts = actualHost.split('.');
290
+ const actual = parseAuthority(host);
118
291
  let hostMatched = false;
119
292
  for (const pattern of this.routehost) {
120
- const hSegments = parseHostToSegments(pattern); // 这里的 parsePattern 应该和你 path 解析逻辑一致
121
- let hi = 0; // pattern segment index
122
- let hj = 0; // actual host parts index
293
+ const parsedPattern = parseHostPattern(pattern);
294
+ const hostParts = parsedPattern.matchWholeHost || actual.kind === 'ipv6'
295
+ ? [actual.hostname]
296
+ : actual.hostname.split('.');
297
+ const joiner = parsedPattern.matchWholeHost || actual.kind === 'ipv6' ? ':' : '.';
123
298
  let tempParams = {};
124
- let possible = true;
125
- while (hi < hSegments.length) {
126
- const seg = hSegments[hi];
127
- const nextSegIsStatic = !!hSegments[hi + 1] && hSegments[hi + 1].type === 'static';
128
- if (seg.type === 'static') {
129
- if (hj >= hostParts.length || hostParts[hj] !== seg.value.toLowerCase()) {
130
- possible = false;
131
- break;
132
- }
133
- hi++;
134
- hj++;
135
- }
136
- else {
137
- // 参数匹配逻辑 (:, ?, +, *)
138
- switch (seg.modifier) {
139
- case '': // required single
140
- if (hj >= hostParts.length) {
141
- possible = false;
142
- break;
143
- }
144
- tempParams[seg.name] = hostParts[hj];
145
- hi++;
146
- hj++;
147
- break;
148
- case '?': // optional single
149
- if (hj < hostParts.length) {
150
- if (nextSegIsStatic && hostParts[hj] === hSegments[hi + 1].value.toLowerCase()) {
151
- tempParams[seg.name] = undefined;
152
- hi++;
153
- }
154
- else {
155
- tempParams[seg.name] = hostParts[hj];
156
- hj++;
157
- hi++;
158
- }
159
- }
160
- else {
161
- tempParams[seg.name] = undefined;
162
- hi++;
163
- }
164
- break;
165
- case '+': // required multi
166
- if (hj >= hostParts.length) {
167
- possible = false;
168
- break;
169
- }
170
- let endP = hostParts.length;
171
- if (nextSegIsStatic) {
172
- let found = hostParts.indexOf(hSegments[hi + 1].value.toLowerCase(), hj);
173
- if (found === -1) {
174
- possible = false;
175
- break;
176
- }
177
- endP = found;
178
- }
179
- if (endP === hj) {
180
- possible = false;
181
- break;
182
- }
183
- tempParams[seg.name] = hostParts.slice(hj, endP).join('.');
184
- hj = endP;
185
- hi++;
186
- break;
187
- case '*': // optional multi
188
- let endS = hostParts.length;
189
- if (nextSegIsStatic) {
190
- let found = hostParts.indexOf(hSegments[hi + 1].value.toLowerCase(), hj);
191
- if (found !== -1)
192
- endS = found;
193
- }
194
- const val = hostParts.slice(hj, endS).join('.');
195
- tempParams[seg.name] = val === '' ? undefined : val;
196
- hj = endS;
197
- hi++;
198
- break;
199
- default:
200
- possible = false;
201
- break;
202
- }
203
- if (!possible)
204
- break;
205
- }
206
- }
207
- if (possible && hj === hostParts.length) {
208
- hostMatched = true;
209
- hostParams = tempParams;
210
- break; // 只要匹配到一个 host pattern 就行
299
+ const hostOk = matchHostSegments(parsedPattern.hostSegments, hostParts, joiner, tempParams);
300
+ if (!hostOk)
301
+ continue;
302
+ if (parsedPattern.portSegment) {
303
+ const portOk = matchSingleSegment(parsedPattern.portSegment, actual.port, tempParams);
304
+ if (!portOk)
305
+ continue;
211
306
  }
307
+ hostMatched = true;
308
+ hostParams = tempParams;
309
+ break; // 只要匹配到一个 host pattern 就行
212
310
  }
213
311
  if (!hostMatched)
214
312
  return null;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@yumerijs/core",
3
- "version": "3.0.0-alpha.1",
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
  ],