@dangao/bun-server 2.3.0 → 3.0.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.
Files changed (147) hide show
  1. package/README.md +81 -3
  2. package/dist/auth/jwt.d.ts.map +1 -1
  3. package/dist/config/service.d.ts +0 -1
  4. package/dist/config/service.d.ts.map +1 -1
  5. package/dist/core/application.d.ts +13 -0
  6. package/dist/core/application.d.ts.map +1 -1
  7. package/dist/core/cluster.d.ts.map +1 -1
  8. package/dist/core/server.d.ts +12 -9
  9. package/dist/core/server.d.ts.map +1 -1
  10. package/dist/dashboard/controller.d.ts.map +1 -1
  11. package/dist/database/connection-pool.d.ts +3 -3
  12. package/dist/database/connection-pool.d.ts.map +1 -1
  13. package/dist/database/sql-manager.d.ts +8 -4
  14. package/dist/database/sql-manager.d.ts.map +1 -1
  15. package/dist/database/sqlite-adapter.d.ts +7 -3
  16. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  17. package/dist/debug/recorder.d.ts +0 -1
  18. package/dist/debug/recorder.d.ts.map +1 -1
  19. package/dist/files/static-middleware.d.ts.map +1 -1
  20. package/dist/files/storage.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +40264 -3542
  24. package/dist/index.node.mjs +17689 -0
  25. package/dist/middleware/builtin/static-file.d.ts +4 -2
  26. package/dist/middleware/builtin/static-file.d.ts.map +1 -1
  27. package/dist/platform/bun/crypto.d.ts +3 -0
  28. package/dist/platform/bun/crypto.d.ts.map +1 -0
  29. package/dist/platform/bun/fs.d.ts +3 -0
  30. package/dist/platform/bun/fs.d.ts.map +1 -0
  31. package/dist/platform/bun/http.d.ts +15 -0
  32. package/dist/platform/bun/http.d.ts.map +1 -0
  33. package/dist/platform/bun/index.d.ts +3 -0
  34. package/dist/platform/bun/index.d.ts.map +1 -0
  35. package/dist/platform/bun/parser.d.ts +3 -0
  36. package/dist/platform/bun/parser.d.ts.map +1 -0
  37. package/dist/platform/bun/process.d.ts +3 -0
  38. package/dist/platform/bun/process.d.ts.map +1 -0
  39. package/dist/platform/detector.d.ts +9 -0
  40. package/dist/platform/detector.d.ts.map +1 -0
  41. package/dist/platform/index.d.ts +4 -0
  42. package/dist/platform/index.d.ts.map +1 -0
  43. package/dist/platform/node/crypto.d.ts +3 -0
  44. package/dist/platform/node/crypto.d.ts.map +1 -0
  45. package/dist/platform/node/fs.d.ts +3 -0
  46. package/dist/platform/node/fs.d.ts.map +1 -0
  47. package/dist/platform/node/http.d.ts +3 -0
  48. package/dist/platform/node/http.d.ts.map +1 -0
  49. package/dist/platform/node/index.d.ts +3 -0
  50. package/dist/platform/node/index.d.ts.map +1 -0
  51. package/dist/platform/node/parser.d.ts +3 -0
  52. package/dist/platform/node/parser.d.ts.map +1 -0
  53. package/dist/platform/node/process.d.ts +3 -0
  54. package/dist/platform/node/process.d.ts.map +1 -0
  55. package/dist/platform/runtime.d.ts +14 -0
  56. package/dist/platform/runtime.d.ts.map +1 -0
  57. package/dist/platform/types.d.ts +139 -0
  58. package/dist/platform/types.d.ts.map +1 -0
  59. package/dist/prompt/stores/file-store.d.ts.map +1 -1
  60. package/dist/rag/service.d.ts.map +1 -1
  61. package/dist/request/response.d.ts +3 -1
  62. package/dist/request/response.d.ts.map +1 -1
  63. package/dist/security/guards/execution-context.d.ts +2 -2
  64. package/dist/security/guards/execution-context.d.ts.map +1 -1
  65. package/dist/security/guards/types.d.ts +2 -2
  66. package/dist/security/guards/types.d.ts.map +1 -1
  67. package/dist/swagger/generator.d.ts.map +1 -1
  68. package/dist/websocket/registry.d.ts +4 -4
  69. package/dist/websocket/registry.d.ts.map +1 -1
  70. package/docs/deployment.md +31 -7
  71. package/docs/design/query-interceptor-design.md +381 -0
  72. package/docs/idle-timeout.md +6 -4
  73. package/docs/migration.md +43 -0
  74. package/docs/platform.md +299 -0
  75. package/docs/testing.md +60 -0
  76. package/docs/zh/deployment.md +30 -7
  77. package/docs/zh/idle-timeout.md +6 -4
  78. package/docs/zh/migration.md +42 -0
  79. package/docs/zh/platform.md +299 -0
  80. package/docs/zh/testing.md +60 -0
  81. package/package.json +24 -6
  82. package/src/auth/jwt.ts +4 -3
  83. package/src/config/service.ts +7 -6
  84. package/src/core/application.ts +19 -1
  85. package/src/core/cluster.ts +16 -14
  86. package/src/core/server.ts +48 -35
  87. package/src/dashboard/controller.ts +3 -2
  88. package/src/database/connection-pool.ts +32 -20
  89. package/src/database/database-module.ts +1 -1
  90. package/src/database/db-proxy.ts +2 -2
  91. package/src/database/orm/transaction-manager.ts +1 -1
  92. package/src/database/sql-manager.ts +48 -13
  93. package/src/database/sqlite-adapter.ts +45 -12
  94. package/src/debug/recorder.ts +4 -3
  95. package/src/files/static-middleware.ts +3 -2
  96. package/src/files/storage.ts +2 -1
  97. package/src/index.ts +13 -0
  98. package/src/middleware/builtin/static-file.ts +8 -5
  99. package/src/platform/bun/crypto.ts +30 -0
  100. package/src/platform/bun/fs.ts +52 -0
  101. package/src/platform/bun/http.ts +106 -0
  102. package/src/platform/bun/index.ts +17 -0
  103. package/src/platform/bun/parser.ts +19 -0
  104. package/src/platform/bun/process.ts +37 -0
  105. package/src/platform/detector.ts +36 -0
  106. package/src/platform/index.ts +20 -0
  107. package/src/platform/node/crypto.ts +40 -0
  108. package/src/platform/node/fs.ts +115 -0
  109. package/src/platform/node/http.ts +196 -0
  110. package/src/platform/node/index.ts +17 -0
  111. package/src/platform/node/parser.ts +34 -0
  112. package/src/platform/node/process.ts +51 -0
  113. package/src/platform/runtime.ts +50 -0
  114. package/src/platform/types.ts +150 -0
  115. package/src/prompt/stores/file-store.ts +6 -5
  116. package/src/rag/service.ts +2 -1
  117. package/src/request/response.ts +7 -4
  118. package/src/security/guards/execution-context.ts +4 -4
  119. package/src/security/guards/types.ts +2 -2
  120. package/src/swagger/generator.ts +2 -1
  121. package/src/websocket/registry.ts +6 -7
  122. package/tests/controller/path-combination.test.ts +196 -2
  123. package/tests/files/static-middleware.test.ts +5 -2
  124. package/tests/middleware/static-file.test.ts +5 -2
  125. package/tests/platform/bun/crypto.test.ts +8 -0
  126. package/tests/platform/bun/database.test.ts +8 -0
  127. package/tests/platform/bun/fs.test.ts +8 -0
  128. package/tests/platform/bun/parser.test.ts +8 -0
  129. package/tests/platform/bun/process.test.ts +8 -0
  130. package/tests/platform/bun/websocket.test.ts +8 -0
  131. package/tests/platform/detector.test.ts +57 -0
  132. package/tests/platform/node/build-smoke.test.ts +92 -0
  133. package/tests/platform/node/crypto.test.ts +9 -0
  134. package/tests/platform/node/database.test.ts +9 -0
  135. package/tests/platform/node/fs.test.ts +9 -0
  136. package/tests/platform/node/parser.test.ts +9 -0
  137. package/tests/platform/node/process.test.ts +9 -0
  138. package/tests/platform/node/websocket.test.ts +9 -0
  139. package/tests/platform/shared/crypto.cases.ts +49 -0
  140. package/tests/platform/shared/database.cases.ts +43 -0
  141. package/tests/platform/shared/fs.cases.ts +82 -0
  142. package/tests/platform/shared/parser.cases.ts +55 -0
  143. package/tests/platform/shared/process.cases.ts +26 -0
  144. package/tests/platform/shared/suite.ts +33 -0
  145. package/tests/platform/shared/websocket.cases.ts +61 -0
  146. package/tests/request/response.test.ts +5 -2
  147. package/tests/router/router-extended.test.ts +53 -0
