@dangao/bun-server 1.9.0 → 1.12.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 (209) hide show
  1. package/README.md +79 -6
  2. package/dist/cache/cache-module.d.ts +6 -0
  3. package/dist/cache/cache-module.d.ts.map +1 -1
  4. package/dist/client/generator.d.ts +16 -0
  5. package/dist/client/generator.d.ts.map +1 -0
  6. package/dist/client/index.d.ts +4 -0
  7. package/dist/client/index.d.ts.map +1 -0
  8. package/dist/client/runtime.d.ts +15 -0
  9. package/dist/client/runtime.d.ts.map +1 -0
  10. package/dist/client/types.d.ts +36 -0
  11. package/dist/client/types.d.ts.map +1 -0
  12. package/dist/config/config-module.d.ts +7 -0
  13. package/dist/config/config-module.d.ts.map +1 -1
  14. package/dist/config/index.d.ts +1 -1
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/service.d.ts +13 -0
  17. package/dist/config/service.d.ts.map +1 -1
  18. package/dist/config/types.d.ts +10 -0
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/core/application.d.ts +7 -0
  21. package/dist/core/application.d.ts.map +1 -1
  22. package/dist/core/apply-decorators.d.ts +6 -0
  23. package/dist/core/apply-decorators.d.ts.map +1 -0
  24. package/dist/core/cluster.d.ts +47 -0
  25. package/dist/core/cluster.d.ts.map +1 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/server.d.ts +8 -0
  29. package/dist/core/server.d.ts.map +1 -1
  30. package/dist/dashboard/controller.d.ts +55 -0
  31. package/dist/dashboard/controller.d.ts.map +1 -0
  32. package/dist/dashboard/dashboard-extension.d.ts +20 -0
  33. package/dist/dashboard/dashboard-extension.d.ts.map +1 -0
  34. package/dist/dashboard/dashboard-module.d.ts +13 -0
  35. package/dist/dashboard/dashboard-module.d.ts.map +1 -0
  36. package/dist/dashboard/index.d.ts +4 -0
  37. package/dist/dashboard/index.d.ts.map +1 -0
  38. package/dist/dashboard/types.d.ts +16 -0
  39. package/dist/dashboard/types.d.ts.map +1 -0
  40. package/dist/dashboard/ui.d.ts +7 -0
  41. package/dist/dashboard/ui.d.ts.map +1 -0
  42. package/dist/database/database-module.d.ts +7 -0
  43. package/dist/database/database-module.d.ts.map +1 -1
  44. package/dist/debug/debug-module.d.ts +13 -0
  45. package/dist/debug/debug-module.d.ts.map +1 -0
  46. package/dist/debug/debug-ui-middleware.d.ts +8 -0
  47. package/dist/debug/debug-ui-middleware.d.ts.map +1 -0
  48. package/dist/debug/index.d.ts +5 -0
  49. package/dist/debug/index.d.ts.map +1 -0
  50. package/dist/debug/middleware.d.ts +12 -0
  51. package/dist/debug/middleware.d.ts.map +1 -0
  52. package/dist/debug/recorder.d.ts +61 -0
  53. package/dist/debug/recorder.d.ts.map +1 -0
  54. package/dist/debug/types.d.ts +48 -0
  55. package/dist/debug/types.d.ts.map +1 -0
  56. package/dist/debug/ui.d.ts +6 -0
  57. package/dist/debug/ui.d.ts.map +1 -0
  58. package/dist/di/async-module.d.ts +49 -0
  59. package/dist/di/async-module.d.ts.map +1 -0
  60. package/dist/di/lifecycle.d.ts +49 -0
  61. package/dist/di/lifecycle.d.ts.map +1 -0
  62. package/dist/di/module-registry.d.ts +24 -0
  63. package/dist/di/module-registry.d.ts.map +1 -1
  64. package/dist/index.d.ts +9 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1887 -35
  67. package/dist/router/route.d.ts +5 -7
  68. package/dist/router/route.d.ts.map +1 -1
  69. package/dist/swagger/generator.d.ts +10 -0
  70. package/dist/swagger/generator.d.ts.map +1 -1
  71. package/dist/testing/test-client.d.ts +49 -0
  72. package/dist/testing/test-client.d.ts.map +1 -0
  73. package/dist/testing/testing-module.d.ts +90 -0
  74. package/dist/testing/testing-module.d.ts.map +1 -0
  75. package/dist/websocket/registry.d.ts +1 -6
  76. package/dist/websocket/registry.d.ts.map +1 -1
  77. package/docs/async-module.md +59 -0
  78. package/docs/client-generation.md +100 -0
  79. package/docs/cluster.md +81 -0
  80. package/docs/custom-decorators.md +1 -7
  81. package/docs/dashboard.md +54 -0
  82. package/docs/debug.md +58 -0
  83. package/docs/extensions.md +0 -2
  84. package/docs/guide.md +0 -1
  85. package/docs/lifecycle.md +72 -0
  86. package/docs/testing.md +110 -0
  87. package/docs/zh/async-module.md +98 -0
  88. package/docs/zh/client-generation.md +92 -0
  89. package/docs/zh/cluster.md +74 -0
  90. package/docs/zh/custom-decorators.md +1 -7
  91. package/docs/zh/dashboard.md +69 -0
  92. package/docs/zh/debug.md +81 -0
  93. package/docs/zh/extensions.md +0 -2
  94. package/docs/zh/guide.md +0 -1
  95. package/docs/zh/lifecycle.md +87 -0
  96. package/docs/zh/migration.md +0 -5
  97. package/docs/zh/testing.md +119 -0
  98. package/package.json +4 -4
  99. package/src/cache/cache-module.ts +25 -0
  100. package/src/client/generator.ts +36 -0
  101. package/src/client/index.ts +8 -0
  102. package/src/client/runtime.ts +101 -0
  103. package/src/client/types.ts +38 -0
  104. package/src/config/config-module.ts +44 -4
  105. package/src/config/index.ts +1 -0
  106. package/src/config/service.ts +50 -0
  107. package/src/config/types.ts +12 -0
  108. package/src/core/application.ts +37 -0
  109. package/src/core/apply-decorators.ts +31 -0
  110. package/src/core/cluster.ts +143 -0
  111. package/src/core/index.ts +1 -0
  112. package/src/core/server.ts +14 -1
  113. package/src/dashboard/controller.ts +227 -0
  114. package/src/dashboard/dashboard-extension.ts +26 -0
  115. package/src/dashboard/dashboard-module.ts +38 -0
  116. package/src/dashboard/index.ts +3 -0
  117. package/src/dashboard/types.ts +16 -0
  118. package/src/dashboard/ui.ts +219 -0
  119. package/src/database/database-module.ts +20 -0
  120. package/src/debug/debug-module.ts +70 -0
  121. package/src/debug/debug-ui-middleware.ts +110 -0
  122. package/src/debug/index.ts +9 -0
  123. package/src/debug/middleware.ts +126 -0
  124. package/src/debug/recorder.ts +141 -0
  125. package/src/debug/types.ts +49 -0
  126. package/src/debug/ui.ts +393 -0
  127. package/src/di/async-module.ts +141 -0
  128. package/src/di/lifecycle.ts +117 -0
  129. package/src/di/module-registry.ts +75 -0
  130. package/src/index.ts +35 -0
  131. package/src/router/route.ts +20 -20
  132. package/src/swagger/generator.ts +100 -0
  133. package/src/testing/test-client.ts +112 -0
  134. package/src/testing/testing-module.ts +238 -0
  135. package/src/websocket/registry.ts +3 -16
  136. package/tests/auth/auth-decorators.test.ts +0 -1
  137. package/tests/auth/oauth2-service.test.ts +0 -1
  138. package/tests/cache/cache-decorators-extended.test.ts +0 -1
  139. package/tests/cache/cache-decorators.test.ts +0 -1
  140. package/tests/cache/cache-interceptors.test.ts +0 -1
  141. package/tests/cache/cache-module.test.ts +0 -1
  142. package/tests/cache/cache-service-proxy.test.ts +0 -1
  143. package/tests/client/client-generator.test.ts +142 -0
  144. package/tests/config/config-center-integration.test.ts +0 -1
  145. package/tests/config/config-module-extended.test.ts +0 -1
  146. package/tests/config/config-module.test.ts +0 -1
  147. package/tests/controller/controller.test.ts +0 -1
  148. package/tests/controller/param-binder.test.ts +0 -1
  149. package/tests/controller/path-combination.test.ts +0 -1
  150. package/tests/core/application.test.ts +34 -0
  151. package/tests/core/apply-decorators.test.ts +109 -0
  152. package/tests/core/cluster.test.ts +32 -0
  153. package/tests/dashboard/dashboard-module.test.ts +85 -0
  154. package/tests/database/database-module.test.ts +0 -1
  155. package/tests/database/orm.test.ts +0 -1
  156. package/tests/database/postgres-mysql-integration.test.ts +0 -1
  157. package/tests/database/transaction.test.ts +0 -1
  158. package/tests/debug/debug-module.test.ts +141 -0
  159. package/tests/di/async-module.test.ts +125 -0
  160. package/tests/di/container.test.ts +0 -1
  161. package/tests/di/lifecycle.test.ts +140 -0
  162. package/tests/error/error-handler.test.ts +0 -1
  163. package/tests/events/event-decorators.test.ts +0 -1
  164. package/tests/events/event-listener-scanner.test.ts +0 -1
  165. package/tests/events/event-module.test.ts +0 -1
  166. package/tests/extensions/logger-module.test.ts +0 -1
  167. package/tests/health/health-module.test.ts +0 -1
  168. package/tests/integration/oauth2-e2e.test.ts +0 -1
  169. package/tests/integration/session-e2e.test.ts +0 -1
  170. package/tests/interceptor/base-interceptor.test.ts +0 -1
  171. package/tests/interceptor/builtin/cache-interceptor.test.ts +0 -1
  172. package/tests/interceptor/builtin/log-interceptor.test.ts +0 -1
  173. package/tests/interceptor/builtin/permission-interceptor.test.ts +0 -1
  174. package/tests/interceptor/interceptor-advanced-integration.test.ts +0 -1
  175. package/tests/interceptor/interceptor-chain.test.ts +0 -1
  176. package/tests/interceptor/interceptor-integration.test.ts +0 -1
  177. package/tests/interceptor/interceptor-metadata.test.ts +0 -1
  178. package/tests/interceptor/interceptor-registry.test.ts +0 -1
  179. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -1
  180. package/tests/metrics/metrics-module.test.ts +0 -1
  181. package/tests/microservice/config-center.test.ts +0 -1
  182. package/tests/microservice/service-client-decorators.test.ts +0 -1
  183. package/tests/microservice/service-registry-decorators.test.ts +0 -1
  184. package/tests/microservice/service-registry.test.ts +0 -1
  185. package/tests/middleware/builtin/middleware-builtin-extended.test.ts +0 -1
  186. package/tests/middleware/builtin/rate-limit.test.ts +0 -1
  187. package/tests/middleware/middleware-decorators.test.ts +0 -1
  188. package/tests/middleware/middleware-pipeline.test.ts +0 -1
  189. package/tests/middleware/middleware.test.ts +0 -1
  190. package/tests/perf/optimization.test.ts +0 -1
  191. package/tests/queue/queue-decorators.test.ts +0 -1
  192. package/tests/queue/queue-module.test.ts +0 -1
  193. package/tests/queue/queue-service.test.ts +0 -1
  194. package/tests/router/router-decorators.test.ts +0 -1
  195. package/tests/router/router-extended.test.ts +0 -1
  196. package/tests/security/guards/guards-integration.test.ts +0 -1
  197. package/tests/security/guards/guards.test.ts +0 -1
  198. package/tests/security/guards/reflector.test.ts +0 -1
  199. package/tests/security/security-filter.test.ts +0 -1
  200. package/tests/security/security-module-extended.test.ts +0 -1
  201. package/tests/security/security-module.test.ts +0 -1
  202. package/tests/session/session-decorators.test.ts +0 -1
  203. package/tests/session/session-module.test.ts +0 -1
  204. package/tests/swagger/decorators.test.ts +0 -1
  205. package/tests/swagger/swagger-module.test.ts +0 -1
  206. package/tests/swagger/ui.test.ts +0 -1
  207. package/tests/testing/testing-module.test.ts +129 -0
  208. package/tests/validation/class-validator.test.ts +0 -1
  209. package/tests/validation/controller-validation.test.ts +0 -1
