@dangao/bun-server 2.2.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.
- package/README.md +81 -3
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/ai/providers/google-provider.d.ts.map +1 -1
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
- package/dist/ai/providers/openai-provider.d.ts.map +1 -1
- package/dist/ai/service.d.ts.map +1 -1
- package/dist/ai/types.d.ts +5 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/auth/jwt.d.ts.map +1 -1
- package/dist/config/service.d.ts +0 -1
- package/dist/config/service.d.ts.map +1 -1
- package/dist/core/application.d.ts +30 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/server.d.ts +29 -9
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts.map +1 -1
- package/dist/database/connection-pool.d.ts +3 -3
- package/dist/database/connection-pool.d.ts.map +1 -1
- package/dist/database/sql-manager.d.ts +8 -4
- package/dist/database/sql-manager.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.d.ts +7 -3
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/debug/recorder.d.ts +0 -1
- package/dist/debug/recorder.d.ts.map +1 -1
- package/dist/files/static-middleware.d.ts.map +1 -1
- package/dist/files/storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40335 -3523
- package/dist/index.node.mjs +17689 -0
- package/dist/mcp/server.d.ts +5 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/middleware/builtin/static-file.d.ts +4 -2
- package/dist/middleware/builtin/static-file.d.ts.map +1 -1
- package/dist/platform/bun/crypto.d.ts +3 -0
- package/dist/platform/bun/crypto.d.ts.map +1 -0
- package/dist/platform/bun/fs.d.ts +3 -0
- package/dist/platform/bun/fs.d.ts.map +1 -0
- package/dist/platform/bun/http.d.ts +15 -0
- package/dist/platform/bun/http.d.ts.map +1 -0
- package/dist/platform/bun/index.d.ts +3 -0
- package/dist/platform/bun/index.d.ts.map +1 -0
- package/dist/platform/bun/parser.d.ts +3 -0
- package/dist/platform/bun/parser.d.ts.map +1 -0
- package/dist/platform/bun/process.d.ts +3 -0
- package/dist/platform/bun/process.d.ts.map +1 -0
- package/dist/platform/detector.d.ts +9 -0
- package/dist/platform/detector.d.ts.map +1 -0
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/node/crypto.d.ts +3 -0
- package/dist/platform/node/crypto.d.ts.map +1 -0
- package/dist/platform/node/fs.d.ts +3 -0
- package/dist/platform/node/fs.d.ts.map +1 -0
- package/dist/platform/node/http.d.ts +3 -0
- package/dist/platform/node/http.d.ts.map +1 -0
- package/dist/platform/node/index.d.ts +3 -0
- package/dist/platform/node/index.d.ts.map +1 -0
- package/dist/platform/node/parser.d.ts +3 -0
- package/dist/platform/node/parser.d.ts.map +1 -0
- package/dist/platform/node/process.d.ts +3 -0
- package/dist/platform/node/process.d.ts.map +1 -0
- package/dist/platform/runtime.d.ts +14 -0
- package/dist/platform/runtime.d.ts.map +1 -0
- package/dist/platform/types.d.ts +139 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -1
- package/dist/rag/service.d.ts.map +1 -1
- package/dist/request/response.d.ts +3 -1
- package/dist/request/response.d.ts.map +1 -1
- package/dist/security/guards/execution-context.d.ts +2 -2
- package/dist/security/guards/execution-context.d.ts.map +1 -1
- package/dist/security/guards/types.d.ts +2 -2
- package/dist/security/guards/types.d.ts.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/websocket/registry.d.ts +4 -4
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/deployment.md +31 -7
- package/docs/design/query-interceptor-design.md +381 -0
- package/docs/idle-timeout.md +101 -8
- package/docs/migration.md +43 -0
- package/docs/platform.md +299 -0
- package/docs/testing.md +60 -0
- package/docs/zh/deployment.md +30 -7
- package/docs/zh/idle-timeout.md +99 -6
- package/docs/zh/migration.md +42 -0
- package/docs/zh/platform.md +299 -0
- package/docs/zh/testing.md +60 -0
- package/package.json +24 -6
- package/src/ai/providers/anthropic-provider.ts +5 -2
- package/src/ai/providers/google-provider.ts +3 -0
- package/src/ai/providers/ollama-provider.ts +3 -0
- package/src/ai/providers/openai-provider.ts +5 -2
- package/src/ai/service.ts +17 -5
- package/src/ai/types.ts +5 -0
- package/src/auth/jwt.ts +4 -3
- package/src/config/service.ts +7 -6
- package/src/core/application.ts +38 -1
- package/src/core/cluster.ts +16 -14
- package/src/core/context.ts +7 -0
- package/src/core/server.ts +162 -46
- package/src/dashboard/controller.ts +3 -2
- package/src/database/connection-pool.ts +32 -20
- package/src/database/database-module.ts +1 -1
- package/src/database/db-proxy.ts +2 -2
- package/src/database/orm/transaction-manager.ts +1 -1
- package/src/database/sql-manager.ts +48 -13
- package/src/database/sqlite-adapter.ts +45 -12
- package/src/debug/recorder.ts +4 -3
- package/src/files/static-middleware.ts +3 -2
- package/src/files/storage.ts +2 -1
- package/src/index.ts +13 -0
- package/src/mcp/server.ts +6 -15
- package/src/middleware/builtin/static-file.ts +8 -5
- package/src/platform/bun/crypto.ts +30 -0
- package/src/platform/bun/fs.ts +52 -0
- package/src/platform/bun/http.ts +106 -0
- package/src/platform/bun/index.ts +17 -0
- package/src/platform/bun/parser.ts +19 -0
- package/src/platform/bun/process.ts +37 -0
- package/src/platform/detector.ts +36 -0
- package/src/platform/index.ts +20 -0
- package/src/platform/node/crypto.ts +40 -0
- package/src/platform/node/fs.ts +115 -0
- package/src/platform/node/http.ts +196 -0
- package/src/platform/node/index.ts +17 -0
- package/src/platform/node/parser.ts +34 -0
- package/src/platform/node/process.ts +51 -0
- package/src/platform/runtime.ts +50 -0
- package/src/platform/types.ts +150 -0
- package/src/prompt/stores/file-store.ts +6 -5
- package/src/rag/service.ts +2 -1
- package/src/request/response.ts +7 -4
- package/src/security/guards/execution-context.ts +4 -4
- package/src/security/guards/types.ts +2 -2
- package/src/swagger/generator.ts +2 -1
- package/src/websocket/registry.ts +6 -7
- package/tests/controller/path-combination.test.ts +196 -2
- package/tests/files/static-middleware.test.ts +5 -2
- package/tests/middleware/static-file.test.ts +5 -2
- package/tests/platform/bun/crypto.test.ts +8 -0
- package/tests/platform/bun/database.test.ts +8 -0
- package/tests/platform/bun/fs.test.ts +8 -0
- package/tests/platform/bun/parser.test.ts +8 -0
- package/tests/platform/bun/process.test.ts +8 -0
- package/tests/platform/bun/websocket.test.ts +8 -0
- package/tests/platform/detector.test.ts +57 -0
- package/tests/platform/node/build-smoke.test.ts +92 -0
- package/tests/platform/node/crypto.test.ts +9 -0
- package/tests/platform/node/database.test.ts +9 -0
- package/tests/platform/node/fs.test.ts +9 -0
- package/tests/platform/node/parser.test.ts +9 -0
- package/tests/platform/node/process.test.ts +9 -0
- package/tests/platform/node/websocket.test.ts +9 -0
- package/tests/platform/shared/crypto.cases.ts +49 -0
- package/tests/platform/shared/database.cases.ts +43 -0
- package/tests/platform/shared/fs.cases.ts +82 -0
- package/tests/platform/shared/parser.cases.ts +55 -0
- package/tests/platform/shared/process.cases.ts +26 -0
- package/tests/platform/shared/suite.ts +33 -0
- package/tests/platform/shared/websocket.cases.ts +61 -0
- package/tests/request/response.test.ts +5 -2
- package/tests/router/router-extended.test.ts +53 -0
|
@@ -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
|
|
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
|
|
90
|
-
const files =
|
|
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
|
|
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
|
|
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
|
}
|
package/src/rag/service.ts
CHANGED
|
@@ -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 =
|
|
124
|
+
const file = getRuntime().fs.file(source.path);
|
|
124
125
|
return file.text();
|
|
125
126
|
}
|
|
126
127
|
|
package/src/request/response.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
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
|
|
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 {
|
|
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:
|
|
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():
|
|
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:
|
|
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 {
|
|
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():
|
|
46
|
+
getClient(): IWebSocket<unknown>;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* 获取消息数据
|
package/src/swagger/generator.ts
CHANGED
|
@@ -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
|
|
315
|
+
return getRuntime().parser.renderMarkdown(md);
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
/**
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
/**
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
import { createStaticFileMiddleware } from '../../src/files/static-middleware';
|
|
7
7
|
import { Context } from '../../src/core/context';
|
|
8
|
+
import { initRuntime } from '../../src/platform/runtime';
|
|
9
|
+
|
|
10
|
+
initRuntime('bun');
|
|
8
11
|
|
|
9
12
|
describe('Static File Middleware', () => {
|
|
10
13
|
let root: string;
|
|
11
14
|
|
|
12
15
|
beforeEach(async () => {
|
|
13
16
|
root = await mkdtemp(join(tmpdir(), 'bun-static-'));
|
|
14
|
-
await
|
|
17
|
+
await writeFile(join(root, 'hello.txt'), 'hello world', 'utf-8');
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
afterEach(async () => {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdir, rm } from 'fs/promises';
|
|
2
|
+
import { mkdir, rm, writeFile as fsWriteFile } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
5
|
import { createStaticFileMiddleware } from '../../src/middleware/builtin/static-file';
|
|
6
6
|
import { Context } from '../../src/core/context';
|
|
7
|
+
import { initRuntime } from '../../src/platform/runtime';
|
|
8
|
+
|
|
9
|
+
initRuntime('bun');
|
|
7
10
|
|
|
8
11
|
const TMP_DIR = join(process.cwd(), 'tmp-static-test');
|
|
9
12
|
|
|
10
13
|
async function writeFile(path: string, content: string): Promise<void> {
|
|
11
|
-
await
|
|
14
|
+
await fsWriteFile(path, content, 'utf-8');
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
describe('StaticFileMiddleware', () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { initRuntime, getRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runCryptoCases } from '../shared/crypto.cases';
|
|
4
|
+
|
|
5
|
+
describe('BunCryptoAdapter', () => {
|
|
6
|
+
initRuntime('bun');
|
|
7
|
+
runCryptoCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().crypto);
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { initRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runDatabaseCases } from '../shared/database.cases';
|
|
4
|
+
|
|
5
|
+
describe('BunDatabase', () => {
|
|
6
|
+
initRuntime('bun');
|
|
7
|
+
runDatabaseCases({ test, expect: expect as any, beforeEach: () => {} }, 'bun');
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { initRuntime, getRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runFsCases } from '../shared/fs.cases';
|
|
4
|
+
|
|
5
|
+
describe('BunFsAdapter', () => {
|
|
6
|
+
initRuntime('bun');
|
|
7
|
+
runFsCases({ test, expect: expect as any, beforeEach }, () => getRuntime().fs);
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { initRuntime, getRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runParserCases } from '../shared/parser.cases';
|
|
4
|
+
|
|
5
|
+
describe('BunParserAdapter', () => {
|
|
6
|
+
initRuntime('bun');
|
|
7
|
+
runParserCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().parser);
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { initRuntime, getRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runProcessCases } from '../shared/process.cases';
|
|
4
|
+
|
|
5
|
+
describe('BunProcessAdapter', () => {
|
|
6
|
+
initRuntime('bun');
|
|
7
|
+
runProcessCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().process);
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { initRuntime, getRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runWebSocketCases } from '../shared/websocket.cases';
|
|
4
|
+
|
|
5
|
+
describe('BunWebSocket', () => {
|
|
6
|
+
initRuntime('bun');
|
|
7
|
+
runWebSocketCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime());
|
|
8
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { resolvePlatform } from '../../src/platform/detector';
|
|
3
|
+
|
|
4
|
+
describe('resolvePlatform priority chain', () => {
|
|
5
|
+
const originalArgv = process.argv.slice();
|
|
6
|
+
const originalEnv = process.env['BUN_SERVER_PLATFORM'];
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
process.argv.length = 0;
|
|
10
|
+
for (const arg of originalArgv) process.argv.push(arg);
|
|
11
|
+
if (originalEnv === undefined) {
|
|
12
|
+
delete process.env['BUN_SERVER_PLATFORM'];
|
|
13
|
+
} else {
|
|
14
|
+
process.env['BUN_SERVER_PLATFORM'] = originalEnv;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('bootstrap config takes highest priority over CLI arg', () => {
|
|
19
|
+
process.argv.push('--platform=node');
|
|
20
|
+
expect(resolvePlatform('bun')).toBe('bun');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('CLI arg --platform=node', () => {
|
|
24
|
+
delete process.env['BUN_SERVER_PLATFORM'];
|
|
25
|
+
process.argv.push('--platform=node');
|
|
26
|
+
expect(resolvePlatform()).toBe('node');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('CLI arg --platform=bun', () => {
|
|
30
|
+
delete process.env['BUN_SERVER_PLATFORM'];
|
|
31
|
+
process.argv.push('--platform=bun');
|
|
32
|
+
expect(resolvePlatform()).toBe('bun');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('env var BUN_SERVER_PLATFORM=node', () => {
|
|
36
|
+
process.env['BUN_SERVER_PLATFORM'] = 'node';
|
|
37
|
+
expect(resolvePlatform()).toBe('node');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('env var BUN_SERVER_PLATFORM=bun', () => {
|
|
41
|
+
process.env['BUN_SERVER_PLATFORM'] = 'bun';
|
|
42
|
+
expect(resolvePlatform()).toBe('bun');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('CLI arg takes priority over env var', () => {
|
|
46
|
+
process.env['BUN_SERVER_PLATFORM'] = 'node';
|
|
47
|
+
process.argv.push('--platform=bun');
|
|
48
|
+
expect(resolvePlatform()).toBe('bun');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('auto-detect returns bun when running in Bun', () => {
|
|
52
|
+
delete process.env['BUN_SERVER_PLATFORM'];
|
|
53
|
+
// We're running in Bun (bun:test), so auto-detect should return 'bun'
|
|
54
|
+
const result = resolvePlatform();
|
|
55
|
+
expect(result).toBe('bun');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 烟雾测试:验证 bun build --target=node 的输出能被 Node.js 原生运行
|
|
9
|
+
*
|
|
10
|
+
* 测试流程:
|
|
11
|
+
* 1. 创建最小化 app fixture
|
|
12
|
+
* 2. 使用 bun build --target=node 编译
|
|
13
|
+
* 3. 用 node 运行编译产物
|
|
14
|
+
* 4. 验证应用启动成功
|
|
15
|
+
*/
|
|
16
|
+
describe('Build Smoke Test (bun build --target=node)', () => {
|
|
17
|
+
test('compiled output runs on Node.js', async () => {
|
|
18
|
+
const tmpDir = join(tmpdir(), `build-smoke-${Date.now()}`);
|
|
19
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
const outDir = join(tmpDir, 'out');
|
|
22
|
+
mkdirSync(outDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
// Create a minimal fixture app using the framework
|
|
25
|
+
const fixturePath = join(tmpDir, 'fixture.ts');
|
|
26
|
+
const pkgRoot = resolve(__dirname, '../../..');
|
|
27
|
+
|
|
28
|
+
writeFileSync(fixturePath, `
|
|
29
|
+
import { Application } from '${pkgRoot}/src/index.ts';
|
|
30
|
+
import { Module } from '${pkgRoot}/src/index.ts';
|
|
31
|
+
|
|
32
|
+
@Module({})
|
|
33
|
+
class AppModule {}
|
|
34
|
+
|
|
35
|
+
const app = new Application({ platform: 'node' });
|
|
36
|
+
app.registerModule(AppModule);
|
|
37
|
+
app.listen(0).then(() => {
|
|
38
|
+
const server = app.getServer()?.getServer();
|
|
39
|
+
console.log('SMOKE_OK port=' + (server?.port ?? 0));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}).catch((err) => {
|
|
42
|
+
console.error('SMOKE_FAIL', err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
const outFile = join(outDir, 'app.cjs');
|
|
48
|
+
|
|
49
|
+
// Build with bun
|
|
50
|
+
const buildResult = await runCommand('bun', [
|
|
51
|
+
'build',
|
|
52
|
+
fixturePath,
|
|
53
|
+
'--target=node',
|
|
54
|
+
'--outfile=' + outFile,
|
|
55
|
+
'--format=cjs',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
if (buildResult.exitCode !== 0) {
|
|
59
|
+
// Build may fail in CI without Bun; skip gracefully
|
|
60
|
+
console.warn('[build-smoke] bun build failed, skipping test:', buildResult.stderr);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Run compiled file with node
|
|
65
|
+
const nodeResult = await runCommand('node', [outFile], 5000);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
69
|
+
} catch {
|
|
70
|
+
// ignore cleanup errors
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(nodeResult.exitCode).toBe(0);
|
|
74
|
+
expect(nodeResult.stdout).toContain('SMOKE_OK');
|
|
75
|
+
}, 30000);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function runCommand(
|
|
79
|
+
cmd: string,
|
|
80
|
+
args: string[],
|
|
81
|
+
timeout = 15000,
|
|
82
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const child = spawn(cmd, args, { timeout });
|
|
85
|
+
let stdout = '';
|
|
86
|
+
let stderr = '';
|
|
87
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
88
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
89
|
+
child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
90
|
+
child.on('error', (err) => resolve({ exitCode: 1, stdout, stderr: err.message }));
|
|
91
|
+
});
|
|
92
|
+
}
|