@kevisual/router 0.0.22 → 0.0.24
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/dist/router-browser.d.ts +10 -3
- package/dist/router-browser.js +17439 -530
- package/dist/router-sign.d.ts +1 -0
- package/dist/router-sign.js +15 -7
- package/dist/router.d.ts +10 -35
- package/dist/router.js +17446 -597
- package/package.json +22 -9
- package/readme.md +14 -0
- package/src/app-browser.ts +5 -0
- package/src/app.ts +7 -1
- package/src/auto/call-sock.ts +151 -0
- package/src/auto/listen/cleanup.ts +102 -0
- package/src/auto/listen/run-check.ts +50 -0
- package/src/auto/listen-sock.ts +245 -0
- package/src/auto/load-ts.ts +37 -0
- package/src/auto/runtime.ts +19 -0
- package/src/auto/utils/glob.ts +83 -0
- package/src/browser.ts +3 -1
- package/src/index.ts +3 -3
- package/src/route.ts +8 -3
- package/src/sign.ts +22 -9
- package/src/test/static.ts +22 -0
- package/src/utils/is-engine.ts +15 -0
- package/src/validator/index.ts +3 -1
- package/src/connect.ts +0 -67
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@kevisual/router",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.24",
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/router.js",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"build": "npm run clean && rollup -c",
|
|
11
11
|
"build:app": "npm run build && rsync dist/*browser* ../deploy/dist",
|
|
12
12
|
"watch": "rollup -c -w",
|
|
13
|
-
"clean": "rm -rf dist"
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"auto:bun": "bun test/auto/app.ts",
|
|
15
|
+
"auto:deno": "bun test/auto/app.ts"
|
|
14
16
|
},
|
|
15
17
|
"files": [
|
|
16
18
|
"dist",
|
|
@@ -21,19 +23,24 @@
|
|
|
21
23
|
"author": "abearxiong",
|
|
22
24
|
"license": "MIT",
|
|
23
25
|
"devDependencies": {
|
|
24
|
-
"@kevisual/
|
|
26
|
+
"@kevisual/local-proxy": "^0.0.3",
|
|
27
|
+
"@kevisual/query": "^0.0.29",
|
|
25
28
|
"@rollup/plugin-alias": "^5.1.1",
|
|
26
|
-
"@rollup/plugin-commonjs": "^28.0.
|
|
29
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
27
30
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
28
|
-
"@rollup/plugin-typescript": "^12.1.
|
|
31
|
+
"@rollup/plugin-typescript": "^12.1.3",
|
|
32
|
+
"@types/bun": "^1.2.10",
|
|
33
|
+
"@types/deno": "^2.3.0",
|
|
29
34
|
"@types/lodash-es": "^4.17.12",
|
|
30
|
-
"@types/node": "^
|
|
35
|
+
"@types/node": "^24.0.3",
|
|
36
|
+
"@types/send": "^0.17.5",
|
|
31
37
|
"@types/ws": "^8.18.1",
|
|
32
38
|
"@types/xml2js": "^0.4.14",
|
|
33
39
|
"cookie": "^1.0.2",
|
|
40
|
+
"deno": "^2.4.3",
|
|
34
41
|
"lodash-es": "^4.17.21",
|
|
35
42
|
"nanoid": "^5.1.5",
|
|
36
|
-
"rollup": "^4.
|
|
43
|
+
"rollup": "^4.44.0",
|
|
37
44
|
"rollup-plugin-dts": "^6.2.1",
|
|
38
45
|
"ts-loader": "^9.5.2",
|
|
39
46
|
"ts-node": "^10.9.2",
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
"typescript": "^5.8.3",
|
|
42
49
|
"ws": "npm:@kevisual/ws",
|
|
43
50
|
"xml2js": "^0.6.2",
|
|
44
|
-
"zod": "^3.25.
|
|
51
|
+
"zod": "^3.25.67"
|
|
45
52
|
},
|
|
46
53
|
"repository": {
|
|
47
54
|
"type": "git",
|
|
@@ -49,7 +56,8 @@
|
|
|
49
56
|
},
|
|
50
57
|
"dependencies": {
|
|
51
58
|
"path-to-regexp": "^8.2.0",
|
|
52
|
-
"selfsigned": "^2.4.1"
|
|
59
|
+
"selfsigned": "^2.4.1",
|
|
60
|
+
"send": "^1.2.0"
|
|
53
61
|
},
|
|
54
62
|
"publishConfig": {
|
|
55
63
|
"access": "public"
|
|
@@ -90,6 +98,11 @@
|
|
|
90
98
|
"require": "./mod.ts",
|
|
91
99
|
"types": "./mod.d.ts"
|
|
92
100
|
},
|
|
101
|
+
"./auto.ts": {
|
|
102
|
+
"import": "./auto.ts",
|
|
103
|
+
"require": "./auto.ts",
|
|
104
|
+
"types": "./auto.ts"
|
|
105
|
+
},
|
|
93
106
|
"./src/*": {
|
|
94
107
|
"import": "./src/*",
|
|
95
108
|
"require": "./src/*"
|
package/readme.md
CHANGED
|
@@ -20,3 +20,17 @@ app
|
|
|
20
20
|
})
|
|
21
21
|
.addTo(app);
|
|
22
22
|
```
|
|
23
|
+
## 兼容服务器
|
|
24
|
+
```
|
|
25
|
+
import { App } from '@kevisual/router';
|
|
26
|
+
|
|
27
|
+
const app = new App();
|
|
28
|
+
app.listen(4002);
|
|
29
|
+
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
|
|
30
|
+
initProxy({
|
|
31
|
+
pagesDir: './demo',
|
|
32
|
+
watch: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.onServerRequest(proxyRoute);
|
|
36
|
+
```
|
package/src/app.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Server, ServerOpts, HandleCtx } from './server/server.ts';
|
|
|
3
3
|
import { WsServer } from './server/ws-server.ts';
|
|
4
4
|
import { CustomError } from './result/error.ts';
|
|
5
5
|
import { handleServer } from './server/handle-server.ts';
|
|
6
|
-
import { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
7
|
|
|
8
8
|
type RouterHandle = (msg: { path: string; [key: string]: any }) => { code: string; data?: any; message?: string; [key: string]: any };
|
|
9
9
|
type AppOptions<T = {}> = {
|
|
@@ -105,6 +105,12 @@ export class App<T = {}, U = AppReqRes> {
|
|
|
105
105
|
static handleRequest(req: IncomingMessage, res: ServerResponse) {
|
|
106
106
|
return handleServer(req, res);
|
|
107
107
|
}
|
|
108
|
+
onServerRequest(fn: (req: IncomingMessage, res: ServerResponse) => void) {
|
|
109
|
+
if (!this.server) {
|
|
110
|
+
throw new Error('Server is not initialized');
|
|
111
|
+
}
|
|
112
|
+
this.server.on(fn);
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
export * from './browser.ts';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
|
|
3
|
+
type QueryData = {
|
|
4
|
+
path?: string;
|
|
5
|
+
key?: string;
|
|
6
|
+
payload?: any;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type CallSockOptions = {
|
|
11
|
+
socketPath?: string;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
method?: 'GET' | 'POST';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const callSock = async (data: QueryData, options: CallSockOptions = {}): Promise<any> => {
|
|
17
|
+
const { socketPath = './app.sock', timeout = 5000, method = 'POST' } = options;
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const client = createConnection(socketPath);
|
|
21
|
+
let responseData = '';
|
|
22
|
+
let timer: NodeJS.Timeout;
|
|
23
|
+
|
|
24
|
+
// 设置超时
|
|
25
|
+
if (timeout > 0) {
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
client.destroy();
|
|
28
|
+
reject(new Error(`Socket call timeout after ${timeout}ms`));
|
|
29
|
+
}, timeout);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
client.on('connect', () => {
|
|
33
|
+
try {
|
|
34
|
+
let request: string;
|
|
35
|
+
|
|
36
|
+
if (method === 'GET') {
|
|
37
|
+
// GET 请求:参数放在 URL 中
|
|
38
|
+
const searchParams = new URLSearchParams();
|
|
39
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
40
|
+
if (key === 'payload' && typeof value === 'object') {
|
|
41
|
+
searchParams.append(key, JSON.stringify(value));
|
|
42
|
+
} else {
|
|
43
|
+
searchParams.append(key, String(value));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const queryString = searchParams.toString();
|
|
48
|
+
const url = queryString ? `/?${queryString}` : '/';
|
|
49
|
+
|
|
50
|
+
request = [`GET ${url} HTTP/1.1`, 'Host: localhost', 'Connection: close', '', ''].join('\r\n');
|
|
51
|
+
} else {
|
|
52
|
+
// POST 请求:数据放在 body 中
|
|
53
|
+
const body = JSON.stringify(data);
|
|
54
|
+
const contentLength = Buffer.byteLength(body, 'utf8');
|
|
55
|
+
|
|
56
|
+
request = [
|
|
57
|
+
'POST / HTTP/1.1',
|
|
58
|
+
'Host: localhost',
|
|
59
|
+
'Content-Type: application/json',
|
|
60
|
+
`Content-Length: ${contentLength}`,
|
|
61
|
+
'Connection: close',
|
|
62
|
+
'',
|
|
63
|
+
body,
|
|
64
|
+
].join('\r\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
client.write(request);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (timer) clearTimeout(timer);
|
|
70
|
+
client.destroy();
|
|
71
|
+
reject(error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
client.on('data', (chunk) => {
|
|
76
|
+
responseData += chunk.toString();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
client.on('end', () => {
|
|
80
|
+
if (timer) clearTimeout(timer);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// 解析 HTTP 响应
|
|
84
|
+
const response = parseHttpResponse(responseData);
|
|
85
|
+
|
|
86
|
+
if (response.statusCode >= 400) {
|
|
87
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.body}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 尝试解析 JSON 响应
|
|
92
|
+
try {
|
|
93
|
+
const result = JSON.parse(response.body);
|
|
94
|
+
resolve(result);
|
|
95
|
+
} catch {
|
|
96
|
+
// 如果不是 JSON,直接返回文本
|
|
97
|
+
resolve(response.body);
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
reject(error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
client.on('error', (error) => {
|
|
105
|
+
if (timer) clearTimeout(timer);
|
|
106
|
+
reject(error);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
client.on('timeout', () => {
|
|
110
|
+
if (timer) clearTimeout(timer);
|
|
111
|
+
client.destroy();
|
|
112
|
+
reject(new Error('Socket connection timeout'));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// 解析 HTTP 响应的辅助函数
|
|
118
|
+
function parseHttpResponse(responseData: string) {
|
|
119
|
+
const [headerSection, ...bodyParts] = responseData.split('\r\n\r\n');
|
|
120
|
+
const body = bodyParts.join('\r\n\r\n');
|
|
121
|
+
|
|
122
|
+
const lines = headerSection.split('\r\n');
|
|
123
|
+
const statusLine = lines[0];
|
|
124
|
+
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
|
|
125
|
+
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;
|
|
126
|
+
|
|
127
|
+
const headers: Record<string, string> = {};
|
|
128
|
+
for (let i = 1; i < lines.length; i++) {
|
|
129
|
+
const [key, ...valueParts] = lines[i].split(':');
|
|
130
|
+
if (key && valueParts.length > 0) {
|
|
131
|
+
headers[key.trim().toLowerCase()] = valueParts.join(':').trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
statusCode,
|
|
137
|
+
headers,
|
|
138
|
+
body: body || '',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 便捷方法
|
|
143
|
+
export const callSockGet = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
|
|
144
|
+
return callSock(data, { ...options, method: 'GET' });
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const callSockPost = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
|
|
148
|
+
return callSock(data, { ...options, method: 'POST' });
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const autoCall = callSockPost;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getRuntime } from '../runtime.ts';
|
|
2
|
+
|
|
3
|
+
let isClean = false;
|
|
4
|
+
export const deleteFileDetached = async (path: string, pidPath: string = './app.pid') => {
|
|
5
|
+
const runtime = getRuntime();
|
|
6
|
+
if (runtime.isDeno) {
|
|
7
|
+
// Deno 实现 - 启动后不等待结果
|
|
8
|
+
const process = new Deno.Command('sh', {
|
|
9
|
+
args: ['-c', `rm -f "${path}" & rm -f "${pidPath}"`],
|
|
10
|
+
stdout: 'null',
|
|
11
|
+
stderr: 'null',
|
|
12
|
+
});
|
|
13
|
+
process.spawn(); // 不等待结果
|
|
14
|
+
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const { spawn } = await import('node:child_process');
|
|
18
|
+
const child = spawn('sh', ['-c', `rm -f "${path}" & rm -f "${pidPath}"`], {
|
|
19
|
+
detached: true,
|
|
20
|
+
stdio: 'ignore',
|
|
21
|
+
});
|
|
22
|
+
child.unref(); // 完全分离
|
|
23
|
+
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CleanupOptions = {
|
|
27
|
+
path: string;
|
|
28
|
+
close?: () => Promise<void>;
|
|
29
|
+
pidPath?: string;
|
|
30
|
+
};
|
|
31
|
+
export const cleanup = async ({ path, close = async () => {}, pidPath = './app.pid' }: CleanupOptions) => {
|
|
32
|
+
const runtime = getRuntime();
|
|
33
|
+
|
|
34
|
+
// 检查文件是否存在并删除
|
|
35
|
+
const cleanupFile = async () => {
|
|
36
|
+
if (isClean) return;
|
|
37
|
+
isClean = true;
|
|
38
|
+
if (runtime.isDeno) {
|
|
39
|
+
await deleteFileDetached(path, pidPath);
|
|
40
|
+
}
|
|
41
|
+
await close();
|
|
42
|
+
if (!runtime.isDeno) {
|
|
43
|
+
await deleteFileDetached(path, pidPath);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 根据运行时环境注册不同的退出监听器
|
|
48
|
+
if (runtime.isDeno) {
|
|
49
|
+
// Deno 环境
|
|
50
|
+
const handleSignal = () => {
|
|
51
|
+
cleanupFile();
|
|
52
|
+
Deno.exit(0);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
Deno.addSignalListener('SIGINT', handleSignal);
|
|
57
|
+
Deno.addSignalListener('SIGTERM', handleSignal);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn('[DEBUG] Failed to add signal listeners:', error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 对于 beforeunload 和 unload,使用异步清理
|
|
63
|
+
const handleUnload = () => {
|
|
64
|
+
cleanupFile();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
globalThis.addEventListener('beforeunload', handleUnload);
|
|
68
|
+
globalThis.addEventListener('unload', handleUnload);
|
|
69
|
+
} else if (runtime.isNode || runtime.isBun) {
|
|
70
|
+
// Node.js 和 Bun 环境
|
|
71
|
+
import('process').then(({ default: process }) => {
|
|
72
|
+
// 信号处理使用同步清理,然后退出
|
|
73
|
+
const signalHandler = async (signal: string) => {
|
|
74
|
+
await cleanupFile();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
process.on('SIGINT', () => signalHandler('SIGINT'));
|
|
79
|
+
process.on('SIGTERM', () => signalHandler('SIGTERM'));
|
|
80
|
+
process.on('SIGUSR1', () => signalHandler('SIGUSR1'));
|
|
81
|
+
process.on('SIGUSR2', () => signalHandler('SIGUSR2'));
|
|
82
|
+
|
|
83
|
+
process.on('exit', async () => {
|
|
84
|
+
await cleanupFile();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
process.on('uncaughtException', async (error) => {
|
|
88
|
+
console.error('Uncaught Exception:', error);
|
|
89
|
+
await cleanupFile();
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
94
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
95
|
+
await cleanupFile();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 返回手动清理函数,以便需要时主动调用
|
|
101
|
+
return cleanupFile;
|
|
102
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getRuntime } from '../runtime.ts';
|
|
2
|
+
|
|
3
|
+
export const getPid = async () => {
|
|
4
|
+
const runtime = getRuntime();
|
|
5
|
+
|
|
6
|
+
let pid = 0;
|
|
7
|
+
if (runtime.isDeno) {
|
|
8
|
+
pid = Deno.pid;
|
|
9
|
+
} else {
|
|
10
|
+
pid = process.pid;
|
|
11
|
+
}
|
|
12
|
+
return pid;
|
|
13
|
+
};
|
|
14
|
+
export const writeAppid = async (pidPath = './app.pid') => {
|
|
15
|
+
const fs = await import('node:fs');
|
|
16
|
+
const pid = await getPid();
|
|
17
|
+
fs.writeFileSync(pidPath, pid + '');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getPidFromFileAndStop = async () => {
|
|
21
|
+
const fs = await import('node:fs');
|
|
22
|
+
if (fs.existsSync('./app.pid')) {
|
|
23
|
+
const pid = parseInt(fs.readFileSync('./app.pid', 'utf-8'), 10);
|
|
24
|
+
if (!isNaN(pid)) {
|
|
25
|
+
if (pid === 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid);
|
|
30
|
+
console.log(`Stopped process with PID ${pid}`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(`Failed to stop process with PID ${pid}:`, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const runFirstCheck = async (path: string, pidPath: string) => {
|
|
39
|
+
await getPidFromFileAndStop();
|
|
40
|
+
await writeAppid(pidPath);
|
|
41
|
+
try {
|
|
42
|
+
const fs = await import('node:fs');
|
|
43
|
+
if (fs.existsSync(path)) {
|
|
44
|
+
fs.unlinkSync(path);
|
|
45
|
+
console.log(`Socket file ${path} cleaned up during first check`);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`Failed to clean up socket file ${path} during first check:`, error);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http';
|
|
2
|
+
import { QueryRouterServer } from '../index.ts';
|
|
3
|
+
import { getRuntime } from './runtime.ts';
|
|
4
|
+
import { runFirstCheck } from './listen/run-check.ts';
|
|
5
|
+
import { cleanup } from './listen/cleanup.ts';
|
|
6
|
+
|
|
7
|
+
type ListenSocketOptions = {
|
|
8
|
+
path?: string;
|
|
9
|
+
app?: QueryRouterServer;
|
|
10
|
+
pidPath?: string;
|
|
11
|
+
};
|
|
12
|
+
const server = async (req, app: QueryRouterServer) => {
|
|
13
|
+
const runtime = getRuntime();
|
|
14
|
+
let data;
|
|
15
|
+
if (!runtime.isNode) {
|
|
16
|
+
data = await getRequestParams(req);
|
|
17
|
+
} else {
|
|
18
|
+
data = await parseBody(req);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await app.queryRoute(data as any);
|
|
22
|
+
const response = new Response(JSON.stringify(result));
|
|
23
|
+
response.headers.set('Content-Type', 'application/json');
|
|
24
|
+
return response;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const listenSocket = async (options?: ListenSocketOptions) => {
|
|
28
|
+
const path = options?.path || './app.sock';
|
|
29
|
+
const pidPath = options?.pidPath || './app.pid';
|
|
30
|
+
const runtime = getRuntime();
|
|
31
|
+
let app = options?.app || globalThis.context?.app;
|
|
32
|
+
if (!app) {
|
|
33
|
+
app = new QueryRouterServer();
|
|
34
|
+
}
|
|
35
|
+
await runFirstCheck(path, pidPath);
|
|
36
|
+
let close = async () => {};
|
|
37
|
+
cleanup({ path, close });
|
|
38
|
+
if (runtime.isDeno) {
|
|
39
|
+
// 检查 Deno 版本是否支持 Unix domain socket
|
|
40
|
+
try {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
const listener = Deno.listen({
|
|
43
|
+
transport: 'unix',
|
|
44
|
+
path: path,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 处理连接
|
|
48
|
+
(async () => {
|
|
49
|
+
for await (const conn of listener) {
|
|
50
|
+
(async () => {
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
const httpConn = Deno.serveHttp(conn);
|
|
53
|
+
for await (const requestEvent of httpConn) {
|
|
54
|
+
try {
|
|
55
|
+
const response = await server(requestEvent.request, app);
|
|
56
|
+
await requestEvent.respondWith(response);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
close = async () => {
|
|
65
|
+
listener.close();
|
|
66
|
+
};
|
|
67
|
+
return listener;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// 如果 Unix socket 不支持,回退到 HTTP 服务器
|
|
70
|
+
console.warn('Unix socket not supported in this Deno environment, falling back to HTTP server');
|
|
71
|
+
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
const listener = Deno.listen({ port: 0 }); // 使用随机端口
|
|
74
|
+
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
console.log(`Deno server listening on port ${listener.addr.port}`);
|
|
77
|
+
|
|
78
|
+
(async () => {
|
|
79
|
+
for await (const conn of listener) {
|
|
80
|
+
(async () => {
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
const httpConn = Deno.serveHttp(conn);
|
|
83
|
+
for await (const requestEvent of httpConn) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await server(requestEvent.request, app);
|
|
86
|
+
await requestEvent.respondWith(response);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
|
|
95
|
+
return listener;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (runtime.isBun) {
|
|
100
|
+
const bunServer = Bun.serve({
|
|
101
|
+
unix: path,
|
|
102
|
+
fetch(req) {
|
|
103
|
+
return server(req, app);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
close = async () => {
|
|
107
|
+
await bunServer.stop();
|
|
108
|
+
};
|
|
109
|
+
return bunServer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Node.js 环境
|
|
113
|
+
const http = await import('http');
|
|
114
|
+
|
|
115
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const response = await server(req, app);
|
|
118
|
+
|
|
119
|
+
// 设置响应头
|
|
120
|
+
response.headers.forEach((value, key) => {
|
|
121
|
+
res.setHeader(key, value);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 设置状态码
|
|
125
|
+
res.statusCode = response.status;
|
|
126
|
+
|
|
127
|
+
// 读取响应体并写入
|
|
128
|
+
const body = await response.text();
|
|
129
|
+
res.end(body);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error handling request:', error);
|
|
132
|
+
res.statusCode = 500;
|
|
133
|
+
res.end('Internal Server Error');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
httpServer.listen(path);
|
|
138
|
+
close = async () => {
|
|
139
|
+
httpServer.close();
|
|
140
|
+
};
|
|
141
|
+
return httpServer;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const getRequestParams = async (req: Request) => {
|
|
145
|
+
let urlParams: Record<string, any> = {};
|
|
146
|
+
let bodyParams: Record<string, any> = {};
|
|
147
|
+
|
|
148
|
+
// 获取URL参数
|
|
149
|
+
const url = new URL(req.url);
|
|
150
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
151
|
+
// 尝试解析JSON payload
|
|
152
|
+
if (key === 'payload') {
|
|
153
|
+
try {
|
|
154
|
+
urlParams[key] = JSON.parse(value);
|
|
155
|
+
} catch {
|
|
156
|
+
urlParams[key] = value;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
urlParams[key] = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 获取body参数
|
|
164
|
+
if (req.method.toLowerCase() === 'post' && req.body) {
|
|
165
|
+
const contentType = req.headers.get('content-type') || '';
|
|
166
|
+
if (contentType.includes('application/json')) {
|
|
167
|
+
try {
|
|
168
|
+
bodyParams = await req.json();
|
|
169
|
+
} catch {
|
|
170
|
+
// 如果解析失败,保持空对象
|
|
171
|
+
}
|
|
172
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
173
|
+
const formData = await req.text();
|
|
174
|
+
const params = new URLSearchParams(formData);
|
|
175
|
+
for (const [key, value] of params.entries()) {
|
|
176
|
+
bodyParams[key] = value;
|
|
177
|
+
}
|
|
178
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
179
|
+
try {
|
|
180
|
+
const formData = await req.formData();
|
|
181
|
+
for (const [key, value] of formData.entries()) {
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
bodyParams[key] = value instanceof File ? value : value.toString();
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// 如果解析失败,保持空对象
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// body参数优先,合并数据
|
|
192
|
+
return {
|
|
193
|
+
...urlParams,
|
|
194
|
+
...bodyParams,
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
|
199
|
+
return new Promise<T>((resolve, reject) => {
|
|
200
|
+
const arr: any[] = [];
|
|
201
|
+
req.on('data', (chunk) => {
|
|
202
|
+
arr.push(chunk);
|
|
203
|
+
});
|
|
204
|
+
req.on('end', () => {
|
|
205
|
+
try {
|
|
206
|
+
const body = Buffer.concat(arr).toString();
|
|
207
|
+
|
|
208
|
+
// 获取 Content-Type 头信息
|
|
209
|
+
const contentType = req.headers['content-type'] || '';
|
|
210
|
+
|
|
211
|
+
// 处理 application/json
|
|
212
|
+
if (contentType.includes('application/json')) {
|
|
213
|
+
resolve(JSON.parse(body) as T);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// 处理 application/x-www-form-urlencoded
|
|
217
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
218
|
+
const formData = new URLSearchParams(body);
|
|
219
|
+
const result: Record<string, any> = {};
|
|
220
|
+
|
|
221
|
+
formData.forEach((value, key) => {
|
|
222
|
+
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
|
223
|
+
try {
|
|
224
|
+
result[key] = JSON.parse(value);
|
|
225
|
+
} catch {
|
|
226
|
+
result[key] = value;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
resolve(result as T);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 默认尝试 JSON 解析
|
|
235
|
+
try {
|
|
236
|
+
resolve(JSON.parse(body) as T);
|
|
237
|
+
} catch {
|
|
238
|
+
resolve({} as T);
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
resolve({} as T);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
};
|