@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.
Files changed (166) hide show
  1. package/README.md +81 -3
  2. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
  3. package/dist/ai/providers/google-provider.d.ts.map +1 -1
  4. package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
  5. package/dist/ai/providers/openai-provider.d.ts.map +1 -1
  6. package/dist/ai/service.d.ts.map +1 -1
  7. package/dist/ai/types.d.ts +5 -0
  8. package/dist/ai/types.d.ts.map +1 -1
  9. package/dist/auth/jwt.d.ts.map +1 -1
  10. package/dist/config/service.d.ts +0 -1
  11. package/dist/config/service.d.ts.map +1 -1
  12. package/dist/core/application.d.ts +30 -0
  13. package/dist/core/application.d.ts.map +1 -1
  14. package/dist/core/cluster.d.ts.map +1 -1
  15. package/dist/core/context.d.ts +5 -0
  16. package/dist/core/context.d.ts.map +1 -1
  17. package/dist/core/server.d.ts +29 -9
  18. package/dist/core/server.d.ts.map +1 -1
  19. package/dist/dashboard/controller.d.ts.map +1 -1
  20. package/dist/database/connection-pool.d.ts +3 -3
  21. package/dist/database/connection-pool.d.ts.map +1 -1
  22. package/dist/database/sql-manager.d.ts +8 -4
  23. package/dist/database/sql-manager.d.ts.map +1 -1
  24. package/dist/database/sqlite-adapter.d.ts +7 -3
  25. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  26. package/dist/debug/recorder.d.ts +0 -1
  27. package/dist/debug/recorder.d.ts.map +1 -1
  28. package/dist/files/static-middleware.d.ts.map +1 -1
  29. package/dist/files/storage.d.ts.map +1 -1
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +40335 -3523
  33. package/dist/index.node.mjs +17689 -0
  34. package/dist/mcp/server.d.ts +5 -2
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/middleware/builtin/static-file.d.ts +4 -2
  37. package/dist/middleware/builtin/static-file.d.ts.map +1 -1
  38. package/dist/platform/bun/crypto.d.ts +3 -0
  39. package/dist/platform/bun/crypto.d.ts.map +1 -0
  40. package/dist/platform/bun/fs.d.ts +3 -0
  41. package/dist/platform/bun/fs.d.ts.map +1 -0
  42. package/dist/platform/bun/http.d.ts +15 -0
  43. package/dist/platform/bun/http.d.ts.map +1 -0
  44. package/dist/platform/bun/index.d.ts +3 -0
  45. package/dist/platform/bun/index.d.ts.map +1 -0
  46. package/dist/platform/bun/parser.d.ts +3 -0
  47. package/dist/platform/bun/parser.d.ts.map +1 -0
  48. package/dist/platform/bun/process.d.ts +3 -0
  49. package/dist/platform/bun/process.d.ts.map +1 -0
  50. package/dist/platform/detector.d.ts +9 -0
  51. package/dist/platform/detector.d.ts.map +1 -0
  52. package/dist/platform/index.d.ts +4 -0
  53. package/dist/platform/index.d.ts.map +1 -0
  54. package/dist/platform/node/crypto.d.ts +3 -0
  55. package/dist/platform/node/crypto.d.ts.map +1 -0
  56. package/dist/platform/node/fs.d.ts +3 -0
  57. package/dist/platform/node/fs.d.ts.map +1 -0
  58. package/dist/platform/node/http.d.ts +3 -0
  59. package/dist/platform/node/http.d.ts.map +1 -0
  60. package/dist/platform/node/index.d.ts +3 -0
  61. package/dist/platform/node/index.d.ts.map +1 -0
  62. package/dist/platform/node/parser.d.ts +3 -0
  63. package/dist/platform/node/parser.d.ts.map +1 -0
  64. package/dist/platform/node/process.d.ts +3 -0
  65. package/dist/platform/node/process.d.ts.map +1 -0
  66. package/dist/platform/runtime.d.ts +14 -0
  67. package/dist/platform/runtime.d.ts.map +1 -0
  68. package/dist/platform/types.d.ts +139 -0
  69. package/dist/platform/types.d.ts.map +1 -0
  70. package/dist/prompt/stores/file-store.d.ts.map +1 -1
  71. package/dist/rag/service.d.ts.map +1 -1
  72. package/dist/request/response.d.ts +3 -1
  73. package/dist/request/response.d.ts.map +1 -1
  74. package/dist/security/guards/execution-context.d.ts +2 -2
  75. package/dist/security/guards/execution-context.d.ts.map +1 -1
  76. package/dist/security/guards/types.d.ts +2 -2
  77. package/dist/security/guards/types.d.ts.map +1 -1
  78. package/dist/swagger/generator.d.ts.map +1 -1
  79. package/dist/websocket/registry.d.ts +4 -4
  80. package/dist/websocket/registry.d.ts.map +1 -1
  81. package/docs/deployment.md +31 -7
  82. package/docs/design/query-interceptor-design.md +381 -0
  83. package/docs/idle-timeout.md +101 -8
  84. package/docs/migration.md +43 -0
  85. package/docs/platform.md +299 -0
  86. package/docs/testing.md +60 -0
  87. package/docs/zh/deployment.md +30 -7
  88. package/docs/zh/idle-timeout.md +99 -6
  89. package/docs/zh/migration.md +42 -0
  90. package/docs/zh/platform.md +299 -0
  91. package/docs/zh/testing.md +60 -0
  92. package/package.json +24 -6
  93. package/src/ai/providers/anthropic-provider.ts +5 -2
  94. package/src/ai/providers/google-provider.ts +3 -0
  95. package/src/ai/providers/ollama-provider.ts +3 -0
  96. package/src/ai/providers/openai-provider.ts +5 -2
  97. package/src/ai/service.ts +17 -5
  98. package/src/ai/types.ts +5 -0
  99. package/src/auth/jwt.ts +4 -3
  100. package/src/config/service.ts +7 -6
  101. package/src/core/application.ts +38 -1
  102. package/src/core/cluster.ts +16 -14
  103. package/src/core/context.ts +7 -0
  104. package/src/core/server.ts +162 -46
  105. package/src/dashboard/controller.ts +3 -2
  106. package/src/database/connection-pool.ts +32 -20
  107. package/src/database/database-module.ts +1 -1
  108. package/src/database/db-proxy.ts +2 -2
  109. package/src/database/orm/transaction-manager.ts +1 -1
  110. package/src/database/sql-manager.ts +48 -13
  111. package/src/database/sqlite-adapter.ts +45 -12
  112. package/src/debug/recorder.ts +4 -3
  113. package/src/files/static-middleware.ts +3 -2
  114. package/src/files/storage.ts +2 -1
  115. package/src/index.ts +13 -0
  116. package/src/mcp/server.ts +6 -15
  117. package/src/middleware/builtin/static-file.ts +8 -5
  118. package/src/platform/bun/crypto.ts +30 -0
  119. package/src/platform/bun/fs.ts +52 -0
  120. package/src/platform/bun/http.ts +106 -0
  121. package/src/platform/bun/index.ts +17 -0
  122. package/src/platform/bun/parser.ts +19 -0
  123. package/src/platform/bun/process.ts +37 -0
  124. package/src/platform/detector.ts +36 -0
  125. package/src/platform/index.ts +20 -0
  126. package/src/platform/node/crypto.ts +40 -0
  127. package/src/platform/node/fs.ts +115 -0
  128. package/src/platform/node/http.ts +196 -0
  129. package/src/platform/node/index.ts +17 -0
  130. package/src/platform/node/parser.ts +34 -0
  131. package/src/platform/node/process.ts +51 -0
  132. package/src/platform/runtime.ts +50 -0
  133. package/src/platform/types.ts +150 -0
  134. package/src/prompt/stores/file-store.ts +6 -5
  135. package/src/rag/service.ts +2 -1
  136. package/src/request/response.ts +7 -4
  137. package/src/security/guards/execution-context.ts +4 -4
  138. package/src/security/guards/types.ts +2 -2
  139. package/src/swagger/generator.ts +2 -1
  140. package/src/websocket/registry.ts +6 -7
  141. package/tests/controller/path-combination.test.ts +196 -2
  142. package/tests/files/static-middleware.test.ts +5 -2
  143. package/tests/middleware/static-file.test.ts +5 -2
  144. package/tests/platform/bun/crypto.test.ts +8 -0
  145. package/tests/platform/bun/database.test.ts +8 -0
  146. package/tests/platform/bun/fs.test.ts +8 -0
  147. package/tests/platform/bun/parser.test.ts +8 -0
  148. package/tests/platform/bun/process.test.ts +8 -0
  149. package/tests/platform/bun/websocket.test.ts +8 -0
  150. package/tests/platform/detector.test.ts +57 -0
  151. package/tests/platform/node/build-smoke.test.ts +92 -0
  152. package/tests/platform/node/crypto.test.ts +9 -0
  153. package/tests/platform/node/database.test.ts +9 -0
  154. package/tests/platform/node/fs.test.ts +9 -0
  155. package/tests/platform/node/parser.test.ts +9 -0
  156. package/tests/platform/node/process.test.ts +9 -0
  157. package/tests/platform/node/websocket.test.ts +9 -0
  158. package/tests/platform/shared/crypto.cases.ts +49 -0
  159. package/tests/platform/shared/database.cases.ts +43 -0
  160. package/tests/platform/shared/fs.cases.ts +82 -0
  161. package/tests/platform/shared/parser.cases.ts +55 -0
  162. package/tests/platform/shared/process.cases.ts +26 -0
  163. package/tests/platform/shared/suite.ts +33 -0
  164. package/tests/platform/shared/websocket.cases.ts +61 -0
  165. package/tests/request/response.test.ts +5 -2
  166. package/tests/router/router-extended.test.ts +53 -0
