@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.
- package/README.md +79 -6
- package/dist/cache/cache-module.d.ts +6 -0
- package/dist/cache/cache-module.d.ts.map +1 -1
- package/dist/client/generator.d.ts +16 -0
- package/dist/client/generator.d.ts.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/runtime.d.ts +15 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/client/types.d.ts +36 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/config/config-module.d.ts +7 -0
- package/dist/config/config-module.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/service.d.ts +13 -0
- package/dist/config/service.d.ts.map +1 -1
- package/dist/config/types.d.ts +10 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/core/application.d.ts +7 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/apply-decorators.d.ts +6 -0
- package/dist/core/apply-decorators.d.ts.map +1 -0
- package/dist/core/cluster.d.ts +47 -0
- package/dist/core/cluster.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/server.d.ts +8 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts +55 -0
- package/dist/dashboard/controller.d.ts.map +1 -0
- package/dist/dashboard/dashboard-extension.d.ts +20 -0
- package/dist/dashboard/dashboard-extension.d.ts.map +1 -0
- package/dist/dashboard/dashboard-module.d.ts +13 -0
- package/dist/dashboard/dashboard-module.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/types.d.ts +16 -0
- package/dist/dashboard/types.d.ts.map +1 -0
- package/dist/dashboard/ui.d.ts +7 -0
- package/dist/dashboard/ui.d.ts.map +1 -0
- package/dist/database/database-module.d.ts +7 -0
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/debug/debug-module.d.ts +13 -0
- package/dist/debug/debug-module.d.ts.map +1 -0
- package/dist/debug/debug-ui-middleware.d.ts +8 -0
- package/dist/debug/debug-ui-middleware.d.ts.map +1 -0
- package/dist/debug/index.d.ts +5 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/middleware.d.ts +12 -0
- package/dist/debug/middleware.d.ts.map +1 -0
- package/dist/debug/recorder.d.ts +61 -0
- package/dist/debug/recorder.d.ts.map +1 -0
- package/dist/debug/types.d.ts +48 -0
- package/dist/debug/types.d.ts.map +1 -0
- package/dist/debug/ui.d.ts +6 -0
- package/dist/debug/ui.d.ts.map +1 -0
- package/dist/di/async-module.d.ts +49 -0
- package/dist/di/async-module.d.ts.map +1 -0
- package/dist/di/lifecycle.d.ts +49 -0
- package/dist/di/lifecycle.d.ts.map +1 -0
- package/dist/di/module-registry.d.ts +24 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1887 -35
- package/dist/router/route.d.ts +5 -7
- package/dist/router/route.d.ts.map +1 -1
- package/dist/swagger/generator.d.ts +10 -0
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/testing/test-client.d.ts +49 -0
- package/dist/testing/test-client.d.ts.map +1 -0
- package/dist/testing/testing-module.d.ts +90 -0
- package/dist/testing/testing-module.d.ts.map +1 -0
- package/dist/websocket/registry.d.ts +1 -6
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/async-module.md +59 -0
- package/docs/client-generation.md +100 -0
- package/docs/cluster.md +81 -0
- package/docs/custom-decorators.md +1 -7
- package/docs/dashboard.md +54 -0
- package/docs/debug.md +58 -0
- package/docs/extensions.md +0 -2
- package/docs/guide.md +0 -1
- package/docs/lifecycle.md +72 -0
- package/docs/testing.md +110 -0
- package/docs/zh/async-module.md +98 -0
- package/docs/zh/client-generation.md +92 -0
- package/docs/zh/cluster.md +74 -0
- package/docs/zh/custom-decorators.md +1 -7
- package/docs/zh/dashboard.md +69 -0
- package/docs/zh/debug.md +81 -0
- package/docs/zh/extensions.md +0 -2
- package/docs/zh/guide.md +0 -1
- package/docs/zh/lifecycle.md +87 -0
- package/docs/zh/migration.md +0 -5
- package/docs/zh/testing.md +119 -0
- package/package.json +4 -4
- package/src/cache/cache-module.ts +25 -0
- package/src/client/generator.ts +36 -0
- package/src/client/index.ts +8 -0
- package/src/client/runtime.ts +101 -0
- package/src/client/types.ts +38 -0
- package/src/config/config-module.ts +44 -4
- package/src/config/index.ts +1 -0
- package/src/config/service.ts +50 -0
- package/src/config/types.ts +12 -0
- package/src/core/application.ts +37 -0
- package/src/core/apply-decorators.ts +31 -0
- package/src/core/cluster.ts +143 -0
- package/src/core/index.ts +1 -0
- package/src/core/server.ts +14 -1
- package/src/dashboard/controller.ts +227 -0
- package/src/dashboard/dashboard-extension.ts +26 -0
- package/src/dashboard/dashboard-module.ts +38 -0
- package/src/dashboard/index.ts +3 -0
- package/src/dashboard/types.ts +16 -0
- package/src/dashboard/ui.ts +219 -0
- package/src/database/database-module.ts +20 -0
- package/src/debug/debug-module.ts +70 -0
- package/src/debug/debug-ui-middleware.ts +110 -0
- package/src/debug/index.ts +9 -0
- package/src/debug/middleware.ts +126 -0
- package/src/debug/recorder.ts +141 -0
- package/src/debug/types.ts +49 -0
- package/src/debug/ui.ts +393 -0
- package/src/di/async-module.ts +141 -0
- package/src/di/lifecycle.ts +117 -0
- package/src/di/module-registry.ts +75 -0
- package/src/index.ts +35 -0
- package/src/router/route.ts +20 -20
- package/src/swagger/generator.ts +100 -0
- package/src/testing/test-client.ts +112 -0
- package/src/testing/testing-module.ts +238 -0
- package/src/websocket/registry.ts +3 -16
- package/tests/auth/auth-decorators.test.ts +0 -1
- package/tests/auth/oauth2-service.test.ts +0 -1
- package/tests/cache/cache-decorators-extended.test.ts +0 -1
- package/tests/cache/cache-decorators.test.ts +0 -1
- package/tests/cache/cache-interceptors.test.ts +0 -1
- package/tests/cache/cache-module.test.ts +0 -1
- package/tests/cache/cache-service-proxy.test.ts +0 -1
- package/tests/client/client-generator.test.ts +142 -0
- package/tests/config/config-center-integration.test.ts +0 -1
- package/tests/config/config-module-extended.test.ts +0 -1
- package/tests/config/config-module.test.ts +0 -1
- package/tests/controller/controller.test.ts +0 -1
- package/tests/controller/param-binder.test.ts +0 -1
- package/tests/controller/path-combination.test.ts +0 -1
- package/tests/core/application.test.ts +34 -0
- package/tests/core/apply-decorators.test.ts +109 -0
- package/tests/core/cluster.test.ts +32 -0
- package/tests/dashboard/dashboard-module.test.ts +85 -0
- package/tests/database/database-module.test.ts +0 -1
- package/tests/database/orm.test.ts +0 -1
- package/tests/database/postgres-mysql-integration.test.ts +0 -1
- package/tests/database/transaction.test.ts +0 -1
- package/tests/debug/debug-module.test.ts +141 -0
- package/tests/di/async-module.test.ts +125 -0
- package/tests/di/container.test.ts +0 -1
- package/tests/di/lifecycle.test.ts +140 -0
- package/tests/error/error-handler.test.ts +0 -1
- package/tests/events/event-decorators.test.ts +0 -1
- package/tests/events/event-listener-scanner.test.ts +0 -1
- package/tests/events/event-module.test.ts +0 -1
- package/tests/extensions/logger-module.test.ts +0 -1
- package/tests/health/health-module.test.ts +0 -1
- package/tests/integration/oauth2-e2e.test.ts +0 -1
- package/tests/integration/session-e2e.test.ts +0 -1
- package/tests/interceptor/base-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/cache-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/log-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/permission-interceptor.test.ts +0 -1
- package/tests/interceptor/interceptor-advanced-integration.test.ts +0 -1
- package/tests/interceptor/interceptor-chain.test.ts +0 -1
- package/tests/interceptor/interceptor-integration.test.ts +0 -1
- package/tests/interceptor/interceptor-metadata.test.ts +0 -1
- package/tests/interceptor/interceptor-registry.test.ts +0 -1
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -1
- package/tests/metrics/metrics-module.test.ts +0 -1
- package/tests/microservice/config-center.test.ts +0 -1
- package/tests/microservice/service-client-decorators.test.ts +0 -1
- package/tests/microservice/service-registry-decorators.test.ts +0 -1
- package/tests/microservice/service-registry.test.ts +0 -1
- package/tests/middleware/builtin/middleware-builtin-extended.test.ts +0 -1
- package/tests/middleware/builtin/rate-limit.test.ts +0 -1
- package/tests/middleware/middleware-decorators.test.ts +0 -1
- package/tests/middleware/middleware-pipeline.test.ts +0 -1
- package/tests/middleware/middleware.test.ts +0 -1
- package/tests/perf/optimization.test.ts +0 -1
- package/tests/queue/queue-decorators.test.ts +0 -1
- package/tests/queue/queue-module.test.ts +0 -1
- package/tests/queue/queue-service.test.ts +0 -1
- package/tests/router/router-decorators.test.ts +0 -1
- package/tests/router/router-extended.test.ts +0 -1
- package/tests/security/guards/guards-integration.test.ts +0 -1
- package/tests/security/guards/guards.test.ts +0 -1
- package/tests/security/guards/reflector.test.ts +0 -1
- package/tests/security/security-filter.test.ts +0 -1
- package/tests/security/security-module-extended.test.ts +0 -1
- package/tests/security/security-module.test.ts +0 -1
- package/tests/session/session-decorators.test.ts +0 -1
- package/tests/session/session-module.test.ts +0 -1
- package/tests/swagger/decorators.test.ts +0 -1
- package/tests/swagger/swagger-module.test.ts +0 -1
- package/tests/swagger/ui.test.ts +0 -1
- package/tests/testing/testing-module.test.ts +129 -0
- package/tests/validation/class-validator.test.ts +0 -1
- package/tests/validation/controller-validation.test.ts +0 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Application } from '../core/application';
|
|
2
|
+
|
|
3
|
+
interface RequestOptions {
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body?: unknown;
|
|
6
|
+
query?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TestResponse {
|
|
10
|
+
status: number;
|
|
11
|
+
headers: Headers;
|
|
12
|
+
body: unknown;
|
|
13
|
+
text: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* HTTP 测试客户端
|
|
19
|
+
* 基于 fetch API,提供简洁的 HTTP 请求方法
|
|
20
|
+
*/
|
|
21
|
+
export class TestHttpClient {
|
|
22
|
+
private readonly baseUrl: string;
|
|
23
|
+
private readonly app: Application;
|
|
24
|
+
|
|
25
|
+
public constructor(baseUrl: string, app: Application) {
|
|
26
|
+
this.baseUrl = baseUrl;
|
|
27
|
+
this.app = app;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 发送 GET 请求
|
|
32
|
+
*/
|
|
33
|
+
public async get(path: string, options?: RequestOptions): Promise<TestResponse> {
|
|
34
|
+
return this.request('GET', path, options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 发送 POST 请求
|
|
39
|
+
*/
|
|
40
|
+
public async post(path: string, options?: RequestOptions): Promise<TestResponse> {
|
|
41
|
+
return this.request('POST', path, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 发送 PUT 请求
|
|
46
|
+
*/
|
|
47
|
+
public async put(path: string, options?: RequestOptions): Promise<TestResponse> {
|
|
48
|
+
return this.request('PUT', path, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 发送 DELETE 请求
|
|
53
|
+
*/
|
|
54
|
+
public async delete(path: string, options?: RequestOptions): Promise<TestResponse> {
|
|
55
|
+
return this.request('DELETE', path, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 发送 PATCH 请求
|
|
60
|
+
*/
|
|
61
|
+
public async patch(path: string, options?: RequestOptions): Promise<TestResponse> {
|
|
62
|
+
return this.request('PATCH', path, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 关闭测试服务器
|
|
67
|
+
*/
|
|
68
|
+
public async close(): Promise<void> {
|
|
69
|
+
await this.app.stop();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async request(method: string, path: string, options?: RequestOptions): Promise<TestResponse> {
|
|
73
|
+
let url = `${this.baseUrl}${path}`;
|
|
74
|
+
|
|
75
|
+
if (options?.query) {
|
|
76
|
+
const params = new URLSearchParams(options.query);
|
|
77
|
+
url += `?${params.toString()}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fetchOptions: RequestInit = {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
...(options?.headers || {}),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (options?.body && method !== 'GET') {
|
|
89
|
+
fetchOptions.body = typeof options.body === 'string'
|
|
90
|
+
? options.body
|
|
91
|
+
: JSON.stringify(options.body);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const response = await fetch(url, fetchOptions);
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
|
|
97
|
+
let body: unknown;
|
|
98
|
+
try {
|
|
99
|
+
body = JSON.parse(text);
|
|
100
|
+
} catch {
|
|
101
|
+
body = text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
status: response.status,
|
|
106
|
+
headers: response.headers,
|
|
107
|
+
body,
|
|
108
|
+
text,
|
|
109
|
+
ok: response.ok,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
import { Application, type ApplicationOptions } from '../core/application';
|
|
4
|
+
import { Container } from '../di/container';
|
|
5
|
+
import { ModuleRegistry } from '../di/module-registry';
|
|
6
|
+
import { ControllerRegistry } from '../controller/controller';
|
|
7
|
+
import { RouteRegistry } from '../router/registry';
|
|
8
|
+
import { MODULE_METADATA_KEY, type ModuleMetadata, type ModuleClass, type ModuleProvider, type ProviderToken } from '../di/module';
|
|
9
|
+
import type { Constructor } from '../core/types';
|
|
10
|
+
import { TestHttpClient } from './test-client';
|
|
11
|
+
|
|
12
|
+
interface ProviderOverride {
|
|
13
|
+
token: ProviderToken;
|
|
14
|
+
useValue?: unknown;
|
|
15
|
+
useClass?: Constructor<unknown>;
|
|
16
|
+
useFactory?: () => unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 测试模块构建器
|
|
21
|
+
* 提供流畅的 API 来创建测试用的隔离模块环境
|
|
22
|
+
*/
|
|
23
|
+
export class TestingModuleBuilder {
|
|
24
|
+
private readonly metadata: ModuleMetadata;
|
|
25
|
+
private readonly overrides: ProviderOverride[] = [];
|
|
26
|
+
|
|
27
|
+
public constructor(metadata: ModuleMetadata) {
|
|
28
|
+
this.metadata = { ...metadata };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 覆盖指定 provider
|
|
33
|
+
* @param token - 要覆盖的 provider token
|
|
34
|
+
*/
|
|
35
|
+
public overrideProvider(token: ProviderToken): ProviderOverrideBuilder {
|
|
36
|
+
return new ProviderOverrideBuilder(this, token);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 编译测试模块,返回可用的 TestingModule 实例
|
|
41
|
+
*/
|
|
42
|
+
public async compile(): Promise<TestingModule> {
|
|
43
|
+
const providers = this.buildProviders();
|
|
44
|
+
return new TestingModule(this.metadata, providers, this.overrides);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @internal */
|
|
48
|
+
public addOverride(override: ProviderOverride): void {
|
|
49
|
+
this.overrides.push(override);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private buildProviders(): ModuleProvider[] {
|
|
53
|
+
const providers = [...(this.metadata.providers || [])];
|
|
54
|
+
const overrideMap = new Map<string | symbol, ProviderOverride>();
|
|
55
|
+
|
|
56
|
+
for (const override of this.overrides) {
|
|
57
|
+
const key = typeof override.token === 'function'
|
|
58
|
+
? override.token.name
|
|
59
|
+
: override.token;
|
|
60
|
+
overrideMap.set(key as string | symbol, override);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result: ModuleProvider[] = [];
|
|
64
|
+
|
|
65
|
+
for (const provider of providers) {
|
|
66
|
+
const token = typeof provider === 'function'
|
|
67
|
+
? provider.name
|
|
68
|
+
: (provider as { provide?: ProviderToken }).provide;
|
|
69
|
+
const key = typeof token === 'function' ? token.name : token;
|
|
70
|
+
|
|
71
|
+
const override = overrideMap.get(key as string | symbol);
|
|
72
|
+
if (override) {
|
|
73
|
+
if (override.useValue !== undefined) {
|
|
74
|
+
result.push({ provide: override.token, useValue: override.useValue });
|
|
75
|
+
} else if (override.useClass) {
|
|
76
|
+
result.push({ provide: override.token, useClass: override.useClass });
|
|
77
|
+
} else if (override.useFactory) {
|
|
78
|
+
result.push({ provide: override.token, useFactory: override.useFactory as (container: Container) => unknown });
|
|
79
|
+
}
|
|
80
|
+
overrideMap.delete(key as string | symbol);
|
|
81
|
+
} else {
|
|
82
|
+
result.push(provider);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const override of overrideMap.values()) {
|
|
87
|
+
if (override.useValue !== undefined) {
|
|
88
|
+
result.push({ provide: override.token, useValue: override.useValue });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class ProviderOverrideBuilder {
|
|
97
|
+
private readonly builder: TestingModuleBuilder;
|
|
98
|
+
private readonly token: ProviderToken;
|
|
99
|
+
|
|
100
|
+
public constructor(builder: TestingModuleBuilder, token: ProviderToken) {
|
|
101
|
+
this.builder = builder;
|
|
102
|
+
this.token = token;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 使用固定值覆盖
|
|
107
|
+
*/
|
|
108
|
+
public useValue(value: unknown): TestingModuleBuilder {
|
|
109
|
+
this.builder.addOverride({ token: this.token, useValue: value });
|
|
110
|
+
return this.builder;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 使用替代类覆盖
|
|
115
|
+
*/
|
|
116
|
+
public useClass(cls: Constructor<unknown>): TestingModuleBuilder {
|
|
117
|
+
this.builder.addOverride({ token: this.token, useClass: cls });
|
|
118
|
+
return this.builder;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 使用工厂函数覆盖
|
|
123
|
+
*/
|
|
124
|
+
public useFactory(factory: () => unknown): TestingModuleBuilder {
|
|
125
|
+
this.builder.addOverride({ token: this.token, useFactory: factory });
|
|
126
|
+
return this.builder;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 编译后的测试模块
|
|
132
|
+
* 提供 DI 容器访问和 HTTP 测试客户端创建
|
|
133
|
+
*/
|
|
134
|
+
export class TestingModule {
|
|
135
|
+
private readonly metadata: ModuleMetadata;
|
|
136
|
+
private readonly providers: ModuleProvider[];
|
|
137
|
+
private readonly overrides: ProviderOverride[];
|
|
138
|
+
private app?: Application;
|
|
139
|
+
private container?: Container;
|
|
140
|
+
|
|
141
|
+
public constructor(
|
|
142
|
+
metadata: ModuleMetadata,
|
|
143
|
+
providers: ModuleProvider[],
|
|
144
|
+
overrides: ProviderOverride[],
|
|
145
|
+
) {
|
|
146
|
+
this.metadata = metadata;
|
|
147
|
+
this.providers = providers;
|
|
148
|
+
this.overrides = overrides;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 从容器中获取 provider 实例
|
|
153
|
+
*/
|
|
154
|
+
public get<T>(token: Constructor<T> | string | symbol): T {
|
|
155
|
+
return this.getContainer().resolve<T>(token);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 创建一个 Application 实例并注册所有 providers、controllers
|
|
160
|
+
* @param options - 应用配置
|
|
161
|
+
*/
|
|
162
|
+
public createApplication(options: ApplicationOptions = {}): Application {
|
|
163
|
+
if (this.app) return this.app;
|
|
164
|
+
|
|
165
|
+
this.app = new Application({
|
|
166
|
+
enableSignalHandlers: false,
|
|
167
|
+
...options,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const container = this.app.getContainer();
|
|
171
|
+
this.container = container;
|
|
172
|
+
|
|
173
|
+
for (const provider of this.providers) {
|
|
174
|
+
if (typeof provider === 'function') {
|
|
175
|
+
container.register(provider);
|
|
176
|
+
} else if ('useValue' in provider && provider.provide) {
|
|
177
|
+
container.registerInstance(provider.provide, provider.useValue);
|
|
178
|
+
} else if ('useFactory' in provider && provider.provide) {
|
|
179
|
+
container.register(provider.provide as Constructor<unknown>, {
|
|
180
|
+
factory: () => (provider as { useFactory: (container: Container) => unknown }).useFactory(container),
|
|
181
|
+
});
|
|
182
|
+
} else if ('useClass' in provider) {
|
|
183
|
+
const token = (provider as { provide?: ProviderToken }).provide ?? (provider as { useClass: Constructor<unknown> }).useClass;
|
|
184
|
+
container.register(token as Constructor<unknown>, {
|
|
185
|
+
implementation: (provider as { useClass: Constructor<unknown> }).useClass,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.metadata.imports) {
|
|
191
|
+
for (const moduleClass of this.metadata.imports) {
|
|
192
|
+
this.app.registerModule(moduleClass);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.metadata.controllers) {
|
|
197
|
+
for (const ctrl of this.metadata.controllers) {
|
|
198
|
+
this.app.registerController(ctrl);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return this.app;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 创建 HTTP 测试客户端
|
|
207
|
+
* 自动启动应用,获取随机端口,返回 TestHttpClient
|
|
208
|
+
*/
|
|
209
|
+
public async createHttpClient(options: ApplicationOptions = {}): Promise<TestHttpClient> {
|
|
210
|
+
const app = this.createApplication(options);
|
|
211
|
+
await app.listen(0);
|
|
212
|
+
const server = app.getServer();
|
|
213
|
+
const port = server?.getPort() ?? 3000;
|
|
214
|
+
return new TestHttpClient(`http://localhost:${port}`, app);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 获取 DI 容器
|
|
219
|
+
*/
|
|
220
|
+
public getContainer(): Container {
|
|
221
|
+
if (this.container) return this.container;
|
|
222
|
+
this.createApplication();
|
|
223
|
+
return this.container!;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 测试工具入口
|
|
229
|
+
*/
|
|
230
|
+
export class Test {
|
|
231
|
+
/**
|
|
232
|
+
* 创建测试模块
|
|
233
|
+
* @param metadata - 模块元数据
|
|
234
|
+
*/
|
|
235
|
+
public static createTestingModule(metadata: ModuleMetadata): TestingModuleBuilder {
|
|
236
|
+
return new TestingModuleBuilder(metadata);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -46,12 +46,7 @@ export class WebSocketGatewayRegistry {
|
|
|
46
46
|
return WebSocketGatewayRegistry.instance;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
* 解析路径,生成匹配模式和参数名列表
|
|
51
|
-
* @param path - 路由路径
|
|
52
|
-
* @returns 匹配模式和参数名列表
|
|
53
|
-
*/
|
|
54
|
-
private parsePath(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
49
|
+
private static parsePath(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
55
50
|
const paramNames: string[] = [];
|
|
56
51
|
const patternString = path
|
|
57
52
|
.replace(/:([^/]+)/g, (_, paramName) => {
|
|
@@ -60,8 +55,7 @@ export class WebSocketGatewayRegistry {
|
|
|
60
55
|
})
|
|
61
56
|
.replace(/\*/g, '.*');
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
return { pattern, paramNames };
|
|
58
|
+
return { pattern: new RegExp(`^${patternString}$`), paramNames };
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
public register(gatewayClass: Constructor<unknown>): void {
|
|
@@ -79,8 +73,7 @@ export class WebSocketGatewayRegistry {
|
|
|
79
73
|
this.container.register(gatewayClass);
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
const { pattern, paramNames } = this.parsePath(metadata.path);
|
|
76
|
+
const { pattern, paramNames } = WebSocketGatewayRegistry.parsePath(metadata.path);
|
|
84
77
|
const isStatic = !metadata.path.includes(':') && !metadata.path.includes('*');
|
|
85
78
|
|
|
86
79
|
const definition: GatewayDefinition = {
|
|
@@ -94,7 +87,6 @@ export class WebSocketGatewayRegistry {
|
|
|
94
87
|
|
|
95
88
|
this.gateways.set(metadata.path, definition);
|
|
96
89
|
|
|
97
|
-
// 分别存储静态和动态路由
|
|
98
90
|
if (isStatic) {
|
|
99
91
|
this.staticGateways.set(metadata.path, definition);
|
|
100
92
|
} else {
|
|
@@ -108,12 +100,10 @@ export class WebSocketGatewayRegistry {
|
|
|
108
100
|
* @returns 是否有匹配的网关
|
|
109
101
|
*/
|
|
110
102
|
public hasGateway(path: string): boolean {
|
|
111
|
-
// 先检查静态路由
|
|
112
103
|
if (this.staticGateways.has(path)) {
|
|
113
104
|
return true;
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
// 遍历动态路由
|
|
117
107
|
for (const gateway of this.dynamicGateways) {
|
|
118
108
|
if (gateway.pattern.test(path)) {
|
|
119
109
|
return true;
|
|
@@ -135,17 +125,14 @@ export class WebSocketGatewayRegistry {
|
|
|
135
125
|
* @returns 匹配的网关定义和路径参数
|
|
136
126
|
*/
|
|
137
127
|
private getGateway(path: string): { definition: GatewayDefinition; params: Record<string, string> } | undefined {
|
|
138
|
-
// 先检查静态路由
|
|
139
128
|
const staticGateway = this.staticGateways.get(path);
|
|
140
129
|
if (staticGateway) {
|
|
141
130
|
return { definition: staticGateway, params: {} };
|
|
142
131
|
}
|
|
143
132
|
|
|
144
|
-
// 遍历动态路由
|
|
145
133
|
for (const gateway of this.dynamicGateways) {
|
|
146
134
|
const match = path.match(gateway.pattern);
|
|
147
135
|
if (match) {
|
|
148
|
-
// 提取路径参数
|
|
149
136
|
const params: Record<string, string> = {};
|
|
150
137
|
for (let i = 0; i < gateway.paramNames.length; i++) {
|
|
151
138
|
params[gateway.paramNames[i]] = match[i + 1] ?? '';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, expect, test, afterEach } from 'bun:test';
|
|
3
|
+
import { ClientGenerator } from '../../src/client/generator';
|
|
4
|
+
import { createClient } from '../../src/client/runtime';
|
|
5
|
+
import { Application } from '../../src/core/application';
|
|
6
|
+
import { Controller } from '../../src/controller';
|
|
7
|
+
import { GET, POST } from '../../src/router/decorators';
|
|
8
|
+
import { Param, Body } from '../../src/controller';
|
|
9
|
+
|
|
10
|
+
@Controller('/api/users')
|
|
11
|
+
class UserController {
|
|
12
|
+
@GET('/')
|
|
13
|
+
public listUsers(): object {
|
|
14
|
+
return [{ id: 1, name: 'Alice' }];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@GET('/:id')
|
|
18
|
+
public getUser(@Param('id') id: string): object {
|
|
19
|
+
return { id, name: 'Alice' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@POST('/')
|
|
23
|
+
public createUser(@Body() body: unknown): object {
|
|
24
|
+
return { ...(body as object), id: '99' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Controller('/api/posts')
|
|
29
|
+
class PostController {
|
|
30
|
+
@GET('/')
|
|
31
|
+
public listPosts(): object {
|
|
32
|
+
return [{ id: 1, title: 'Hello' }];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('ClientGenerator', () => {
|
|
37
|
+
let app: Application | undefined;
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
if (app) {
|
|
41
|
+
await app.stop();
|
|
42
|
+
app = undefined;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should generate route manifest from registered controllers', () => {
|
|
47
|
+
app = new Application({ enableSignalHandlers: false });
|
|
48
|
+
app.registerController(UserController);
|
|
49
|
+
app.registerController(PostController);
|
|
50
|
+
|
|
51
|
+
const manifest = ClientGenerator.generate();
|
|
52
|
+
|
|
53
|
+
expect(manifest.routes.length).toBeGreaterThanOrEqual(4);
|
|
54
|
+
|
|
55
|
+
const userList = manifest.routes.find(
|
|
56
|
+
(r) => r.controllerName === 'UserController' && r.methodName === 'listUsers',
|
|
57
|
+
);
|
|
58
|
+
expect(userList).toBeDefined();
|
|
59
|
+
expect(userList!.method).toBe('GET');
|
|
60
|
+
expect(userList!.path).toMatch(/^\/api\/users\/?$/);
|
|
61
|
+
|
|
62
|
+
const userGet = manifest.routes.find(
|
|
63
|
+
(r) => r.controllerName === 'UserController' && r.methodName === 'getUser',
|
|
64
|
+
);
|
|
65
|
+
expect(userGet).toBeDefined();
|
|
66
|
+
expect(userGet!.path).toBe('/api/users/:id');
|
|
67
|
+
|
|
68
|
+
const userCreate = manifest.routes.find(
|
|
69
|
+
(r) => r.controllerName === 'UserController' && r.methodName === 'createUser',
|
|
70
|
+
);
|
|
71
|
+
expect(userCreate).toBeDefined();
|
|
72
|
+
expect(userCreate!.method).toBe('POST');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should generate valid JSON', () => {
|
|
76
|
+
app = new Application({ enableSignalHandlers: false });
|
|
77
|
+
app.registerController(UserController);
|
|
78
|
+
|
|
79
|
+
const json = ClientGenerator.generateJSON();
|
|
80
|
+
const parsed = JSON.parse(json);
|
|
81
|
+
expect(parsed.routes).toBeArray();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('createClient', () => {
|
|
86
|
+
let app: Application | undefined;
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
if (app) {
|
|
90
|
+
await app.stop();
|
|
91
|
+
app = undefined;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should create client from manifest and make requests', async () => {
|
|
96
|
+
app = new Application({ enableSignalHandlers: false });
|
|
97
|
+
app.registerController(UserController);
|
|
98
|
+
app.registerController(PostController);
|
|
99
|
+
await app.listen(0);
|
|
100
|
+
|
|
101
|
+
const port = app.getServer()!.getPort();
|
|
102
|
+
const manifest = ClientGenerator.generate();
|
|
103
|
+
|
|
104
|
+
const client = createClient(manifest, {
|
|
105
|
+
baseUrl: `http://localhost:${port}`,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(client.user).toBeDefined();
|
|
109
|
+
expect(client.post).toBeDefined();
|
|
110
|
+
|
|
111
|
+
const users = await client.user.listUsers();
|
|
112
|
+
expect(users).toEqual([{ id: 1, name: 'Alice' }]);
|
|
113
|
+
|
|
114
|
+
const user = await client.user.getUser({ params: { id: '42' } });
|
|
115
|
+
expect(user).toEqual({ id: '42', name: 'Alice' });
|
|
116
|
+
|
|
117
|
+
const created = await client.user.createUser({
|
|
118
|
+
body: { name: 'Bob' },
|
|
119
|
+
});
|
|
120
|
+
expect(created).toEqual({ name: 'Bob', id: '99' });
|
|
121
|
+
|
|
122
|
+
const posts = await client.post.listPosts();
|
|
123
|
+
expect(posts).toEqual([{ id: 1, title: 'Hello' }]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should support custom headers', async () => {
|
|
127
|
+
app = new Application({ enableSignalHandlers: false });
|
|
128
|
+
app.registerController(UserController);
|
|
129
|
+
await app.listen(0);
|
|
130
|
+
|
|
131
|
+
const port = app.getServer()!.getPort();
|
|
132
|
+
const manifest = ClientGenerator.generate();
|
|
133
|
+
|
|
134
|
+
const client = createClient(manifest, {
|
|
135
|
+
baseUrl: `http://localhost:${port}`,
|
|
136
|
+
headers: { 'X-Custom': 'test' },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const users = await client.user.listUsers();
|
|
140
|
+
expect(users).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -53,5 +53,39 @@ describe('Application', () => {
|
|
|
53
53
|
const data = await response.json();
|
|
54
54
|
expect(data.error).toBe('Not Found');
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
test('should accept reusePort option', async () => {
|
|
58
|
+
const port = getTestPort();
|
|
59
|
+
app = new Application({ port, reusePort: true });
|
|
60
|
+
await app.listen();
|
|
61
|
+
|
|
62
|
+
const server = app.getServer();
|
|
63
|
+
expect(server).toBeDefined();
|
|
64
|
+
expect(server?.isRunning()).toBe(true);
|
|
65
|
+
|
|
66
|
+
const response = await fetch(`http://localhost:${port}/api/ping`);
|
|
67
|
+
expect(response.status).toBe(404);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should allow two servers on same port with reusePort (Linux only)', async () => {
|
|
71
|
+
if (process.platform !== 'linux') {
|
|
72
|
+
console.log('Skipping reusePort dual-bind test: only works on Linux');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const port = getTestPort();
|
|
77
|
+
app = new Application({ port, reusePort: true });
|
|
78
|
+
await app.listen();
|
|
79
|
+
|
|
80
|
+
const app2 = new Application({ port, reusePort: true });
|
|
81
|
+
await app2.listen();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(`http://localhost:${port}/api/test`);
|
|
85
|
+
expect(response.status).toBe(404);
|
|
86
|
+
} finally {
|
|
87
|
+
await app2.stop();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
56
90
|
});
|
|
57
91
|
|