@@ -19,6 +19,7 @@ import { ConfigModule } from '../config/config-module';
19
19
  import { CacheModule, CACHE_POST_PROCESSOR_TOKEN } from '../cache';
20
20
  import { LoggerManager } from '@dangao/logsmith';
21
21
  import { EventModule } from '../events/event-module';
22
+ import { AsyncProviderRegistry } from '../di/async-module';
22
23
 
23
24
  /**
24
25
  * 应用配置选项
@@ -45,6 +46,14 @@ export interface ApplicationOptions {
45
46
  * 默认 true
46
47
  */
47
48
  enableSignalHandlers?: boolean;
49
+
50
+ /**
51
+ * 是否启用 SO_REUSEPORT
52
+ * 允许多进程绑定同一端口,用于多进程负载均衡
53
+ * 仅 Linux 有效,macOS/Windows 会忽略
54
+ * @default false
55
+ */
56
+ reusePort?: boolean;
48
57
  }
49
58
 
50
59
  /**
@@ -97,6 +106,12 @@ export class Application {
97
106
  throw new Error('Application is already running');
98
107
  }
99
108
 
109
+ // 初始化异步 providers(forRootAsync 注册的工厂)
110
+ const asyncRegistry = AsyncProviderRegistry.getInstance();
111
+ if (asyncRegistry.hasPending()) {
112
+ await asyncRegistry.initializeAll(this.getContainer());
113
+ }
114
+
100
115
  // 初始化所有扩展(包括数据库连接等)
101
116
  await this.initializeExtensions();
102
117
 
@@ -106,12 +121,20 @@ export class Application {
106
121
  // 自动初始化事件监听器(如果 EventModule 已注册且启用了 autoScan)
107
122
  this.initializeEventListeners();
108
123
 
124
+ // 调用生命周期钩子:onModuleInit
125
+ const registry = ModuleRegistry.getInstance();
126
+ await registry.callModuleInitHooks();
127
+
128
+ // 调用生命周期钩子:onApplicationBootstrap
129
+ await registry.callBootstrapHooks();
130
+
109
131
  const finalPort = port ?? this.options.port ?? 3000;
110
132
  const finalHostname = hostname ?? this.options.hostname;
111
133
 
112
134
  const serverOptions: ServerOptions = {
113
135
  port: finalPort,
114
136
  hostname: finalHostname,
137
+ reusePort: this.options.reusePort,
115
138
  fetch: this.handleRequest.bind(this),
116
139
  websocketRegistry: this.websocketRegistry,
117
140
  gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
@@ -214,6 +237,13 @@ export class Application {
214
237
  // 移除信号处理器
215
238
  this.removeSignalHandlers();
216
239
 
240
+ // 调用生命周期钩子:onApplicationShutdown
241
+ const moduleRegistry = ModuleRegistry.getInstance();
242
+ await moduleRegistry.callShutdownHooks();
243
+
244
+ // 调用生命周期钩子:onModuleDestroy
245
+ await moduleRegistry.callModuleDestroyHooks();
246
+
217
247
  // 自动注销服务(如果使用了 @ServiceRegistry 装饰器)
218
248
  await this.deregisterServices();
219
249
 
@@ -232,6 +262,13 @@ export class Application {
232
262
  // 移除信号处理器
233
263
  this.removeSignalHandlers();
234
264
 
265
+ // 调用生命周期钩子:onApplicationShutdown
266
+ const moduleRegistry = ModuleRegistry.getInstance();
267
+ await moduleRegistry.callShutdownHooks();
268
+
269
+ // 调用生命周期钩子:onModuleDestroy
270
+ await moduleRegistry.callModuleDestroyHooks();
271
+
235
272
  // 自动注销服务(如果使用了 @ServiceRegistry 装饰器)
236
273
  await this.deregisterServices();
237
274
 
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 装饰器组合工具
3
+ * 将多个装饰器合并为一个可复用的装饰器
4
+ */
5
+ export function applyDecorators(
6
+ ...decorators: Array<ClassDecorator | MethodDecorator | PropertyDecorator>
7
+ ): ClassDecorator & MethodDecorator & PropertyDecorator {
8
+ return ((
9
+ target: unknown,
10
+ propertyKey?: string | symbol,
11
+ descriptor?: PropertyDescriptor,
12
+ ) => {
13
+ for (const decorator of decorators.reverse()) {
14
+ if (descriptor) {
15
+ const result = (decorator as MethodDecorator)(
16
+ target as object,
17
+ propertyKey!,
18
+ descriptor,
19
+ );
20
+ if (result) {
21
+ descriptor = result as PropertyDescriptor;
22
+ }
23
+ } else if (propertyKey) {
24
+ (decorator as PropertyDecorator)(target as object, propertyKey);
25
+ } else {
26
+ (decorator as ClassDecorator)(target as Function);
27
+ }
28
+ }
29
+ return descriptor;
30
+ }) as ClassDecorator & MethodDecorator & PropertyDecorator;
31
+ }
@@ -0,0 +1,143 @@
1
+ import { spawn } from 'bun';
2
+ import { LoggerManager } from '@dangao/logsmith';
3
+
4
+ export interface ClusterOptions {
5
+ /**
6
+ * 是否启用集群模式
7
+ * @default false
8
+ */
9
+ enabled?: boolean;
10
+ /**
11
+ * Worker 数量。'auto' 表示使用 CPU 核心数
12
+ * @default 'auto'
13
+ */
14
+ workers?: number | 'auto';
15
+ }
16
+
17
+ /**
18
+ * 集群管理器
19
+ * 自动派生 worker 进程,每个 worker 使用 reusePort 绑定相同端口
20
+ */
21
+ export class ClusterManager {
22
+ private readonly workerCount: number;
23
+ private readonly workers: ReturnType<typeof spawn>[] = [];
24
+ private readonly scriptPath: string;
25
+ private readonly port: number;
26
+ private readonly hostname?: string;
27
+
28
+ public constructor(options: {
29
+ workers: number | 'auto';
30
+ scriptPath: string;
31
+ port: number;
32
+ hostname?: string;
33
+ }) {
34
+ this.workerCount =
35
+ options.workers === 'auto' ? navigator.hardwareConcurrency : options.workers;
36
+ this.scriptPath = options.scriptPath;
37
+ this.port = options.port;
38
+ this.hostname = options.hostname;
39
+ }
40
+
41
+ /**
42
+ * 启动所有 worker 进程
43
+ */
44
+ public start(): void {
45
+ const logger = LoggerManager.getLogger();
46
+ logger.info(
47
+ `[Cluster] Starting ${this.workerCount} workers on port ${this.port}`,
48
+ );
49
+
50
+ for (let i = 0; i < this.workerCount; i++) {
51
+ const worker = spawn({
52
+ cmd: ['bun', 'run', this.scriptPath],
53
+ env: {
54
+ ...process.env,
55
+ PORT: String(this.port),
56
+ REUSE_PORT: '1',
57
+ CLUSTER_WORKER: '1',
58
+ WORKER_ID: String(i),
59
+ ...(this.hostname ? { HOSTNAME: this.hostname } : {}),
60
+ },
61
+ stdout: 'inherit',
62
+ stderr: 'inherit',
63
+ });
64
+ this.workers.push(worker);
65
+ }
66
+
67
+ logger.info(
68
+ `[Cluster] ${this.workerCount} workers started (reusePort mode)`,
69
+ );
70
+
71
+ // Monitor workers and auto-restart on crash
72
+ this.monitorWorkers();
73
+ }
74
+
75
+ /**
76
+ * 停止所有 worker 进程
77
+ */
78
+ public async stop(): Promise<void> {
79
+ const logger = LoggerManager.getLogger();
80
+ logger.info('[Cluster] Stopping all workers...');
81
+
82
+ for (const worker of this.workers) {
83
+ worker.kill('SIGTERM');
84
+ }
85
+
86
+ // Wait up to 5 seconds for workers to exit
87
+ const timeout = setTimeout(() => {
88
+ for (const worker of this.workers) {
89
+ worker.kill('SIGKILL');
90
+ }
91
+ }, 5000);
92
+
93
+ await Promise.all(this.workers.map((w) => w.exited));
94
+ clearTimeout(timeout);
95
+
96
+ this.workers.length = 0;
97
+ logger.info('[Cluster] All workers stopped');
98
+ }
99
+
100
+ private monitorWorkers(): void {
101
+ // For each worker, restart if it crashes unexpectedly
102
+ for (let i = 0; i < this.workers.length; i++) {
103
+ const index = i;
104
+ this.workers[index].exited.then((exitCode) => {
105
+ if (exitCode !== 0 && exitCode !== null) {
106
+ const logger = LoggerManager.getLogger();
107
+ logger.warn(
108
+ `[Cluster] Worker ${index} exited with code ${exitCode}, restarting...`,
109
+ );
110
+
111
+ const newWorker = spawn({
112
+ cmd: ['bun', 'run', this.scriptPath],
113
+ env: {
114
+ ...process.env,
115
+ PORT: String(this.port),
116
+ REUSE_PORT: '1',
117
+ CLUSTER_WORKER: '1',
118
+ WORKER_ID: String(index),
119
+ ...(this.hostname ? { HOSTNAME: this.hostname } : {}),
120
+ },
121
+ stdout: 'inherit',
122
+ stderr: 'inherit',
123
+ });
124
+ this.workers[index] = newWorker;
125
+ }
126
+ });
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 检查当前进程是否为 cluster worker
132
+ */
133
+ public static isWorker(): boolean {
134
+ return process.env.CLUSTER_WORKER === '1';
135
+ }
136
+
137
+ /**
138
+ * 获取当前 worker ID(仅在 worker 进程中有效)
139
+ */
140
+ public static getWorkerId(): number {
141
+ return Number(process.env.WORKER_ID ?? -1);
142
+ }
143
+ }
package/src/core/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Application, type ApplicationOptions } from './application';
2
2
  export { BunServer, type ServerOptions } from './server';
3
+ export { ClusterManager, type ClusterOptions } from './cluster';
3
4
  export { Context } from './context';
4
5
  export { ContextService, CONTEXT_SERVICE_TOKEN, contextStore } from './context-service';
5
6
 
@@ -33,6 +33,14 @@ export interface ServerOptions {
33
33
  * 默认 30 秒
34
34
  */
35
35
  gracefulShutdownTimeout?: number;
36
+
37
+ /**
38
+ * 是否启用 SO_REUSEPORT
39
+ * 允许多进程绑定同一端口,用于多进程负载均衡
40
+ * 仅 Linux 有效,macOS/Windows 会忽略
41
+ * @default false
42
+ */
43
+ reusePort?: boolean;
36
44
  }
37
45
 
38
46
  /**
@@ -70,6 +78,7 @@ export class BunServer {
70
78
  this.server = Bun.serve({
71
79
  port: this.options.port ?? 3000,
72
80
  hostname: this.options.hostname,
81
+ reusePort: this.options.reusePort,
73
82
  fetch: (
74
83
  request: Request,
75
84
  server: Server<WebSocketConnectionData>,
@@ -150,7 +159,7 @@ export class BunServer {
150
159
  });
151
160
 
152
161
  const hostname = this.options.hostname ?? "localhost";
153
- const port = this.options.port ?? 3000;
162
+ const port = this.server.port;
154
163
  logger.info(`Server started at http://${hostname}:${port}`);
155
164
  }
156
165
 
@@ -249,8 +258,12 @@ export class BunServer {
249
258
 
250
259
  /**
251
260
  * 获取服务器端口
261
+ * 如果服务器正在运行,返回实际绑定的端口(支持 port:0 场景)
252
262
  */
253
263
  public getPort(): number {
264
+ if (this.server) {
265
+ return this.server.port ?? this.options.port ?? 3000;
266
+ }
254
267
  return this.options.port ?? 3000;
255
268
  }
256
269
 
@@ -0,0 +1,227 @@
1
+ import { RouteRegistry } from '../router/registry';
2
+ import type { Context } from '../core/context';
3
+ import { createDashboardHTML } from './ui';
4
+ import { ControllerRegistry } from '../controller/controller';
5
+ import { HEALTH_INDICATORS_TOKEN } from '../health/types';
6
+ import type { HealthIndicator } from '../health/types';
7
+
8
+ /**
9
+ * Dashboard 服务
10
+ * 提供监控 UI 和 API 端点
11
+ */
12
+ export class DashboardService {
13
+ private readonly basePath: string;
14
+ private readonly auth?: { username: string; password: string };
15
+ private readonly startTime: number = Date.now();
16
+
17
+ /**
18
+ * 创建 Dashboard 服务实例
19
+ * @param basePath - Dashboard 基础路径
20
+ * @param auth - Basic Auth 认证配置(可选)
21
+ */
22
+ public constructor(
23
+ basePath: string,
24
+ auth?: { username: string; password: string },
25
+ ) {
26
+ this.basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
27
+ this.auth = auth;
28
+ }
29
+
30
+ /**
31
+ * 注册 Dashboard 路由
32
+ */
33
+ public registerRoutes(): void {
34
+ const registry = RouteRegistry.getInstance();
35
+
36
+ registry.register('GET', this.basePath, (ctx) => this.handleDashboard(ctx));
37
+ registry.register(
38
+ 'GET',
39
+ `${this.basePath}/api/system`,
40
+ (ctx) => this.handleSystem(ctx),
41
+ );
42
+ registry.register(
43
+ 'GET',
44
+ `${this.basePath}/api/routes`,
45
+ (ctx) => this.handleRoutes(ctx),
46
+ );
47
+ registry.register(
48
+ 'GET',
49
+ `${this.basePath}/api/health`,
50
+ (ctx) => this.handleHealth(ctx),
51
+ );
52
+ registry.register(
53
+ 'POST',
54
+ `${this.basePath}/api/markdown`,
55
+ (ctx) => this.handleMarkdownRender(ctx),
56
+ );
57
+ }
58
+
59
+ /**
60
+ * 检查 Basic Auth
61
+ * @param ctx - 请求上下文
62
+ * @returns 是否通过认证,未配置 auth 时返回 true
63
+ */
64
+ private checkAuth(ctx: Context): boolean {
65
+ if (!this.auth) {
66
+ return true;
67
+ }
68
+ const authHeader = ctx.getHeader('Authorization');
69
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
70
+ return false;
71
+ }
72
+ try {
73
+ const decoded = atob(authHeader.slice(6));
74
+ const [username, password] = decoded.split(':');
75
+ return (
76
+ username === this.auth.username && password === this.auth.password
77
+ );
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 返回 401 Unauthorized 响应
85
+ */
86
+ private unauthorizedResponse(): Response {
87
+ return new Response('Unauthorized', {
88
+ status: 401,
89
+ headers: {
90
+ 'WWW-Authenticate': 'Basic realm="Dashboard"',
91
+ 'Content-Type': 'text/plain',
92
+ },
93
+ });
94
+ }
95
+
96
+ /**
97
+ * 处理 Dashboard 主页面
98
+ */
99
+ private async handleDashboard(ctx: Context): Promise<Response> {
100
+ if (!this.checkAuth(ctx)) {
101
+ return this.unauthorizedResponse();
102
+ }
103
+ const html = createDashboardHTML(this.basePath);
104
+ return new Response(html, {
105
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
106
+ });
107
+ }
108
+
109
+ /**
110
+ * 处理系统信息 API
111
+ */
112
+ private async handleSystem(ctx: Context): Promise<Response> {
113
+ if (!this.checkAuth(ctx)) {
114
+ return this.unauthorizedResponse();
115
+ }
116
+ const mem = process.memoryUsage();
117
+ const data = {
118
+ uptime: Math.floor(process.uptime()),
119
+ memory: {
120
+ rss: mem.rss,
121
+ heapUsed: mem.heapUsed,
122
+ heapTotal: mem.heapTotal,
123
+ },
124
+ platform: process.platform,
125
+ bunVersion: typeof Bun !== 'undefined' ? Bun.version : undefined,
126
+ };
127
+ return new Response(JSON.stringify(data), {
128
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
129
+ });
130
+ }
131
+
132
+ /**
133
+ * 处理路由列表 API
134
+ */
135
+ private async handleRoutes(ctx: Context): Promise<Response> {
136
+ if (!this.checkAuth(ctx)) {
137
+ return this.unauthorizedResponse();
138
+ }
139
+ const registry = RouteRegistry.getInstance();
140
+ const router = registry.getRouter();
141
+ const routes = router.getRoutes();
142
+ const data = routes.map((r) => ({
143
+ method: r.method,
144
+ path: r.path,
145
+ controller: r.controllerClass?.name ?? undefined,
146
+ methodName: r.methodName ?? undefined,
147
+ }));
148
+ return new Response(JSON.stringify(data), {
149
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
150
+ });
151
+ }
152
+
153
+ /**
154
+ * 将 Markdown 文本渲染为 HTML
155
+ * 利用 Bun 1.3.8+ 内置 Bun.markdown 解析器(Zig 实现,支持 GFM)
156
+ */
157
+ private async handleMarkdownRender(ctx: Context): Promise<Response> {
158
+ if (!this.checkAuth(ctx)) {
159
+ return this.unauthorizedResponse();
160
+ }
161
+ try {
162
+ const body = await ctx.request.json() as { content?: string };
163
+ if (!body.content) {
164
+ return new Response(JSON.stringify({ error: 'content field is required' }), {
165
+ status: 400,
166
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
167
+ });
168
+ }
169
+ const html = Bun.markdown.html(body.content, { headings: true });
170
+ return new Response(JSON.stringify({ html }), {
171
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
172
+ });
173
+ } catch (err) {
174
+ return new Response(JSON.stringify({ error: (err as Error).message }), {
175
+ status: 500,
176
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
177
+ });
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 处理健康检查 API
183
+ * 如果 HealthModule 已注册则运行健康指示器,否则返回基本状态
184
+ */
185
+ private async handleHealth(ctx: Context): Promise<Response> {
186
+ if (!this.checkAuth(ctx)) {
187
+ return this.unauthorizedResponse();
188
+ }
189
+ try {
190
+ const container = ControllerRegistry.getInstance().getContainer();
191
+ const indicators = container.resolve<HealthIndicator[]>(
192
+ HEALTH_INDICATORS_TOKEN,
193
+ );
194
+ const details: Record<string, { status: string; details?: unknown }> = {};
195
+ for (const indicator of indicators) {
196
+ try {
197
+ const result = await indicator.check();
198
+ details[indicator.name] = result;
199
+ } catch (err) {
200
+ details[indicator.name] = {
201
+ status: 'down',
202
+ details: { error: (err as Error).message },
203
+ };
204
+ }
205
+ }
206
+ const allUp =
207
+ Object.keys(details).length === 0 ||
208
+ Object.values(details).every((r) => r.status === 'up');
209
+ const data = {
210
+ status: allUp ? 'up' : 'down',
211
+ timestamp: Date.now(),
212
+ details,
213
+ };
214
+ return new Response(JSON.stringify(data), {
215
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
216
+ });
217
+ } catch {
218
+ const data = {
219
+ status: 'up',
220
+ timestamp: Date.now(),
221
+ };
222
+ return new Response(JSON.stringify(data), {
223
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
224
+ });
225
+ }
226
+ }
227
+ }
@@ -0,0 +1,26 @@
1
+ import type { ApplicationExtension } from '../extensions/types';
2
+ import type { Container } from '../di/container';
3
+ import { DashboardService } from './controller';
4
+
5
+ /**
6
+ * Dashboard 扩展
7
+ * 在应用初始化时注册 Dashboard 路由
8
+ */
9
+ export class DashboardExtension implements ApplicationExtension {
10
+ private readonly service: DashboardService;
11
+
12
+ /**
13
+ * 创建 Dashboard 扩展
14
+ * @param service - Dashboard 服务实例
15
+ */
16
+ public constructor(service: DashboardService) {
17
+ this.service = service;
18
+ }
19
+
20
+ /**
21
+ * 注册扩展,将 Dashboard 路由注册到 RouteRegistry
22
+ */
23
+ public register(_container: Container): void {
24
+ this.service.registerRoutes();
25
+ }
26
+ }
@@ -0,0 +1,38 @@
1
+ import { Module, MODULE_METADATA_KEY } from '../di/module';
2
+ import { DashboardService } from './controller';
3
+ import { DashboardExtension } from './dashboard-extension';
4
+ import type { DashboardModuleOptions } from './types';
5
+
6
+ /**
7
+ * Dashboard 模块
8
+ * 提供监控 Web UI,可视化指标、健康状态、路由和系统信息
9
+ */
10
+ @Module({
11
+ extensions: [],
12
+ controllers: [],
13
+ providers: [],
14
+ })
15
+ export class DashboardModule {
16
+ /**
17
+ * 创建 Dashboard 模块
18
+ * @param options - 模块配置
19
+ */
20
+ public static forRoot(
21
+ options: DashboardModuleOptions = {},
22
+ ): typeof DashboardModule {
23
+ const path = options.path ?? '/_dashboard';
24
+ const basePath = path.endsWith('/') ? path.slice(0, -1) : path;
25
+ const service = new DashboardService(basePath, options.auth);
26
+ const extension = new DashboardExtension(service);
27
+
28
+ const existingMetadata =
29
+ Reflect.getMetadata(MODULE_METADATA_KEY, DashboardModule) || {};
30
+ const metadata = {
31
+ ...existingMetadata,
32
+ extensions: [extension],
33
+ };
34
+ Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, DashboardModule);
35
+
36
+ return DashboardModule;
37
+ }
38
+ }
@@ -0,0 +1,3 @@
1
+ export { DashboardModule } from './dashboard-module';
2
+ export { DashboardService } from './controller';
3
+ export { DASHBOARD_OPTIONS_TOKEN, type DashboardModuleOptions } from './types';
@@ -0,0 +1,16 @@
1
+ export interface DashboardModuleOptions {
2
+ /**
3
+ * Dashboard UI 路径
4
+ * @default '/_dashboard'
5
+ */
6
+ path?: string;
7
+ /**
8
+ * Basic Auth 认证配置
9
+ */
10
+ auth?: {
11
+ username: string;
12
+ password: string;
13
+ };
14
+ }
15
+
16
+ export const DASHBOARD_OPTIONS_TOKEN = Symbol('@dangao/bun-server:dashboard:options');