@@ -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
+ }
@@ -0,0 +1,43 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import type { TestSuite } from './suite';
4
+ import type { PlatformEngine } from '../../../src/platform/types';
5
+ import { SqliteManager } from '../../../src/database/sqlite-adapter';
6
+
7
+ export function runDatabaseCases(suite: TestSuite, engine: PlatformEngine): void {
8
+ const { test, expect } = suite;
9
+
10
+ // better-sqlite3 is not supported when running Node-platform tests under Bun runtime
11
+ const skipBetterSqlite = engine === 'node' && typeof (globalThis as any).Bun !== 'undefined';
12
+
13
+ test('SqliteAdapter: create table, insert and query', () => {
14
+ if (skipBetterSqlite) {
15
+ // better-sqlite3 is not supported in Bun — this test must run under Node.js
16
+ console.log('[skip] better-sqlite3 not available in Bun runtime; run with vitest for Node platform');
17
+ return;
18
+ }
19
+ const dbPath = join(tmpdir(), `platform-db-test-${engine}-${Date.now()}.db`);
20
+ const manager = new SqliteManager();
21
+ const adapter = manager.getOrCreate('test', { database: dbPath, wal: false });
22
+
23
+ adapter.query('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
24
+ adapter.query("INSERT INTO users (name) VALUES ('Alice')");
25
+ const rows = adapter.query<{ id: number; name: string }>('SELECT * FROM users');
26
+
27
+ expect(rows.length).toBeGreaterThanOrEqual(1);
28
+ expect(rows[0]!.name).toBe('Alice');
29
+
30
+ manager.destroy('test');
31
+ });
32
+
33
+ test('SqliteManager.getDefault() throws when not initialized', () => {
34
+ const manager = new SqliteManager();
35
+ let threw = false;
36
+ try {
37
+ manager.getDefault();
38
+ } catch {
39
+ threw = true;
40
+ }
41
+ expect(threw).toBe(true);
42
+ });
43
+ }
@@ -0,0 +1,82 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync, rmSync } from 'node:fs';
4
+ import type { TestSuite } from './suite';
5
+ import type { IFsAdapter } from '../../../src/platform/types';
6
+
7
+ export function runFsCases(suite: TestSuite, getAdapter: () => IFsAdapter): void {
8
+ const { test, expect, beforeEach } = suite;
9
+
10
+ let tmpDir: string;
11
+
12
+ beforeEach(() => {
13
+ tmpDir = join(tmpdir(), `platform-fs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
14
+ mkdirSync(tmpDir, { recursive: true });
15
+ });
16
+
17
+ test('write and read text', async () => {
18
+ const adapter = getAdapter();
19
+ const filePath = join(tmpDir, 'hello.txt');
20
+ await adapter.write(filePath, 'hello world');
21
+ const content = await adapter.file(filePath).text();
22
+ expect(content).toBe('hello world');
23
+ });
24
+
25
+ test('write and read bytes', async () => {
26
+ const adapter = getAdapter();
27
+ const filePath = join(tmpDir, 'bytes.bin');
28
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
29
+ await adapter.write(filePath, data);
30
+ const result = await adapter.file(filePath).bytes();
31
+ expect(result[0]).toBe(1);
32
+ expect(result[4]).toBe(5);
33
+ });
34
+
35
+ test('file.exists() returns true for existing file', async () => {
36
+ const adapter = getAdapter();
37
+ const filePath = join(tmpDir, 'exists.txt');
38
+ await adapter.write(filePath, 'content');
39
+ const exists = await adapter.file(filePath).exists();
40
+ expect(exists).toBe(true);
41
+ });
42
+
43
+ test('file.exists() returns false for missing file', async () => {
44
+ const adapter = getAdapter();
45
+ const filePath = join(tmpDir, 'does-not-exist.txt');
46
+ const exists = await adapter.file(filePath).exists();
47
+ expect(exists).toBe(false);
48
+ });
49
+
50
+ test('file.type returns MIME type', async () => {
51
+ const adapter = getAdapter();
52
+ const filePath = join(tmpDir, 'image.png');
53
+ await adapter.write(filePath, new Uint8Array([0x89, 0x50, 0x4e, 0x47]));
54
+ const mime = adapter.file(filePath).type;
55
+ expect(mime).toContain('png');
56
+ });
57
+
58
+ test('stream() produces readable stream with correct content', async () => {
59
+ const adapter = getAdapter();
60
+ const filePath = join(tmpDir, 'stream.txt');
61
+ await adapter.write(filePath, 'stream content');
62
+ const stream = adapter.file(filePath).stream();
63
+ const reader = stream.getReader();
64
+ const chunks: Uint8Array[] = [];
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+ chunks.push(value);
69
+ }
70
+ const combined = Buffer.concat(chunks).toString('utf-8');
71
+ expect(combined).toBe('stream content');
72
+ });
73
+
74
+ test('glob finds matching files', async () => {
75
+ const adapter = getAdapter();
76
+ await adapter.write(join(tmpDir, 'a.json'), '{}');
77
+ await adapter.write(join(tmpDir, 'b.json'), '{}');
78
+ await adapter.write(join(tmpDir, 'c.txt'), 'text');
79
+ const files = adapter.glob('*.json', tmpDir);
80
+ expect(files.length).toBeGreaterThanOrEqual(2);
81
+ });
82
+ }
@@ -0,0 +1,55 @@
1
+ import type { TestSuite } from './suite';
2
+ import type { IParserAdapter } from '../../../src/platform/types';
3
+
4
+ export function runParserCases(suite: TestSuite, getAdapter: () => IParserAdapter): void {
5
+ const { test, expect } = suite;
6
+
7
+ test('parseJSONC parses standard JSON', () => {
8
+ const adapter = getAdapter();
9
+ const result = adapter.parseJSONC('{"a": 1, "b": "hello"}') as Record<string, unknown>;
10
+ expect(result['a']).toBe(1);
11
+ expect(result['b']).toBe('hello');
12
+ });
13
+
14
+ test('parseJSONC ignores line comments', () => {
15
+ const adapter = getAdapter();
16
+ const result = adapter.parseJSONC('{\n// comment\n"a": 1}') as Record<string, unknown>;
17
+ expect(result['a']).toBe(1);
18
+ });
19
+
20
+ test('parseJSON5 parses JSON5 with trailing commas', () => {
21
+ const adapter = getAdapter();
22
+ const result = adapter.parseJSON5('{a: 1, b: "hello",}') as Record<string, unknown>;
23
+ expect(result['a']).toBe(1);
24
+ });
25
+
26
+ test('parseJSONL parses multiple JSON lines', () => {
27
+ const adapter = getAdapter();
28
+ const content = '{"id":1}\n{"id":2}\n{"id":3}';
29
+ const records = adapter.parseJSONL(content);
30
+ expect(records.length).toBe(3);
31
+ expect((records[0] as Record<string, unknown>)['id']).toBe(1);
32
+ expect((records[2] as Record<string, unknown>)['id']).toBe(3);
33
+ });
34
+
35
+ test('parseJSONL ignores empty lines', () => {
36
+ const adapter = getAdapter();
37
+ const content = '{"id":1}\n\n{"id":2}\n';
38
+ const records = adapter.parseJSONL(content);
39
+ expect(records.length).toBe(2);
40
+ });
41
+
42
+ test('renderMarkdown converts h1 heading', () => {
43
+ const adapter = getAdapter();
44
+ const html = adapter.renderMarkdown('# Hello World');
45
+ expect(html).toContain('<h1');
46
+ expect(html).toContain('Hello World');
47
+ });
48
+
49
+ test('renderMarkdown converts bold text', () => {
50
+ const adapter = getAdapter();
51
+ const html = adapter.renderMarkdown('**bold**');
52
+ expect(html).toContain('<strong>');
53
+ expect(html).toContain('bold');
54
+ });
55
+ }
@@ -0,0 +1,26 @@
1
+ import type { TestSuite } from './suite';
2
+ import type { IProcessAdapter } from '../../../src/platform/types';
3
+
4
+ export function runProcessCases(suite: TestSuite, getAdapter: () => IProcessAdapter): void {
5
+ const { test, expect } = suite;
6
+
7
+ test('sleep waits at least the specified time', async () => {
8
+ const adapter = getAdapter();
9
+ const start = Date.now();
10
+ await adapter.sleep(100);
11
+ const elapsed = Date.now() - start;
12
+ expect(elapsed).toBeGreaterThanOrEqual(90);
13
+ });
14
+
15
+ test('spawn runs a process and returns exit code 0', async () => {
16
+ const adapter = getAdapter();
17
+ const child = adapter.spawn({
18
+ cmd: ['node', '--version'],
19
+ stdout: 'pipe',
20
+ stderr: 'ignore',
21
+ });
22
+ expect(child.pid).toBeGreaterThan(0);
23
+ const exitCode = await child.exited;
24
+ expect(exitCode).toBe(0);
25
+ });
26
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 框架无关的测试套件接口
3
+ * 让 shared cases 文件在 bun:test 和 vitest 中都能运行,无需任何适配层
4
+ */
5
+ export interface TestSuite {
6
+ test: (name: string, fn: () => void | Promise<void>) => void;
7
+ expect: (actual: unknown) => {
8
+ toBe(e: unknown): void;
9
+ toEqual(e: unknown): void;
10
+ toBeTruthy(): void;
11
+ toBeFalsy(): void;
12
+ toContain(e: unknown): void;
13
+ toBeGreaterThan(e: number): void;
14
+ toBeGreaterThanOrEqual(e: number): void;
15
+ toBeLessThan(e: number): void;
16
+ toBeLessThanOrEqual(e: number): void;
17
+ toBeInstanceOf(e: unknown): void;
18
+ toHaveLength(e: number): void;
19
+ not: {
20
+ toBe(e: unknown): void;
21
+ toBeNull(): void;
22
+ toBeUndefined(): void;
23
+ toThrow(): void;
24
+ };
25
+ resolves: {
26
+ toBeTruthy(): Promise<void>;
27
+ };
28
+ rejects: {
29
+ toThrow(): Promise<void>;
30
+ };
31
+ };
32
+ beforeEach: (fn: () => void | Promise<void>) => void;
33
+ }
@@ -0,0 +1,61 @@
1
+ import type { TestSuite } from './suite';
2
+ import type { IPlatform } from '../../../src/platform/types';
3
+
4
+ export function runWebSocketCases(suite: TestSuite, getPlatform: () => IPlatform): void {
5
+ const { test, expect } = suite;
6
+
7
+ test('WebSocket server accepts connection and receives message', async () => {
8
+ const platform = getPlatform();
9
+ const messages: string[] = [];
10
+ let openCalled = false;
11
+
12
+ // Use a promise to track the server-side close event
13
+ let resolveServerClose!: () => void;
14
+ const serverClosePromise = new Promise<void>((r) => { resolveServerClose = r; });
15
+
16
+ const server = await platform.http.serve({
17
+ port: 0,
18
+ fetch: (req, handle) => {
19
+ const upgraded = handle.upgrade?.(req, { data: {} });
20
+ if (upgraded) return undefined as unknown as Response;
21
+ return new Response('not ws', { status: 400 });
22
+ },
23
+ websocket: {
24
+ open: (_ws) => { openCalled = true; },
25
+ message: (ws, msg) => {
26
+ messages.push(msg.toString());
27
+ ws.send('echo: ' + msg.toString());
28
+ },
29
+ close: (_ws) => { resolveServerClose(); },
30
+ },
31
+ });
32
+
33
+ const port = server.port;
34
+
35
+ // Connect using native WebSocket (available in both Bun and Node 22+)
36
+ await new Promise<void>((resolve, reject) => {
37
+ const ws = new WebSocket(`ws://localhost:${port}`);
38
+ ws.addEventListener('open', () => {
39
+ ws.send('hello');
40
+ });
41
+ ws.addEventListener('message', (e) => {
42
+ if (e.data === 'echo: hello') {
43
+ ws.close();
44
+ }
45
+ });
46
+ ws.addEventListener('close', () => {
47
+ resolve();
48
+ });
49
+ ws.addEventListener('error', reject);
50
+ setTimeout(reject, 5000);
51
+ });
52
+
53
+ // Wait for server-side close handler to fire before asserting
54
+ await serverClosePromise;
55
+
56
+ server.stop();
57
+
58
+ expect(openCalled).toBe(true);
59
+ expect(messages).toContain('hello');
60
+ });
61
+ }
@@ -1,8 +1,11 @@
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
  import { ResponseBuilder } from '../../src/request/response';
