@dangao/bun-server 2.3.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/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-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 +40264 -3542
- package/dist/index.node.mjs +17689 -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/design/query-interceptor-design.md +381 -0
- 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 -6
- 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-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/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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { IFsAdapter, IFileRef } from '../types';
|
|
2
|
+
|
|
3
|
+
class BunFileRef implements IFileRef {
|
|
4
|
+
private readonly bunFile: ReturnType<typeof Bun.file>;
|
|
5
|
+
|
|
6
|
+
public constructor(bunFile: ReturnType<typeof Bun.file>) {
|
|
7
|
+
this.bunFile = bunFile;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public get type(): string {
|
|
11
|
+
return this.bunFile.type;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public get size(): number {
|
|
15
|
+
return this.bunFile.size;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public text(): Promise<string> {
|
|
19
|
+
return this.bunFile.text();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public bytes(): Promise<Uint8Array> {
|
|
23
|
+
return this.bunFile.bytes();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public arrayBuffer(): Promise<ArrayBuffer> {
|
|
27
|
+
return this.bunFile.arrayBuffer();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public exists(): Promise<boolean> {
|
|
31
|
+
return this.bunFile.exists();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public stream(): ReadableStream<Uint8Array> {
|
|
35
|
+
return this.bunFile.stream() as ReadableStream<Uint8Array>;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const bunFsAdapter: IFsAdapter = {
|
|
40
|
+
file(path: string): IFileRef {
|
|
41
|
+
return new BunFileRef(Bun.file(path));
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async write(path: string, data: string | Uint8Array | ArrayBuffer): Promise<void> {
|
|
45
|
+
await Bun.write(path, data as any);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
glob(pattern: string, cwd?: string): string[] {
|
|
49
|
+
const g = new Bun.Glob(pattern);
|
|
50
|
+
return Array.from(g.scanSync(cwd ?? '.'));
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { IHttpDriver, IServerHandle, HttpServeOptions, IWebSocket, WebSocketHandlers } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bun ServerWebSocket 的 IWebSocket 包装
|
|
5
|
+
*/
|
|
6
|
+
export class BunWebSocket<T> implements IWebSocket<T> {
|
|
7
|
+
public constructor(private readonly ws: import('bun').ServerWebSocket<T>) {}
|
|
8
|
+
|
|
9
|
+
public get data(): T {
|
|
10
|
+
return this.ws.data;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public get readyState(): number {
|
|
14
|
+
return this.ws.readyState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public send(data: string | Buffer | Uint8Array): void {
|
|
18
|
+
this.ws.send(data as string);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public close(code?: number, reason?: string): void {
|
|
22
|
+
this.ws.close(code, reason);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public getNative(): import('bun').ServerWebSocket<T> {
|
|
26
|
+
return this.ws;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class BunServerHandle implements IServerHandle {
|
|
31
|
+
public constructor(private readonly server: import('bun').Server<unknown>) {}
|
|
32
|
+
|
|
33
|
+
public get port(): number {
|
|
34
|
+
return this.server.port ?? 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public get hostname(): string | undefined {
|
|
38
|
+
return this.server.hostname ?? undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public stop(): void {
|
|
42
|
+
this.server.stop();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public upgrade(request: Request, options?: { data?: unknown }): boolean {
|
|
46
|
+
return this.server.upgrade(request, options as any);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public timeout(request: Request, seconds: number): void {
|
|
50
|
+
this.server.timeout(request, seconds);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public getNative(): unknown {
|
|
54
|
+
return this.server;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildWebSocketHandlers<T>(
|
|
59
|
+
handlers?: WebSocketHandlers<T>,
|
|
60
|
+
): import('bun').WebSocketHandler<T> | undefined {
|
|
61
|
+
if (!handlers) return undefined;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
open: handlers.open
|
|
65
|
+
? (ws) => handlers.open!(new BunWebSocket(ws) as IWebSocket<T>)
|
|
66
|
+
: undefined,
|
|
67
|
+
message: handlers.message
|
|
68
|
+
? (ws, msg) => handlers.message!(new BunWebSocket(ws) as IWebSocket<T>, msg as string | Buffer)
|
|
69
|
+
: undefined,
|
|
70
|
+
close: handlers.close
|
|
71
|
+
? (ws, code, reason) => handlers.close!(new BunWebSocket(ws) as IWebSocket<T>, code, reason)
|
|
72
|
+
: undefined,
|
|
73
|
+
} as import('bun').WebSocketHandler<T>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const bunHttpAdapter: IHttpDriver = {
|
|
77
|
+
async serve<T>(options: HttpServeOptions<T>): Promise<IServerHandle> {
|
|
78
|
+
const wsHandlers = buildWebSocketHandlers(options.websocket);
|
|
79
|
+
|
|
80
|
+
const fetchHandler = (request: Request, bunServer: import('bun').Server<unknown>): Response | Promise<Response | undefined> | undefined => {
|
|
81
|
+
const handle = new BunServerHandle(bunServer);
|
|
82
|
+
return options.fetch(request, handle);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let server: import('bun').Server<unknown>;
|
|
86
|
+
|
|
87
|
+
if (options.unix) {
|
|
88
|
+
server = Bun.serve({
|
|
89
|
+
unix: options.unix,
|
|
90
|
+
fetch: fetchHandler,
|
|
91
|
+
websocket: wsHandlers,
|
|
92
|
+
} as any);
|
|
93
|
+
} else {
|
|
94
|
+
server = Bun.serve({
|
|
95
|
+
port: options.port ?? 3000,
|
|
96
|
+
hostname: options.hostname,
|
|
97
|
+
reusePort: options.reusePort,
|
|
98
|
+
idleTimeout: options.idleTimeout,
|
|
99
|
+
fetch: fetchHandler,
|
|
100
|
+
websocket: wsHandlers,
|
|
101
|
+
} as any);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new BunServerHandle(server);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IPlatform } from '../types';
|
|
2
|
+
import { bunFsAdapter } from './fs';
|
|
3
|
+
import { bunCryptoAdapter } from './crypto';
|
|
4
|
+
import { bunParserAdapter } from './parser';
|
|
5
|
+
import { bunProcessAdapter } from './process';
|
|
6
|
+
import { bunHttpAdapter } from './http';
|
|
7
|
+
|
|
8
|
+
export function createBunPlatform(): IPlatform {
|
|
9
|
+
return {
|
|
10
|
+
engine: 'bun',
|
|
11
|
+
fs: bunFsAdapter,
|
|
12
|
+
crypto: bunCryptoAdapter,
|
|
13
|
+
parser: bunParserAdapter,
|
|
14
|
+
process: bunProcessAdapter,
|
|
15
|
+
http: bunHttpAdapter,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IParserAdapter } from '../types';
|
|
2
|
+
|
|
3
|
+
export const bunParserAdapter: IParserAdapter = {
|
|
4
|
+
parseJSONC(content: string): unknown {
|
|
5
|
+
return Bun.JSONC.parse(content);
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
parseJSON5(content: string): unknown {
|
|
9
|
+
return Bun.JSON5.parse(content);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
parseJSONL(content: string): unknown[] {
|
|
13
|
+
return Bun.JSONL.parse(content) as unknown[];
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
renderMarkdown(md: string): string {
|
|
17
|
+
return Bun.markdown.html(md, { headings: true });
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IProcessAdapter, IChildProcess, SpawnOptions } from '../types';
|
|
2
|
+
|
|
3
|
+
class BunChildProcess implements IChildProcess {
|
|
4
|
+
private readonly child: ReturnType<typeof Bun.spawn>;
|
|
5
|
+
|
|
6
|
+
public constructor(child: ReturnType<typeof Bun.spawn>) {
|
|
7
|
+
this.child = child;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public get pid(): number {
|
|
11
|
+
return this.child.pid;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public get exited(): Promise<number | null> {
|
|
15
|
+
return this.child.exited;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public kill(signal?: string | number): void {
|
|
19
|
+
this.child.kill(signal as Parameters<ReturnType<typeof Bun.spawn>['kill']>[0]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const bunProcessAdapter: IProcessAdapter = {
|
|
24
|
+
spawn(options: SpawnOptions): IChildProcess {
|
|
25
|
+
const child = Bun.spawn({
|
|
26
|
+
cmd: options.cmd,
|
|
27
|
+
env: options.env as Record<string, string>,
|
|
28
|
+
stdout: options.stdout ?? 'inherit',
|
|
29
|
+
stderr: options.stderr ?? 'inherit',
|
|
30
|
+
});
|
|
31
|
+
return new BunChildProcess(child);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async sleep(ms: number): Promise<void> {
|
|
35
|
+
await Bun.sleep(ms);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PlatformEngine } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 解析运行时平台引擎
|
|
5
|
+
* 优先级:bootstrap config > CLI arg (--platform=) > env var (BUN_SERVER_PLATFORM) > auto-detect
|
|
6
|
+
*
|
|
7
|
+
* @param bootstrapConfig - 应用启动时显式指定的平台(最高优先级)
|
|
8
|
+
*/
|
|
9
|
+
export function resolvePlatform(bootstrapConfig?: PlatformEngine): PlatformEngine {
|
|
10
|
+
if (bootstrapConfig) {
|
|
11
|
+
return bootstrapConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// CLI arg: --platform=node or --platform=bun
|
|
15
|
+
for (const arg of process.argv) {
|
|
16
|
+
if (arg.startsWith('--platform=')) {
|
|
17
|
+
const value = arg.slice('--platform='.length).trim();
|
|
18
|
+
if (value === 'node' || value === 'bun') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Env var: BUN_SERVER_PLATFORM=node or BUN_SERVER_PLATFORM=bun
|
|
25
|
+
const envValue = process.env['BUN_SERVER_PLATFORM'];
|
|
26
|
+
if (envValue === 'node' || envValue === 'bun') {
|
|
27
|
+
return envValue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auto-detect: check if running inside Bun
|
|
31
|
+
if (typeof Bun !== 'undefined') {
|
|
32
|
+
return 'bun';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 'node';
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
PlatformEngine,
|
|
3
|
+
IPlatform,
|
|
4
|
+
IFsAdapter,
|
|
5
|
+
IFileRef,
|
|
6
|
+
ICryptoAdapter,
|
|
7
|
+
IHasher,
|
|
8
|
+
IParserAdapter,
|
|
9
|
+
IProcessAdapter,
|
|
10
|
+
IChildProcess,
|
|
11
|
+
SpawnOptions,
|
|
12
|
+
IHttpDriver,
|
|
13
|
+
IServerHandle,
|
|
14
|
+
IWebSocket,
|
|
15
|
+
WebSocketHandlers,
|
|
16
|
+
HttpServeOptions,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
export { resolvePlatform } from './detector';
|
|
20
|
+
export { initRuntime, getRuntime, _resetRuntime } from './runtime';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHmac, createHash } from 'node:crypto';
|
|
2
|
+
import type { ICryptoAdapter, IHasher } from '../types';
|
|
3
|
+
|
|
4
|
+
class NodeHasher implements IHasher {
|
|
5
|
+
private data: Buffer = Buffer.alloc(0);
|
|
6
|
+
private readonly algorithm: string;
|
|
7
|
+
|
|
8
|
+
public constructor(algorithm: string) {
|
|
9
|
+
this.algorithm = algorithm;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public update(data: string | Uint8Array | ArrayBuffer): IHasher {
|
|
13
|
+
if (data instanceof ArrayBuffer) {
|
|
14
|
+
this.data = Buffer.concat([this.data, Buffer.from(data)]);
|
|
15
|
+
} else if (typeof data === 'string') {
|
|
16
|
+
this.data = Buffer.concat([this.data, Buffer.from(data, 'utf-8')]);
|
|
17
|
+
} else {
|
|
18
|
+
this.data = Buffer.concat([this.data, Buffer.from(data)]);
|
|
19
|
+
}
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public digest(): Uint8Array;
|
|
24
|
+
public digest(encoding: 'hex' | 'base64' | 'base64url'): string;
|
|
25
|
+
public digest(encoding?: 'hex' | 'base64' | 'base64url'): Uint8Array | string {
|
|
26
|
+
const hash = createHash(this.algorithm);
|
|
27
|
+
hash.update(this.data);
|
|
28
|
+
if (encoding === 'hex') return hash.digest('hex');
|
|
29
|
+
if (encoding === 'base64') return hash.digest('base64');
|
|
30
|
+
if (encoding === 'base64url') return hash.digest('base64url');
|
|
31
|
+
// node:crypto hash.digest() returns Buffer which extends Uint8Array
|
|
32
|
+
return hash.digest();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const nodeCryptoAdapter: ICryptoAdapter = {
|
|
37
|
+
createHasher(algorithm: string): IHasher {
|
|
38
|
+
return new NodeHasher(algorithm);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFile, writeFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { glob } from 'node:fs/promises';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { lookup as mimeLookup } from 'mime-types';
|
|
6
|
+
import { extname } from 'node:path';
|
|
7
|
+
import type { IFsAdapter, IFileRef } from '../types';
|
|
8
|
+
|
|
9
|
+
class NodeFileRef implements IFileRef {
|
|
10
|
+
private readonly path: string;
|
|
11
|
+
private _type: string | null = null;
|
|
12
|
+
private _size: number | null = null;
|
|
13
|
+
|
|
14
|
+
public constructor(path: string) {
|
|
15
|
+
this.path = path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public get type(): string {
|
|
19
|
+
if (this._type === null) {
|
|
20
|
+
const ext = extname(this.path).toLowerCase();
|
|
21
|
+
this._type = (mimeLookup(ext) || 'application/octet-stream') as string;
|
|
22
|
+
}
|
|
23
|
+
return this._type;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public get size(): number {
|
|
27
|
+
return this._size ?? 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async text(): Promise<string> {
|
|
31
|
+
const buf = await readFile(this.path);
|
|
32
|
+
const stats = await stat(this.path).catch(() => null);
|
|
33
|
+
if (stats) this._size = stats.size;
|
|
34
|
+
return buf.toString('utf-8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public async bytes(): Promise<Uint8Array> {
|
|
38
|
+
const buf = await readFile(this.path);
|
|
39
|
+
const stats = await stat(this.path).catch(() => null);
|
|
40
|
+
if (stats) this._size = stats.size;
|
|
41
|
+
return new Uint8Array(buf);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async arrayBuffer(): Promise<ArrayBuffer> {
|
|
45
|
+
const bytes = await this.bytes();
|
|
46
|
+
return bytes.buffer as ArrayBuffer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async exists(): Promise<boolean> {
|
|
50
|
+
return stat(this.path)
|
|
51
|
+
.then((s) => { this._size = s.size; return true; })
|
|
52
|
+
.catch(() => false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public stream(): ReadableStream<Uint8Array> {
|
|
56
|
+
const nodeStream = createReadStream(this.path);
|
|
57
|
+
// Convert Node.js Readable to Web ReadableStream
|
|
58
|
+
return Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const nodeFsAdapter: IFsAdapter = {
|
|
63
|
+
file(path: string): IFileRef {
|
|
64
|
+
return new NodeFileRef(path);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async write(path: string, data: string | Uint8Array | ArrayBuffer): Promise<void> {
|
|
68
|
+
if (data instanceof ArrayBuffer) {
|
|
69
|
+
await writeFile(path, Buffer.from(data));
|
|
70
|
+
} else {
|
|
71
|
+
await writeFile(path, data as string | Uint8Array);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
glob(pattern: string, cwd?: string): string[] {
|
|
76
|
+
// Use synchronous glob via fast-glob-compatible approach
|
|
77
|
+
// node:fs/promises glob is async; we use sync approach via readdirSync + pattern matching
|
|
78
|
+
const results: string[] = [];
|
|
79
|
+
const base = cwd ?? '.';
|
|
80
|
+
try {
|
|
81
|
+
const { globSync } = require('node:fs');
|
|
82
|
+
if (globSync) {
|
|
83
|
+
// Node 22.0+ has globSync
|
|
84
|
+
const matches = globSync(pattern, { cwd: base });
|
|
85
|
+
return Array.from(matches as string[]);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// fallback
|
|
89
|
+
}
|
|
90
|
+
// Manual fallback: read dir and match with minimatch-style
|
|
91
|
+
try {
|
|
92
|
+
const entries = require('node:fs').readdirSync(base, { withFileTypes: true, recursive: true }) as import('node:fs').Dirent[];
|
|
93
|
+
const regex = patternToRegex(pattern);
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.isFile()) {
|
|
96
|
+
const name = (entry as any).name as string;
|
|
97
|
+
if (regex.test(name)) {
|
|
98
|
+
results.push(name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function patternToRegex(pattern: string): RegExp {
|
|
110
|
+
const escaped = pattern
|
|
111
|
+
.replace(/\./g, '\\.')
|
|
112
|
+
.replace(/\*/g, '[^/]*')
|
|
113
|
+
.replace(/\?/g, '[^/]');
|
|
114
|
+
return new RegExp(`^${escaped}$`);
|
|
115
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import type { IHttpDriver, IServerHandle, HttpServeOptions, IWebSocket, WebSocketHandlers } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ws.WebSocket 的 IWebSocket 包装
|
|
7
|
+
*/
|
|
8
|
+
class NodeWebSocket<T> implements IWebSocket<T> {
|
|
9
|
+
private readonly ws: import('ws').WebSocket;
|
|
10
|
+
private _data: T;
|
|
11
|
+
|
|
12
|
+
public constructor(ws: import('ws').WebSocket, data: T) {
|
|
13
|
+
this.ws = ws;
|
|
14
|
+
this._data = data;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public get data(): T {
|
|
18
|
+
return this._data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public get readyState(): number {
|
|
22
|
+
return this.ws.readyState;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public send(data: string | Buffer | Uint8Array): void {
|
|
26
|
+
this.ws.send(data);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public close(code?: number, reason?: string): void {
|
|
30
|
+
this.ws.close(code, reason);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class NodeServerHandle implements IServerHandle {
|
|
35
|
+
private readonly httpServer: import('node:http').Server;
|
|
36
|
+
private _port: number;
|
|
37
|
+
private _hostname?: string;
|
|
38
|
+
|
|
39
|
+
public constructor(
|
|
40
|
+
httpServer: import('node:http').Server,
|
|
41
|
+
port: number,
|
|
42
|
+
hostname?: string,
|
|
43
|
+
) {
|
|
44
|
+
this.httpServer = httpServer;
|
|
45
|
+
this._port = port;
|
|
46
|
+
this._hostname = hostname;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public get port(): number {
|
|
50
|
+
return this._port;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public get hostname(): string | undefined {
|
|
54
|
+
return this._hostname;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public stop(): void {
|
|
58
|
+
this.httpServer.close();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getNative(): unknown {
|
|
62
|
+
return this.httpServer;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function nodeRequestToWebRequest(req: IncomingMessage): Promise<Request> {
|
|
67
|
+
const host = req.headers.host ?? 'localhost';
|
|
68
|
+
const url = `http://${host}${req.url ?? '/'}`;
|
|
69
|
+
const method = req.method ?? 'GET';
|
|
70
|
+
|
|
71
|
+
const chunks: Buffer[] = [];
|
|
72
|
+
for await (const chunk of req) {
|
|
73
|
+
chunks.push(chunk as Buffer);
|
|
74
|
+
}
|
|
75
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
|
|
76
|
+
|
|
77
|
+
const headers = new Headers();
|
|
78
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
79
|
+
if (value) {
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
for (const v of value) headers.append(key, v);
|
|
82
|
+
} else {
|
|
83
|
+
headers.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return new Request(url, {
|
|
89
|
+
method,
|
|
90
|
+
headers,
|
|
91
|
+
body: body?.length ? body : undefined,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sendWebResponse(res: ServerResponse, webResponse: Response): void {
|
|
96
|
+
res.statusCode = webResponse.status;
|
|
97
|
+
webResponse.headers.forEach((value, key) => {
|
|
98
|
+
res.setHeader(key, value);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!webResponse.body) {
|
|
102
|
+
res.end();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const reader = webResponse.body.getReader();
|
|
107
|
+
const pump = () => {
|
|
108
|
+
reader.read().then(({ done, value }) => {
|
|
109
|
+
if (done) {
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
res.write(value, pump);
|
|
114
|
+
}).catch(() => {
|
|
115
|
+
res.destroy();
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
pump();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const nodeHttpAdapter: IHttpDriver = {
|
|
122
|
+
async serve<T>(options: HttpServeOptions<T>): Promise<IServerHandle> {
|
|
123
|
+
// Initialized after listen; the fetch callback only fires after listen completes
|
|
124
|
+
let serverHandle: NodeServerHandle = null!;
|
|
125
|
+
|
|
126
|
+
const httpServer = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
127
|
+
try {
|
|
128
|
+
const webRequest = await nodeRequestToWebRequest(req);
|
|
129
|
+
const response = await options.fetch(webRequest, serverHandle);
|
|
130
|
+
if (response) {
|
|
131
|
+
sendWebResponse(res, response);
|
|
132
|
+
} else {
|
|
133
|
+
res.statusCode = 404;
|
|
134
|
+
res.end('Not Found');
|
|
135
|
+
}
|
|
136
|
+
} catch (_err) {
|
|
137
|
+
res.statusCode = 500;
|
|
138
|
+
res.end('Internal Server Error');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Mount WebSocket server if handlers provided
|
|
143
|
+
if (options.websocket) {
|
|
144
|
+
setupWebSocket(httpServer, options.websocket);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const requestedPort = options.port ?? 3000;
|
|
148
|
+
const hostname = options.hostname;
|
|
149
|
+
|
|
150
|
+
// Wait for the server to actually start listening so port 0 gets resolved
|
|
151
|
+
await new Promise<void>((resolve, reject) => {
|
|
152
|
+
httpServer.once('error', reject);
|
|
153
|
+
httpServer.once('listening', resolve);
|
|
154
|
+
if (hostname) {
|
|
155
|
+
httpServer.listen(requestedPort, hostname);
|
|
156
|
+
} else {
|
|
157
|
+
httpServer.listen(requestedPort);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const addr = httpServer.address() as import('node:net').AddressInfo;
|
|
162
|
+
serverHandle = new NodeServerHandle(httpServer, addr.port, hostname);
|
|
163
|
+
return serverHandle;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function setupWebSocket<T>(
|
|
168
|
+
httpServer: import('node:http').Server,
|
|
169
|
+
handlers: WebSocketHandlers<T>,
|
|
170
|
+
): void {
|
|
171
|
+
const { WebSocketServer } = require('ws') as typeof import('ws');
|
|
172
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
173
|
+
|
|
174
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
175
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
176
|
+
const nodeWs = new NodeWebSocket<T>(ws, undefined as unknown as T);
|
|
177
|
+
|
|
178
|
+
if (handlers.open) {
|
|
179
|
+
handlers.open(nodeWs);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ws.on('message', (data) => {
|
|
183
|
+
if (handlers.message) {
|
|
184
|
+
const msg = data instanceof Buffer ? data : Buffer.from(data as ArrayBuffer);
|
|
185
|
+
handlers.message(nodeWs, msg);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
ws.on('close', (code, reason) => {
|
|
190
|
+
if (handlers.close) {
|
|
191
|
+
handlers.close(nodeWs, code, reason.toString());
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IPlatform } from '../types';
|
|
2
|
+
import { nodeFsAdapter } from './fs';
|
|
3
|
+
import { nodeCryptoAdapter } from './crypto';
|
|
4
|
+
import { nodeParserAdapter } from './parser';
|
|
5
|
+
import { nodeProcessAdapter } from './process';
|
|
6
|
+
import { nodeHttpAdapter } from './http';
|
|
7
|
+
|
|
8
|
+
export function createNodePlatform(): IPlatform {
|
|
9
|
+
return {
|
|
10
|
+
engine: 'node',
|
|
11
|
+
fs: nodeFsAdapter,
|
|
12
|
+
crypto: nodeCryptoAdapter,
|
|
13
|
+
parser: nodeParserAdapter,
|
|
14
|
+
process: nodeProcessAdapter,
|
|
15
|
+
http: nodeHttpAdapter,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IParserAdapter } from '../types';
|
|
2
|
+
|
|
3
|
+
export const nodeParserAdapter: IParserAdapter = {
|
|
4
|
+
parseJSONC(content: string): unknown {
|
|
5
|
+
const { parse } = require('jsonc-parser') as typeof import('jsonc-parser');
|
|
6
|
+
return parse(content);
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
parseJSON5(content: string): unknown {
|
|
10
|
+
const JSON5 = require('json5') as typeof import('json5');
|
|
11
|
+
return JSON5.parse(content);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
parseJSONL(content: string): unknown[] {
|
|
15
|
+
return content
|
|
16
|
+
.split('\n')
|
|
17
|
+
.map((line) => line.trim())
|
|
18
|
+
.filter((line) => line.length > 0)
|
|
19
|
+
.map((line) => JSON.parse(line));
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
renderMarkdown(md: string): string {
|
|
23
|
+
const { marked } = require('marked') as typeof import('marked');
|
|
24
|
+
const result = marked(md);
|
|
25
|
+
if (typeof result === 'string') {
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
// marked can return a Promise in some configurations — resolve synchronously
|
|
29
|
+
// by using marked.parseInline which is sync, or fall back to a sync config
|
|
30
|
+
const { marked: markedSync } = require('marked') as typeof import('marked');
|
|
31
|
+
markedSync.setOptions({ async: false });
|
|
32
|
+
return markedSync(md) as string;
|
|
33
|
+
},
|
|
34
|
+
};
|