@dangao/bun-server 2.3.0 → 3.0.1
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/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 +13 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/server.d.ts +12 -9
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts.map +1 -1
- package/dist/database/connection-manager.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/service.d.ts +2 -1
- package/dist/database/service.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 +39487 -3496
- package/dist/index.node.mjs +17723 -0
- 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/idle-timeout.md +6 -4
- 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 +6 -4
- 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 -7
- package/src/auth/jwt.ts +4 -3
- package/src/config/service.ts +7 -6
- package/src/core/application.ts +19 -1
- package/src/core/cluster.ts +16 -14
- package/src/core/server.ts +48 -35
- package/src/dashboard/controller.ts +3 -2
- package/src/database/connection-manager.ts +19 -12
- package/src/database/connection-pool.ts +33 -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/service.ts +21 -5
- package/src/database/sql-manager.ts +48 -13
- package/src/database/sqlite-adapter.ts +54 -13
- 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/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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runCryptoCases } from '../shared/crypto.cases';
|
|
4
|
+
|
|
5
|
+
describe('NodeCryptoAdapter', () => {
|
|
6
|
+
_resetRuntime();
|
|
7
|
+
initRuntime('node');
|
|
8
|
+
runCryptoCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().crypto);
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { initRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runDatabaseCases } from '../shared/database.cases';
|
|
4
|
+
|
|
5
|
+
describe('NodeDatabase', () => {
|
|
6
|
+
_resetRuntime();
|
|
7
|
+
initRuntime('node');
|
|
8
|
+
runDatabaseCases({ test, expect: expect as any, beforeEach: () => {} }, 'node');
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runFsCases } from '../shared/fs.cases';
|
|
4
|
+
|
|
5
|
+
describe('NodeFsAdapter', () => {
|
|
6
|
+
_resetRuntime();
|
|
7
|
+
initRuntime('node');
|
|
8
|
+
runFsCases({ test, expect: expect as any, beforeEach }, () => getRuntime().fs);
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runParserCases } from '../shared/parser.cases';
|
|
4
|
+
|
|
5
|
+
describe('NodeParserAdapter', () => {
|
|
6
|
+
_resetRuntime();
|
|
7
|
+
initRuntime('node');
|
|
8
|
+
runParserCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().parser);
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runProcessCases } from '../shared/process.cases';
|
|
4
|
+
|
|
5
|
+
describe('NodeProcessAdapter', () => {
|
|
6
|
+
_resetRuntime();
|
|
7
|
+
initRuntime('node');
|
|
8
|
+
runProcessCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().process);
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
3
|
+
import { runWebSocketCases } from '../shared/websocket.cases';
|
|
4
|
+
|
|
5
|
+
describe('NodeWebSocket', () => {
|
|
6
|
+
_resetRuntime();
|
|
7
|
+
initRuntime('node');
|
|
8
|
+
runWebSocketCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime());
|
|
9
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TestSuite } from './suite';
|
|
2
|
+
import type { ICryptoAdapter } from '../../../src/platform/types';
|
|
3
|
+
|
|
4
|
+
export function runCryptoCases(suite: TestSuite, getAdapter: () => ICryptoAdapter): void {
|
|
5
|
+
const { test, expect } = suite;
|
|
6
|
+
|
|
7
|
+
test('sha256 hex digest', () => {
|
|
8
|
+
const adapter = getAdapter();
|
|
9
|
+
const hasher = adapter.createHasher('sha256');
|
|
10
|
+
hasher.update('hello');
|
|
11
|
+
const hex = hasher.digest('hex');
|
|
12
|
+
// Known sha256('hello')
|
|
13
|
+
expect(hex).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('sha256 base64 digest', () => {
|
|
17
|
+
const adapter = getAdapter();
|
|
18
|
+
const hasher = adapter.createHasher('sha256');
|
|
19
|
+
hasher.update('hello');
|
|
20
|
+
const b64 = hasher.digest('base64');
|
|
21
|
+
expect(typeof b64).toBe('string');
|
|
22
|
+
expect(b64.length).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('sha256 arrayBuffer digest', () => {
|
|
26
|
+
const adapter = getAdapter();
|
|
27
|
+
const hasher = adapter.createHasher('sha256');
|
|
28
|
+
hasher.update('hello');
|
|
29
|
+
const buf = hasher.digest();
|
|
30
|
+
// Both Bun and Node return Buffer (extends Uint8Array), not a plain ArrayBuffer
|
|
31
|
+
expect(buf instanceof Uint8Array).toBe(true);
|
|
32
|
+
expect(buf.byteLength).toBe(32);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('chaining update calls', () => {
|
|
36
|
+
const adapter = getAdapter();
|
|
37
|
+
// sha256('helloworld') should equal updating with 'hello' then 'world' only if same bytes
|
|
38
|
+
// We simply test that the result is consistent
|
|
39
|
+
const h1 = adapter.createHasher('sha256');
|
|
40
|
+
h1.update('hello').update('world');
|
|
41
|
+
const hex1 = h1.digest('hex');
|
|
42
|
+
|
|
43
|
+
const h2 = adapter.createHasher('sha256');
|
|
44
|
+
h2.update('helloworld');
|
|
45
|
+
const hex2 = h2.digest('hex');
|
|
46
|
+
|
|
47
|
+
expect(hex1).toBe(hex2);
|
|
48
|
+
});
|
|
49
|
+
}
|