6
+ import { initRuntime } from '../../src/platform/runtime';
7
+
8
+ initRuntime('bun');
6
9
 
7
10
  describe('ResponseBuilder', () => {
8
11
  let tmpDir: string;
@@ -71,7 +74,7 @@ describe('ResponseBuilder', () => {
71
74
 
72
75
  test('should create file response from path', async () => {
73
76
  const filePath = join(tmpDir, 'hello.txt');
74
- await Bun.write(filePath, 'file-content');
77
+ await writeFile(filePath, 'file-content', 'utf-8');
75
78
 
76
79
  const response = ResponseBuilder.file(filePath, {
77
80
  fileName: 'download.txt',
@@ -94,6 +94,42 @@ describe('Router', () => {
94
94
  const route = router.findRoute('POST', '/users');
95
95
  expect(route).toBeUndefined();
96
96
  });
97
+
98
+ test('should distinguish GET POST PUT DELETE PATCH on the same dynamic route', () => {
99
+ router.get('/api/:id', async () => new Response('get'));
100
+ router.post('/api/:id', async () => new Response('post'));
101
+ router.put('/api/:id', async () => new Response('put'));
102
+ router.delete('/api/:id', async () => new Response('delete'));
103
+ router.patch('/api/:id', async () => new Response('patch'));
104
+
105
+ const getRoute = router.findRoute('GET', '/api/123');
106
+ const postRoute = router.findRoute('POST', '/api/123');
107
+ const putRoute = router.findRoute('PUT', '/api/123');
108
+ const deleteRoute = router.findRoute('DELETE', '/api/123');
109
+ const patchRoute = router.findRoute('PATCH', '/api/123');
110
+
111
+ expect(getRoute).toBeDefined();
112
+ expect(postRoute).toBeDefined();
113
+ expect(putRoute).toBeDefined();
114
+ expect(deleteRoute).toBeDefined();
115
+ expect(patchRoute).toBeDefined();
116
+
117
+ expect(getRoute?.method).toBe('GET');
118
+ expect(postRoute?.method).toBe('POST');
119
+ expect(putRoute?.method).toBe('PUT');
120
+ expect(deleteRoute?.method).toBe('DELETE');
121
+ expect(patchRoute?.method).toBe('PATCH');
122
+ });
123
+
124
+ test('should not cross-match methods on same dynamic route', () => {
125
+ router.get('/api/:id', async () => new Response('get'));
126
+ router.post('/api/:id', async () => new Response('post'));
127
+
128
+ expect(router.findRoute('GET', '/api/123')).toBeDefined();
129
+ expect(router.findRoute('POST', '/api/123')).toBeDefined();
130
+ expect(router.findRoute('PUT', '/api/123')).toBeUndefined();
131
+ expect(router.findRoute('DELETE', '/api/123')).toBeUndefined();
132
+ });
97
133
  });
98
134
 
99
135
  describe('findRouteWithMatch', () => {
@@ -178,6 +214,23 @@ describe('Router', () => {
178
214
  expect((context as any).routeHandler.controller).toBe(MyController);
179
215
  expect((context as any).routeHandler.method).toBe('testMethod');
180
216
  });
217
+
218
+ test('should route each HTTP method to its own handler on the same dynamic path /api/:id', async () => {
219
+ router.get('/api/:id', async () => new Response('get'));
220
+ router.post('/api/:id', async () => new Response('post'));
221
+ router.put('/api/:id', async () => new Response('put'));
222
+ router.delete('/api/:id', async () => new Response('delete'));
223
+ router.patch('/api/:id', async () => new Response('patch'));
224
+
225
+ const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
226
+ for (const method of methods) {
227
+ const request = new Request('http://localhost/api/123', { method });
228
+ const context = new Context(request, new Container());
229
+ const response = await router.handle(context);
230
+ expect(response).toBeDefined();
231
+ expect(await response?.text()).toBe(method.toLowerCase());
232
+ }
233
+ });
181
234
  });
182
235
 
183
236
  describe('preHandle', () => {