@@ -0,0 +1,51 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import type { IProcessAdapter, IChildProcess, SpawnOptions } from '../types';
3
+
4
+ class NodeChildProcess implements IChildProcess {
5
+ private readonly child: ReturnType<typeof nodeSpawn>;
6
+ private readonly _exited: Promise<number | null>;
7
+
8
+ public constructor(child: ReturnType<typeof nodeSpawn>) {
9
+ this.child = child;
10
+ this._exited = new Promise<number | null>((resolve) => {
11
+ child.on('exit', (code) => resolve(code));
12
+ });
13
+ }
14
+
15
+ public get pid(): number {
16
+ return this.child.pid ?? -1;
17
+ }
18
+
19
+ public get exited(): Promise<number | null> {
20
+ return this._exited;
21
+ }
22
+
23
+ public kill(signal?: string | number): void {
24
+ if (typeof signal === 'string') {
25
+ this.child.kill(signal as NodeJS.Signals);
26
+ } else if (typeof signal === 'number') {
27
+ this.child.kill(signal);
28
+ } else {
29
+ this.child.kill('SIGTERM');
30
+ }
31
+ }
32
+ }
33
+
34
+ export const nodeProcessAdapter: IProcessAdapter = {
35
+ spawn(options: SpawnOptions): IChildProcess {
36
+ const [cmd, ...args] = options.cmd;
37
+ const child = nodeSpawn(cmd!, args, {
38
+ env: (options.env ?? process.env) as Record<string, string>,
39
+ stdio: [
40
+ 'ignore',
41
+ options.stdout === 'pipe' ? 'pipe' : options.stdout === 'ignore' ? 'ignore' : 'inherit',
42
+ options.stderr === 'pipe' ? 'pipe' : options.stderr === 'ignore' ? 'ignore' : 'inherit',
43
+ ],
44
+ });
45
+ return new NodeChildProcess(child);
46
+ },
47
+
48
+ async sleep(ms: number): Promise<void> {
49
+ await new Promise<void>((resolve) => setTimeout(resolve, ms));
50
+ },
51
+ };
@@ -0,0 +1,50 @@
1
+ import type { IPlatform, PlatformEngine } from './types';
2
+ import { resolvePlatform } from './detector';
3
+
4
+ let _runtime: IPlatform | null = null;
5
+
6
+ /**
7
+ * 初始化全局运行时平台
8
+ * 应在 Application 构造函数中最先调用
9
+ *
10
+ * @param engine - 显式指定平台(优先级最高),省略则走优先级检测链
11
+ */
12
+ export function initRuntime(engine?: PlatformEngine): void {
13
+ const resolved = resolvePlatform(engine);
14
+
15
+ if (_runtime && _runtime.engine === resolved) {
16
+ return;
17
+ }
18
+
19
+ if (resolved === 'bun') {
20
+ // 动态导入避免 Node.js 环境下引入 Bun 专属 API
21
+ const { createBunPlatform } = require('./bun/index');
22
+ _runtime = createBunPlatform();
23
+ } else {
24
+ const { createNodePlatform } = require('./node/index');
25
+ _runtime = createNodePlatform();
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 获取当前已初始化的平台运行时
31
+ * 必须在 initRuntime() 调用之后使用
32
+ */
33
+ export function getRuntime(): IPlatform {
34
+ if (!_runtime) {
35
+ throw new Error(
36
+ '[Platform] Runtime not initialized. ' +
37
+ 'Make sure initRuntime() is called before using getRuntime(). ' +
38
+ 'This is automatically done by new Application(options).',
39
+ );
40
+ }
41
+ return _runtime;
42
+ }
43
+
44
+ /**
45
+ * 重置运行时(仅用于测试)
46
+ * @internal
47
+ */
48
+ export function _resetRuntime(): void {
49
+ _runtime = null;
50
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * 支持的运行时平台标识
3
+ */
4
+ export type PlatformEngine = 'bun' | 'node';
5
+
6
+ /**
7
+ * 文件引用抽象接口(替代 BunFile 的直接使用)
8
+ */
9
+ export interface IFileRef {
10
+ readonly type: string;
11
+ readonly size: number;
12
+ text(): Promise<string>;
13
+ bytes(): Promise<Uint8Array>;
14
+ arrayBuffer(): Promise<ArrayBuffer>;
15
+ exists(): Promise<boolean>;
16
+ /** 返回可读流,用于 new Response(file.stream(), ...) */
17
+ stream(): ReadableStream<Uint8Array>;
18
+ }
19
+
20
+ /**
21
+ * 文件系统适配器接口
22
+ */
23
+ export interface IFsAdapter {
24
+ /** 获取文件引用 */
25
+ file(path: string): IFileRef;
26
+ /** 写入文件 */
27
+ write(path: string, data: string | Uint8Array | ArrayBuffer): Promise<void>;
28
+ /** 匹配文件列表 */
29
+ glob(pattern: string, cwd?: string): string[];
30
+ }
31
+
32
+ /**
33
+ * 哈希器接口(替代 Bun.CryptoHasher)
34
+ */
35
+ export interface IHasher {
36
+ update(data: string | Uint8Array | ArrayBuffer): IHasher;
37
+ digest(): Uint8Array;
38
+ digest(encoding: 'hex' | 'base64' | 'base64url'): string;
39
+ }
40
+
41
+ /**
42
+ * 加密适配器接口
43
+ */
44
+ export interface ICryptoAdapter {
45
+ createHasher(algorithm: string): IHasher;
46
+ }
47
+
48
+ /**
49
+ * 解析器适配器接口
50
+ */
51
+ export interface IParserAdapter {
52
+ parseJSONC(content: string): unknown;
53
+ parseJSON5(content: string): unknown;
54
+ parseJSONL(content: string): unknown[];
55
+ renderMarkdown(md: string): string;
56
+ }
57
+
58
+ /**
59
+ * 子进程接口
60
+ */
61
+ export interface IChildProcess {
62
+ readonly pid: number;
63
+ readonly exited: Promise<number | null>;
64
+ kill(signal?: string | number): void;
65
+ }
66
+
67
+ /**
68
+ * spawn 选项
69
+ */
70
+ export interface SpawnOptions {
71
+ cmd: string[];
72
+ env?: Record<string, string | undefined>;
73
+ stdout?: 'inherit' | 'pipe' | 'ignore';
74
+ stderr?: 'inherit' | 'pipe' | 'ignore';
75
+ }
76
+
77
+ /**
78
+ * 进程适配器接口
79
+ */
80
+ export interface IProcessAdapter {
81
+ spawn(options: SpawnOptions): IChildProcess;
82
+ sleep(ms: number): Promise<void>;
83
+ }
84
+
85
+ /**
86
+ * WebSocket 连接抽象接口(替代 Bun.ServerWebSocket)
87
+ */
88
+ export interface IWebSocket<T = unknown> {
89
+ readonly data: T;
90
+ readonly readyState: number;
91
+ send(data: string | Buffer | Uint8Array): void;
92
+ close(code?: number, reason?: string): void;
93
+ }
94
+
95
+ /**
96
+ * WebSocket 事件处理器集合
97
+ */
98
+ export interface WebSocketHandlers<T = unknown> {
99
+ open?: (ws: IWebSocket<T>) => void | Promise<void>;
100
+ message?: (ws: IWebSocket<T>, msg: string | Buffer) => void | Promise<void>;
101
+ close?: (ws: IWebSocket<T>, code: number, reason: string) => void | Promise<void>;
102
+ }
103
+
104
+ /**
105
+ * HTTP 服务启动选项
106
+ */
107
+ export interface HttpServeOptions<T = unknown> {
108
+ port?: number;
109
+ hostname?: string;
110
+ reusePort?: boolean;
111
+ idleTimeout?: number;
112
+ /** Unix socket 路径(cluster proxy 模式使用) */
113
+ unix?: string;
114
+ fetch: (request: Request, server: IServerHandle) => Response | Promise<Response | undefined> | undefined;
115
+ websocket?: WebSocketHandlers<T>;
116
+ }
117
+
118
+ /**
119
+ * 服务器句柄接口(替代 Bun.Server)
120
+ */
121
+ export interface IServerHandle {
122
+ readonly port: number;
123
+ readonly hostname?: string;
124
+ stop(): void;
125
+ /** 升级 WebSocket 连接(Bun 原生升级) */
126
+ upgrade?(request: Request, options?: { data?: unknown }): boolean;
127
+ /** 设置连接超时(Bun 专属) */
128
+ timeout?(request: Request, seconds: number): void;
129
+ /** 获取底层原生服务器实例(不推荐,类型为 unknown) */
130
+ getNative(): unknown;
131
+ }
132
+
133
+ /**
134
+ * HTTP 驱动适配器接口
135
+ */
136
+ export interface IHttpDriver {
137
+ serve<T = unknown>(options: HttpServeOptions<T>): Promise<IServerHandle>;
138
+ }
139
+
140
+ /**
141
+ * 平台接口聚合
142
+ */
143
+ export interface IPlatform {
144
+ readonly engine: PlatformEngine;
145
+ readonly fs: IFsAdapter;
146
+ readonly crypto: ICryptoAdapter;
147
+ readonly parser: IParserAdapter;
148
+ readonly process: IProcessAdapter;
149
+ readonly http: IHttpDriver;
150
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  } from '../types';
7
7
  import { extractVariables } from '../types';
8
8
  import { InMemoryPromptStore } from './memory-store';
9
+ import { getRuntime } from '../../platform/runtime';
9
10
 
10
11
  export interface FilePromptStoreConfig {
11
12
  /** Directory containing JSON prompt files (default: './.prompts') */
@@ -73,7 +74,7 @@ export class FilePromptStore implements PromptStore {
73
74
  if (deleted) {
74
75
  try {
75
76
  const path = `${this.promptsDir}/${id}.json`;
76
- await Bun.file(path).exists() && Bun.write(path, ''); // Soft delete (empty file)
77
+ await getRuntime().fs.file(path).exists() && getRuntime().fs.write(path, ''); // Soft delete (empty file)
77
78
  } catch (_error) {
78
79
  // ignore
79
80
  }
@@ -86,12 +87,12 @@ export class FilePromptStore implements PromptStore {
86
87
  this.loaded = true;
87
88
 
88
89
  try {
89
- const glob = new Bun.Glob('*.json');
90
- const files = Array.from(glob.scanSync(this.promptsDir));
90
+ const runtime = getRuntime();
91
+ const files = runtime.fs.glob('*.json', this.promptsDir);
91
92
 
92
93
  for (const file of files) {
93
94
  try {
94
- const content = await Bun.file(`${this.promptsDir}/${file}`).text();
95
+ const content = await runtime.fs.file(`${this.promptsDir}/${file}`).text();
95
96
  if (!content.trim()) continue;
96
97
 
97
98
  const data = JSON.parse(content) as {
@@ -127,7 +128,7 @@ export class FilePromptStore implements PromptStore {
127
128
  null,
128
129
  2,
129
130
  );
130
- await Bun.write(`${this.promptsDir}/${template.id}.json`, content);
131
+ await getRuntime().fs.write(`${this.promptsDir}/${template.id}.json`, content);
131
132
  } catch (_error) {
132
133
  // Ignore write errors (e.g., read-only filesystem)
133
134
  }
@@ -1,5 +1,6 @@
1
1
  import { Injectable } from '../di/decorators';
2
2
  import { Inject } from '../di/decorators';
3
+ import { getRuntime } from '../platform/runtime';
3
4
  import type { EmbeddingService } from '../embedding/service';
4
5
  import { EMBEDDING_SERVICE_TOKEN } from '../embedding/types';
5
6
  import type { VectorStore } from '../vector-store/types';
@@ -120,7 +121,7 @@ export class RagService {
120
121
  return source.content;
121
122
 
122
123
  case 'file': {
123
- const file = Bun.file(source.path);
124
+ const file = getRuntime().fs.file(source.path);
124
125
  return file.text();
125
126
  }
126
127
 
@@ -1,4 +1,7 @@
1
- import type { BodyInit, HeadersInit } from 'bun';
1
+ import { getRuntime } from '../platform/runtime';
2
+
3
+ /** Cross-runtime compatible headers input type */
4
+ type HeadersInit = Headers | string[][] | Record<string, string>;
2
5
 
3
6
  /**
4
7
  * 响应封装类
@@ -131,17 +134,17 @@ export class ResponseBuilder {
131
134
  }
132
135
 
133
136
  if (typeof source === 'string') {
134
- const file = Bun.file(source);
137
+ const file = getRuntime().fs.file(source);
135
138
  if (!headers.has('Content-Type') && file.type) {
136
139
  headers.set('Content-Type', file.type);
137
140
  }
138
- return new Response(file, {
141
+ return new Response(file.stream(), {
139
142
  status: options.status ?? 200,
140
143
  headers,
141
144
  });
142
145
  }
143
146
 
144
- return new Response(source as BodyInit, {
147
+ return new Response(source as any, {
145
148
  status: options.status ?? 200,
146
149
  headers,
147
150
  });
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata';
2
- import type { ServerWebSocket } from 'bun';
2
+ import type { IWebSocket } from '../../platform/types';
3
3
  import type { Context } from '../../core/context';
4
4
  import type { ResponseBuilder } from '../../request/response';
5
5
  import type { Constructor } from '../../core/types';
@@ -38,14 +38,14 @@ class HttpArgumentsHostImpl implements HttpArgumentsHost {
38
38
  */
39
39
  class WsArgumentsHostImpl implements WsArgumentsHost {
40
40
  public constructor(
41
- private readonly client: ServerWebSocket<unknown>,
41
+ private readonly client: IWebSocket<unknown>,
42
42
  private readonly data: unknown,
43
43
  ) {}
44
44
 
45
45
  /**
46
46
  * 获取 WebSocket 客户端
47
47
  */
48
- public getClient(): ServerWebSocket<unknown> {
48
+ public getClient(): IWebSocket<unknown> {
49
49
  return this.client;
50
50
  }
51
51
 
@@ -80,7 +80,7 @@ export class ExecutionContextImpl implements ExecutionContext {
80
80
  * @param client - WebSocket 客户端
81
81
  * @param data - 消息数据
82
82
  */
83
- public setWsContext(client: ServerWebSocket<unknown>, data: unknown): void {
83
+ public setWsContext(client: IWebSocket<unknown>, data: unknown): void {
84
84
  this.wsHost = new WsArgumentsHostImpl(client, data);
85
85
  }
86
86
 
@@ -1,7 +1,7 @@
1
1
  import type { Context } from '../../core/context';
2
2
  import type { ResponseBuilder } from '../../request/response';
3
3
  import type { Constructor } from '../../core/types';
4
- import type { ServerWebSocket } from 'bun';
4
+ import type { IWebSocket } from '../../platform/types';
5
5
 
6
6
  /**
7
7
  * 守卫接口
@@ -43,7 +43,7 @@ export interface WsArgumentsHost {
43
43
  * 获取 WebSocket 客户端
44
44
  * @returns WebSocket 连接对象
45
45
  */
46
- getClient(): ServerWebSocket<unknown>;
46
+ getClient(): IWebSocket<unknown>;
47
47
 
48
48
  /**
49
49
  * 获取消息数据
@@ -1,6 +1,7 @@
1
1
  import type { SwaggerDocument, SwaggerOptions, SwaggerPathItem } from './types';
2
2
  import { ControllerRegistry } from '../controller/controller';
3
3
  import { getControllerMetadata, getRouteMetadata } from '../controller/metadata';
4
+ import { getRuntime } from '../platform/runtime';
4
5
  import {
5
6
  getApiTags,
6
7
  getApiOperation,
@@ -311,7 +312,7 @@ export class SwaggerGenerator {
311
312
  */
312
313
  public generateMarkdownHtml(): string {
313
314
  const md = this.generateMarkdown();
314
- return Bun.markdown.html(md, { headings: true });
315
+ return getRuntime().parser.renderMarkdown(md);
315
316
  }
316
317
 
317
318
  /**
@@ -1,5 +1,4 @@
1
- import type { ServerWebSocket } from 'bun';
2
-
1
+ import type { IWebSocket } from '../platform/types';
3
2
  import { Container } from '../di/container';
4
3
  import { ControllerRegistry } from '../controller/controller';
5
4
  import { getGatewayMetadata, getHandlerMetadata } from './decorators';
@@ -161,7 +160,7 @@ export class WebSocketGatewayRegistry {
161
160
  * @param args - 原始参数(message, code, reason 等,不包括 ws)
162
161
  */
163
162
  private async invokeHandler(
164
- ws: ServerWebSocket<WebSocketConnectionData>,
163
+ ws: IWebSocket<WebSocketConnectionData>,
165
164
  definition: GatewayDefinition,
166
165
  handlerName: string | undefined,
167
166
  ...args: unknown[]
@@ -266,7 +265,7 @@ export class WebSocketGatewayRegistry {
266
265
  }
267
266
  }
268
267
 
269
- public async handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): Promise<void> {
268
+ public async handleOpen(ws: IWebSocket<WebSocketConnectionData>): Promise<void> {
270
269
  const path = ws.data?.path;
271
270
  const match = path ? this.getGateway(path) : undefined;
272
271
  if (!match) {
@@ -281,8 +280,8 @@ export class WebSocketGatewayRegistry {
281
280
  }
282
281
 
283
282
  public async handleMessage(
284
- ws: ServerWebSocket<WebSocketConnectionData>,
285
- message: string | ArrayBuffer | ArrayBufferView,
283
+ ws: IWebSocket<WebSocketConnectionData>,
284
+ message: string | ArrayBuffer | ArrayBufferView | Buffer,
286
285
  ): Promise<void> {
287
286
  const path = ws.data?.path;
288
287
  const match = path ? this.getGateway(path) : undefined;
@@ -298,7 +297,7 @@ export class WebSocketGatewayRegistry {
298
297
  }
299
298
 
300
299
  public async handleClose(
301
- ws: ServerWebSocket<WebSocketConnectionData>,
300
+ ws: IWebSocket<WebSocketConnectionData>,
302
301
  code: number,
303
302
  reason: string,
304
303
  ): Promise<void> {
@@ -1,8 +1,8 @@
1
1
  import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
2
  import { Application } from '../../src/core/application';
3
3
  import { Controller, ControllerRegistry } from '../../src/controller/controller';
4
- import { GET, POST } from '../../src/router/decorators';
5
- import { Param } from '../../src/controller/decorators';
4
+ import { GET, POST, PUT, DELETE, PATCH } from '../../src/router/decorators';
5
+ import { Param, Body } from '../../src/controller/decorators';
6
6
  import { RouteRegistry } from '../../src/router/registry';
7
7
  import { getTestPort } from '../utils/test-port';
8
8
 
@@ -203,6 +203,81 @@ describe('Controller Path Combination', () => {
203
203
  expect(rootResponse.status).toBe(404);
204
204
  });
205
205
 
206
+ test('should match GET POST PUT DELETE PATCH all registered on same dynamic route /api/:id independently', async () => {
207
+ @Controller('/api')
208
+ class ResourceController {
209
+ @GET('/:id')
210
+ public getResource(@Param('id') id: string) {
211
+ return { method: 'GET', id };
212
+ }
213
+
214
+ @POST('/:id')
215
+ public postResource(@Param('id') id: string) {
216
+ return { method: 'POST', id };
217
+ }
218
+
219
+ @PUT('/:id')
220
+ public putResource(@Param('id') id: string) {
221
+ return { method: 'PUT', id };
222
+ }
223
+
224
+ @DELETE('/:id')
225
+ public deleteResource(@Param('id') id: string) {
226
+ return { method: 'DELETE', id };
227
+ }
228
+
229
+ @PATCH('/:id')
230
+ public patchResource(@Param('id') id: string) {
231
+ return { method: 'PATCH', id };
232
+ }
233
+ }
234
+
235
+ app.registerController(ResourceController);
236
+ await app.listen();
237
+
238
+ const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
239
+ for (const method of methods) {
240
+ const res = await fetch(`http://localhost:${port}/api/123`, { method });
241
+ expect(res.status).toBe(200);
242
+ const data = await res.json() as { method: string; id: string };
243
+ expect(data.method).toBe(method);
244
+ expect(data.id).toBe('123');
245
+ }
246
+ });
247
+
248
+ test('should not cross-match methods: only registered methods respond 200, others respond 404/405', async () => {
249
+ @Controller('/items')
250
+ class ItemController {
251
+ @GET('/:id')
252
+ public getItem(@Param('id') id: string) {
253
+ return { method: 'GET', id };
254
+ }
255
+
256
+ @POST('/:id')
257
+ public createItem(@Param('id') id: string) {
258
+ return { method: 'POST', id };
259
+ }
260
+ }
261
+
262
+ app.registerController(ItemController);
263
+ await app.listen();
264
+
265
+ const getRes = await fetch(`http://localhost:${port}/items/42`);
266
+ expect(getRes.status).toBe(200);
267
+ expect((await getRes.json() as { method: string }).method).toBe('GET');
268
+
269
+ const postRes = await fetch(`http://localhost:${port}/items/42`, { method: 'POST' });
270
+ expect(postRes.status).toBe(200);
271
+ expect((await postRes.json() as { method: string }).method).toBe('POST');
272
+
273
+ // PUT and DELETE are not registered — should return a non-200 status
274
+ const putRes = await fetch(`http://localhost:${port}/items/42`, { method: 'PUT' });
275
+ expect(putRes.ok).toBe(false);
276
+
277
+ const deleteRes = await fetch(`http://localhost:${port}/items/42`, { method: 'DELETE' });
278
+ expect(deleteRes.ok).toBe(false);
279
+ });
280
+
206
281
  test('should correctly combine root controller "/" with method path "/health"', async () => {
207
282
  // 这是 metrics-rate-limit-app.ts 示例中的场景
208
283
  // @Controller('/') + @GET('/health') 应该映射到 /health,而不是 //health
@@ -275,6 +350,125 @@ describe('Controller Path Combination', () => {
275
350
  expect(dataResponse.status).toBe(200);
276
351
  expect((await dataResponse.json()).received).toBe(true);
277
352
  });
353
+
354
+ /**
355
+ * 模拟真实业务场景:ProjectController
356
+ *
357
+ * 路由表:
358
+ * GET /api/projects/ → list()
359
+ * GET /api/projects/:id → get(id)
360
+ * POST /api/projects/ → create(dto)
361
+ * PUT /api/projects/:id → update(id, dto)
362
+ * DELETE /api/projects/:id → remove(id)
363
+ * GET /api/projects/:id/pages → listPages(id)
364
+ */
365
+ test('ProjectController: all routes on /api/projects should match independently', async () => {
366
+ const db: Record<string, { id: string; name: string; description: string; pages: string[] }> = {
367
+ 'proj-1': { id: 'proj-1', name: 'Alpha', description: 'desc-a', pages: ['page-1', 'page-2'] },
368
+ 'proj-2': { id: 'proj-2', name: 'Beta', description: 'desc-b', pages: [] },
369
+ };
370
+
371
+ @Controller('/api/projects')
372
+ class ProjectController {
373
+ @GET('/')
374
+ list() {
375
+ return Object.values(db);
376
+ }
377
+
378
+ @GET('/:id')
379
+ get(@Param('id') id: string) {
380
+ return db[id] ?? null;
381
+ }
382
+
383
+ @POST('/')
384
+ create(@Body() dto: { name: string; description?: string }) {
385
+ const id = `proj-new`;
386
+ db[id] = { id, name: dto.name, description: dto.description ?? '', pages: [] };
387
+ return db[id];
388
+ }
389
+
390
+ @PUT('/:id')
391
+ update(@Param('id') id: string, @Body() dto: { name?: string; description?: string }) {
392
+ if (!db[id]) return null;
393
+ db[id] = { ...db[id], ...dto };
394
+ return db[id];
395
+ }
396
+
397
+ @DELETE('/:id')
398
+ remove(@Param('id') id: string) {
399
+ const existed = !!db[id];
400
+ delete db[id];
401
+ return { success: existed };
402
+ }
403
+
404
+ @GET('/:id/pages')
405
+ listPages(@Param('id') id: string) {
406
+ return db[id]?.pages ?? [];
407
+ }
408
+ }
409
+
410
+ app.registerController(ProjectController);
411
+ await app.listen();
412
+
413
+ const base = `http://localhost:${port}/api/projects`;
414
+
415
+ // GET / → 列表
416
+ const listRes = await fetch(`${base}/`);
417
+ expect(listRes.status).toBe(200);
418
+ const list = await listRes.json() as { id: string }[];
419
+ expect(list.length).toBe(2);
420
+
421
+ // GET /:id → 单条
422
+ const getRes = await fetch(`${base}/proj-1`);
423
+ expect(getRes.status).toBe(200);
424
+ const item = await getRes.json() as { id: string; name: string };
425
+ expect(item.id).toBe('proj-1');
426
+ expect(item.name).toBe('Alpha');
427
+
428
+ // POST / → 创建
429
+ const createRes = await fetch(`${base}/`, {
430
+ method: 'POST',
431
+ headers: { 'Content-Type': 'application/json' },
432
+ body: JSON.stringify({ name: 'Gamma', description: 'desc-c' }),
433
+ });
434
+ expect(createRes.status).toBe(200);
435
+ const created = await createRes.json() as { id: string; name: string };
436
+ expect(created.name).toBe('Gamma');
437
+
438
+ // PUT /:id → 更新
439
+ const updateRes = await fetch(`${base}/proj-1`, {
440
+ method: 'PUT',
441
+ headers: { 'Content-Type': 'application/json' },
442
+ body: JSON.stringify({ name: 'Alpha-Updated' }),
443
+ });
444
+ expect(updateRes.status).toBe(200);
445
+ const updated = await updateRes.json() as { name: string };
446
+ expect(updated.name).toBe('Alpha-Updated');
447
+
448
+ // GET /:id/pages → 子资源,不能被 GET /:id 拦截
449
+ const pagesRes = await fetch(`${base}/proj-2/pages`);
450
+ expect(pagesRes.status).toBe(200);
451
+ const pages = await pagesRes.json() as string[];
452
+ expect(Array.isArray(pages)).toBe(true);
453
+
454
+ // proj-1 pages(改名后仍可访问)
455
+ const pages1Res = await fetch(`${base}/proj-1/pages`);
456
+ expect(pages1Res.status).toBe(200);
457
+ const pages1 = await pages1Res.json() as string[];
458
+ expect(pages1).toEqual(['page-1', 'page-2']);
459
+
460
+ // DELETE /:id → 删除
461
+ const delRes = await fetch(`${base}/proj-2`, { method: 'DELETE' });
462
+ expect(delRes.status).toBe(200);
463
+ const delData = await delRes.json() as { success: boolean };
464
+ expect(delData.success).toBe(true);
465
+
466
+ // 删除后 GET /:id 返回 null(项目已不存在)
467
+ const afterDelRes = await fetch(`${base}/proj-2`);
468
+ expect(afterDelRes.status).toBe(200);
469
+ const afterDel = await afterDelRes.json();
470
+ expect(afterDel).toBeNull();
471
+ });
278
472
  });
279
473
 
280
474
  /**