Compare commits
57 Commits
733677f3f3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 87068cd626 | |||
| ac32ff9d4a | |||
| cc74dc6803 | |||
| 24166f9899 | |||
| 10506503eb | |||
| 2483205a22 | |||
| cd96b53f6e | |||
| 19c4cc2e06 | |||
| 27b63e0a2b | |||
| e8794912b6 | |||
| 187900ad55 | |||
| d2ebb5f488 | |||
| 324c4e9862 | |||
| 51305b71c3 | |||
| adaf954ae7 | |||
| d2a03fc959 | |||
| e31b19f931 | |||
| eede990ec8 | |||
| 51dafe0f9a | |||
| aeeb205e1e | |||
| fc35b531e1 | |||
| 6d148e47f1 | |||
| 896f2f5412 | |||
| afad59e0ab | |||
| 17e515ad32 | |||
| 4c11a44b48 | |||
| 06c3cc4236 | |||
| 8823c15aba | |||
| 0e30dfc6ea | |||
| e8f7f61e09 | |||
| 5911f29c8f | |||
| ba7e00bd7a | |||
| bac3e5b393 | |||
| de3187f5f3 | |||
| 1c3e65df8a | |||
| 986b5687c4 | |||
| 803687b219 | |||
| 871aac104a | |||
| 50c87043c8 | |||
| dc2f282f4b | |||
| c99d03550e | |||
| e0c7d40a9c | |||
| 7f369b7b07 | |||
| 2393dfe273 | |||
| 5e38740c7b | |||
| 926c0a09cd | |||
| 780d744a16 | |||
| 303c579e92 | |||
| df737e5f27 | |||
| c462dc31f8 | |||
| d6eb8393e0 | |||
| 52f5f58baf | |||
| 158b12d811 | |||
| 6f0faba703 | |||
| 041432baea | |||
| f265fae3b9 | |||
| 51b2537c25 |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Configure npm authentication
|
- name: Configure npm authentication
|
||||||
run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
|
run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install --no-save
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: npm run build
|
run: npm run build
|
||||||
# Step 6: 发布到 npm
|
# Step 6: 发布到 npm
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
src/app.config.json5
|
src/app.config.json5
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
https-cert.pem
|
||||||
|
https-key.pem
|
||||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||||
20
auto.ts
Normal file
20
auto.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { loadTS, getMatchFiles } from './src/auto/load-ts.ts';
|
||||||
|
import { listenSocket } from './src/auto/listen-sock.ts';
|
||||||
|
import { Route, QueryRouter, QueryRouterServer } from './src/route.ts';
|
||||||
|
|
||||||
|
export { Route, QueryRouter, QueryRouterServer };
|
||||||
|
|
||||||
|
export const App = QueryRouterServer;
|
||||||
|
|
||||||
|
export { createSchema } from './src/validator/index.ts';
|
||||||
|
export type { Rule } from './src/validator/rule.ts';
|
||||||
|
export type { Schema } from 'zod';
|
||||||
|
export type { RouteContext, RouteOpts } from './src/route.ts';
|
||||||
|
|
||||||
|
export type { Run } from './src/route.ts';
|
||||||
|
|
||||||
|
export { CustomError } from './src/result/error.ts';
|
||||||
|
|
||||||
|
export { listenSocket, loadTS, getMatchFiles };
|
||||||
|
|
||||||
|
export { autoCall } from './src/auto/call-sock.ts';
|
||||||
1
demo/deno/a/index.html
Normal file
1
demo/deno/a/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
abc
|
||||||
1
demo/simple/.gitignore
vendored
Normal file
1
demo/simple/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pem
|
||||||
60
demo/simple/package-lock.json
generated
60
demo/simple/package-lock.json
generated
@@ -9,41 +9,49 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abearxiong/router": "../.."
|
"@kevisual/router": "../.."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../..": {
|
"../..": {
|
||||||
"name": "@kevisual/router",
|
"name": "@kevisual/router",
|
||||||
"version": "0.0.2",
|
"version": "0.0.23",
|
||||||
"license": "ISC",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0"
|
"path-to-regexp": "^8.2.0",
|
||||||
|
"selfsigned": "^2.4.1",
|
||||||
|
"send": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^26.0.1",
|
"@kevisual/local-proxy": "^0.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.4",
|
"@kevisual/query": "^0.0.29",
|
||||||
"@rollup/plugin-typescript": "^12.1.0",
|
"@rollup/plugin-alias": "^5.1.1",
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.6",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
|
"@rollup/plugin-typescript": "^12.1.3",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^24.0.3",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/send": "^0.17.5",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.1.5",
|
||||||
"rollup": "^4.22.4",
|
"rollup": "^4.44.0",
|
||||||
"ts-loader": "^9.5.1",
|
"rollup-plugin-dts": "^6.2.1",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.7.0",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.8.3",
|
||||||
"zod": "^3.23.8"
|
"ws": "npm:@kevisual/ws",
|
||||||
|
"xml2js": "^0.6.2",
|
||||||
|
"zod": "^3.25.67"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@abearxiong/router": {
|
|
||||||
"resolved": "../..",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -85,6 +93,10 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kevisual/router": {
|
||||||
|
"resolved": "../..",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.11.tgz",
|
"resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
@@ -157,6 +169,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abearxiong/router": "../.."
|
"@kevisual/router": "../.."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { App } from '@abearxiong/router';
|
import { App } from '@kevisual/router';
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Route, App } from '@abearxiong/router';
|
import { Route, App } from '@kevisual/router';
|
||||||
|
|
||||||
const app = new App({ io: true });
|
const app = new App({ io: true });
|
||||||
app.listen(4002);
|
app.listen(4002);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Route, App } from '@abearxiong/router';
|
import { Route, App } from '@kevisual/router';
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
app.listen(4003);
|
app.listen(4003);
|
||||||
|
|||||||
35
demo/simple/src/apps-https/app.ts
Normal file
35
demo/simple/src/apps-https/app.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Route, App } from '@kevisual/router';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
const app = new App({
|
||||||
|
serverOptions: {
|
||||||
|
cors: {},
|
||||||
|
httpType: 'https',
|
||||||
|
httpsKey: readFileSync('https-key.pem', 'utf8'),
|
||||||
|
httpsCert: readFileSync('https-cert.pem', 'utf-8'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
app.listen(4003, '0.0.0.0', () => {
|
||||||
|
console.log(`http://localhost:4003/api/router?path=demo&key=02`);
|
||||||
|
console.log(`http://localhost:4003/api/router?path=demo&key=01`);
|
||||||
|
console.log(`https://192.168.31.220:4003/api/router?path=demo&key=01`);
|
||||||
|
});
|
||||||
|
const route01 = new Route('demo', '01');
|
||||||
|
route01.run = async (ctx) => {
|
||||||
|
ctx.body = '01';
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
app.use(
|
||||||
|
'demo',
|
||||||
|
async (ctx) => {
|
||||||
|
ctx.body = '01';
|
||||||
|
return ctx;
|
||||||
|
},
|
||||||
|
{ key: '01' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const route02 = new Route('demo', '02');
|
||||||
|
route02.run = async (ctx) => {
|
||||||
|
ctx.body = '02';
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
app.addRoute(route02);
|
||||||
6
demo/simple/src/apps-https/create-sign.ts
Normal file
6
demo/simple/src/apps-https/create-sign.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createCert } from '@kevisual/router/sign';
|
||||||
|
import { writeFileSync } from 'fs';
|
||||||
|
const { key, cert } = createCert();
|
||||||
|
|
||||||
|
writeFileSync('https-key.pem', key);
|
||||||
|
writeFileSync('https-cert.pem', cert);
|
||||||
43
demo/simple/src/browser/app.ts
Normal file
43
demo/simple/src/browser/app.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { CustomError, QueryRouterServer } from '@kevisual/router/browser';
|
||||||
|
|
||||||
|
const router = new QueryRouterServer();
|
||||||
|
|
||||||
|
router
|
||||||
|
.route({
|
||||||
|
path: 'hello',
|
||||||
|
key: 'world',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'Hello, world!';
|
||||||
|
})
|
||||||
|
.addTo(router);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route({
|
||||||
|
path: 'hello',
|
||||||
|
key: 'world2',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'Hello, world!';
|
||||||
|
// throw new CustomError('error');
|
||||||
|
throw new CustomError(5000, 'error');
|
||||||
|
})
|
||||||
|
.addTo(router);
|
||||||
|
|
||||||
|
router
|
||||||
|
.run({
|
||||||
|
path: 'hello',
|
||||||
|
key: 'world',
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router
|
||||||
|
.run({
|
||||||
|
path: 'hello',
|
||||||
|
key: 'world2',
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
19
demo/simple/src/cert/index.ts
Normal file
19
demo/simple/src/cert/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createCert } from '@kevisual/router/src/sign.ts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
const cert = createCert();
|
||||||
|
|
||||||
|
fs.writeFileSync('pem/https-private-key.pem', cert.key);
|
||||||
|
fs.writeFileSync('pem/https-cert.pem', cert.cert);
|
||||||
|
fs.writeFileSync(
|
||||||
|
'pem/https-config.json',
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
createTime: new Date().getTime(),
|
||||||
|
expireDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).getTime(),
|
||||||
|
expireTime: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
78
demo/simple/src/check-handle-context/app.ts
Normal file
78
demo/simple/src/check-handle-context/app.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { App } from '@kevisual/router';
|
||||||
|
import { QueryRouterServer } from '@kevisual/router';
|
||||||
|
const app = new App();
|
||||||
|
const queryApp = new QueryRouterServer();
|
||||||
|
|
||||||
|
// queryApp
|
||||||
|
// .route({
|
||||||
|
// path: 'api',
|
||||||
|
// })
|
||||||
|
// .define(async (ctx) => {
|
||||||
|
// ctx.throw(404, 'Not Found');
|
||||||
|
// ctx.throw(500, 'Internal Server Error');
|
||||||
|
// })
|
||||||
|
// .addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'hello',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
// console.log('hello', ctx);
|
||||||
|
// console.log('hello', ctx.res);
|
||||||
|
console.log('hello', ctx.query.cookies);
|
||||||
|
// ctx.res?.cookie?.('token', 'abc', {
|
||||||
|
// domain: '*', // 设置为顶级域名,允许跨子域共享
|
||||||
|
// // httpOnly: true,
|
||||||
|
// // secure: true,
|
||||||
|
// // sameSite: 'Lax',
|
||||||
|
// });
|
||||||
|
ctx.res.cookie('token', 'abc', {
|
||||||
|
// domain: '*', // 设置为顶级域名,允许跨子域共享
|
||||||
|
// httpOnly: true,
|
||||||
|
// secure: true,
|
||||||
|
// sameSite: 'Lax',
|
||||||
|
});
|
||||||
|
ctx.res.cookie('test_cookie', 'abc', {
|
||||||
|
maxAge: 0,
|
||||||
|
path: '/api/router',
|
||||||
|
});
|
||||||
|
ctx.res.cookie('test_cookie', 'abc', {
|
||||||
|
maxAge: 0,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
ctx.res.cookie('user', 'abc', {
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
|
ctx.res.cookie('session', 'abc', {
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
|
ctx.res.cookie('preferences', 'abc', {
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
|
// const cookies = [
|
||||||
|
// cookie.serialize('user', 'john_doe', {
|
||||||
|
// httpOnly: true,
|
||||||
|
// maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||||
|
// sameSite: 'lax',
|
||||||
|
// }),
|
||||||
|
// cookie.serialize('session', 'xyz123', {
|
||||||
|
// httpOnly: true,
|
||||||
|
// maxAge: 60 * 60 * 24, // 1 day
|
||||||
|
// }),
|
||||||
|
// cookie.serialize('preferences', JSON.stringify({ theme: 'dark' }), {
|
||||||
|
// httpOnly: false, // Accessible via JavaScript
|
||||||
|
// maxAge: 60 * 60 * 24 * 30, // 1 month
|
||||||
|
// }),
|
||||||
|
// ];
|
||||||
|
// ctx.res.setHeader('Set-Cookie', cookies);
|
||||||
|
ctx.res.end('hello' + Math.random().toString(32).slice(2));
|
||||||
|
|
||||||
|
ctx.end = true;
|
||||||
|
return;
|
||||||
|
ctx.body = 'world';
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app.listen(3100, () => {
|
||||||
|
console.log('listening on port http://localhost:3100');
|
||||||
|
});
|
||||||
14
demo/simple/src/check-schema/index.ts
Normal file
14
demo/simple/src/check-schema/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createSchema } from '@kevisual/router';
|
||||||
|
|
||||||
|
const a = createSchema({
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 10,
|
||||||
|
regex: '^[a-zA-Z0-9_]+$',
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(a.safeParse('1234567890'));
|
||||||
|
console.log(a.safeParse('').error);
|
||||||
|
console.log(a.safeParse(undefined));
|
||||||
|
console.log(a.safeParse(null).error);
|
||||||
11
demo/simple/src/define/app.ts
Normal file
11
demo/simple/src/define/app.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// import { App } from '@kevisual/router';
|
||||||
|
import { QueryRouterServer as App } from '@kevisual/router';
|
||||||
|
import { QueryUtil } from '@kevisual/router/define';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
const w = QueryUtil.create({
|
||||||
|
a: { path: 'a', description: 'sdf' },
|
||||||
|
});
|
||||||
|
|
||||||
|
app.route(w.get('a'));
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { QueryRouter, Route, Server } from '@abearxiong/router';
|
import { QueryRouter, Route, Server } from '@kevisual/router';
|
||||||
|
|
||||||
const router = new QueryRouter();
|
const router = new QueryRouter();
|
||||||
|
|
||||||
|
|||||||
28
demo/simple/src/nopath.ts
Normal file
28
demo/simple/src/nopath.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Route, App } from '@kevisual/router/src/index.ts';
|
||||||
|
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
description: 'sdf'
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
ctx.body = 'this is no path fns';
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
|
||||||
|
let id = ''
|
||||||
|
console.log('routes', app.router.routes.map(item => {
|
||||||
|
id = item.id;
|
||||||
|
return {
|
||||||
|
path: item.path,
|
||||||
|
key: item.key,
|
||||||
|
id: item.id,
|
||||||
|
description: item.description
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.call({id: id}).then(res => {
|
||||||
|
console.log('id', id);
|
||||||
|
console.log('res', res);
|
||||||
|
})
|
||||||
28
demo/simple/src/simple-router/a.ts
Normal file
28
demo/simple/src/simple-router/a.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { SimpleRouter } from '@kevisual/router/simple';
|
||||||
|
|
||||||
|
const router = new SimpleRouter();
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
console.log('get /');
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/post', async (req, res) => {
|
||||||
|
console.log('post /post');
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/user/:id', async (req, res) => {
|
||||||
|
console.log('get /user/:id', req.params);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/user/:id', async (req, res) => {
|
||||||
|
console.log('post /user/:id', req.params);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/user/:id/a', async (req, res) => {
|
||||||
|
console.log('post /user/:id', req.params);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.parse({ url: 'http://localhost:3000/', method: 'GET' } as any, {} as any);
|
||||||
|
router.parse({ url: 'http://localhost:3000/post', method: 'POST' } as any, {} as any);
|
||||||
|
router.parse({ url: 'http://localhost:3000/user/1/a', method: 'GET' } as any, {} as any);
|
||||||
|
router.parse({ url: 'http://localhost:3000/user/1/a', method: 'POST' } as any, {} as any);
|
||||||
13
demo/simple/src/test-path/index.ts
Normal file
13
demo/simple/src/test-path/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { pathToRegexp } from 'path-to-regexp';
|
||||||
|
// import { match } from 'path-to-regexp';
|
||||||
|
|
||||||
|
const pattern = pathToRegexp('/users/*splat');
|
||||||
|
const match = pattern.regexp.exec('/users/123/j/d/f');
|
||||||
|
console.log(match);
|
||||||
|
// const pattern = pathToRegexp('/users/:id');
|
||||||
|
// const match = pattern.regexp.exec('/users/123');
|
||||||
|
// console.log(match);
|
||||||
|
// const pattern = '/a/b/*splat';
|
||||||
|
// const matchPath = match(pattern);
|
||||||
|
// console.log(matchPath('/a/b/c'));
|
||||||
|
|
||||||
76
demo/simple/src/test-two-app/app.ts
Normal file
76
demo/simple/src/test-two-app/app.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { App } from '@kevisual/router';
|
||||||
|
|
||||||
|
const app1 = new App();
|
||||||
|
|
||||||
|
app1
|
||||||
|
.route({
|
||||||
|
path: 'app1',
|
||||||
|
key: '01',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '01';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app1);
|
||||||
|
|
||||||
|
app1
|
||||||
|
.route({
|
||||||
|
path: 'app1',
|
||||||
|
key: '02',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '02';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app1);
|
||||||
|
|
||||||
|
const app2 = new App();
|
||||||
|
|
||||||
|
app2
|
||||||
|
.route({
|
||||||
|
path: 'app2',
|
||||||
|
key: '01',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'app2' + '01';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app2);
|
||||||
|
|
||||||
|
app2
|
||||||
|
.route({
|
||||||
|
path: 'app2',
|
||||||
|
key: '02',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'app2' + '02';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app2);
|
||||||
|
const app3 = new App();
|
||||||
|
app3
|
||||||
|
.route({
|
||||||
|
path: 'app3',
|
||||||
|
key: '01',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'app3' + '01';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app3);
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
app.importRoutes(app1.exportRoutes());
|
||||||
|
|
||||||
|
app.importRoutes(app2.exportRoutes());
|
||||||
|
|
||||||
|
app.importApp(app3);
|
||||||
|
|
||||||
|
app.listen(4003, () => {
|
||||||
|
console.log(`http://localhost:4003/api/router?path=app1&key=02`);
|
||||||
|
console.log(`http://localhost:4003/api/router?path=app1&key=01`);
|
||||||
|
console.log(`http://localhost:4003/api/router?path=app2&key=02`);
|
||||||
|
console.log(`http://localhost:4003/api/router?path=app2&key=01`);
|
||||||
|
console.log(`http://localhost:4003/api/router?path=app3&key=01`);
|
||||||
|
});
|
||||||
52
demo/simple/src/three-link/three.ts
Normal file
52
demo/simple/src/three-link/three.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Route, App } from '@kevisual/router';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
const app = new App({
|
||||||
|
serverOptions: {
|
||||||
|
cors: {},
|
||||||
|
httpType: 'https',
|
||||||
|
httpsKey: readFileSync('https-key.pem', 'utf8'),
|
||||||
|
httpsCert: readFileSync('https-cert.pem', 'utf-8'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'demo',
|
||||||
|
key: '01',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.token = '01';
|
||||||
|
ctx.body = '01';
|
||||||
|
ctx.state.t01 = '01';
|
||||||
|
console.log('state01', ctx.state);
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'demo',
|
||||||
|
key: '02',
|
||||||
|
middleware: [{ path: 'demo', key: '01' } as Route],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '02';
|
||||||
|
ctx.state.t02 = '02';
|
||||||
|
console.log('state02', ctx.state, 't', ctx.token);
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'demo',
|
||||||
|
key: '03',
|
||||||
|
middleware: [{ path: 'demo', key: '02' } as Route],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '03';
|
||||||
|
console.log('state03', ctx.state);
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app.call({ path: 'demo', key: '03' }).then((ctx) => {
|
||||||
|
console.log('result', ctx.body);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Route, QueryRouter, RouteContext } from '@abearxiong/router';
|
import { Route, QueryRouter, RouteContext } from '@kevisual/router';
|
||||||
const qr = new QueryRouter();
|
const qr = new QueryRouter();
|
||||||
|
|
||||||
qr.add(
|
qr.add(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"newLine": "LF",
|
"newLine": "LF",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
"rootDir": "./",
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types",
|
"node_modules/@types",
|
||||||
],
|
],
|
||||||
|
|||||||
13
mod.ts
Normal file
13
mod.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Route, QueryRouter, QueryRouterServer } from './src/route.ts';
|
||||||
|
|
||||||
|
export { App } from './src/app.ts';
|
||||||
|
|
||||||
|
export { Route, QueryRouter, QueryRouterServer };
|
||||||
|
|
||||||
|
export { Rule, Schema, createSchema } from './src/validator/index.ts';
|
||||||
|
|
||||||
|
export type { RouteContext, RouteOpts } from './src/route.ts';
|
||||||
|
|
||||||
|
export type { Run } from './src/route.ts';
|
||||||
|
|
||||||
|
export { CustomError } from './src/result/error.ts';
|
||||||
97
package.json
97
package.json
@@ -1,44 +1,101 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package",
|
"$schema": "https://json.schemastore.org/package",
|
||||||
"name": "@kevisual/router",
|
"name": "@kevisual/router",
|
||||||
"version": "0.0.2",
|
"version": "0.0.31",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist/router.js",
|
||||||
|
"types": "./dist/router.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run clean && rollup -c",
|
"build": "npm run clean && rollup -c",
|
||||||
|
"build:app": "npm run build && rsync dist/*browser* ../deploy/dist",
|
||||||
"watch": "rollup -c -w",
|
"watch": "rollup -c -w",
|
||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"src",
|
||||||
|
"mod.ts"
|
||||||
],
|
],
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "abearxiong",
|
||||||
"license": "ISC",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^26.0.1",
|
"@kevisual/local-proxy": "^0.0.6",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.4",
|
"@kevisual/query": "^0.0.29",
|
||||||
"@rollup/plugin-typescript": "^12.1.0",
|
"@rollup/plugin-alias": "^5.1.1",
|
||||||
|
"@rollup/plugin-commonjs": "28.0.8",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^24.9.1",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/send": "^1.2.0",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.1.6",
|
||||||
"rollup": "^4.22.4",
|
"rollup": "^4.52.5",
|
||||||
"ts-loader": "^9.5.1",
|
"rollup-plugin-dts": "^6.2.3",
|
||||||
|
"ts-loader": "^9.5.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.7.0",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.9.3",
|
||||||
"zod": "^3.23.8"
|
"ws": "npm:@kevisual/ws",
|
||||||
|
"xml2js": "^0.6.2",
|
||||||
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/abearxiong/kevisual-router.git"
|
"url": "git+https://github.com/abearxiong/kevisual-router.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0"
|
"path-to-regexp": "^8.3.0",
|
||||||
|
"selfsigned": "^3.0.1",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/router.js",
|
||||||
|
"require": "./dist/router.js",
|
||||||
|
"types": "./dist/router.d.ts"
|
||||||
|
},
|
||||||
|
"./browser": {
|
||||||
|
"import": "./dist/router-browser.js",
|
||||||
|
"require": "./dist/router-browser.js",
|
||||||
|
"types": "./dist/router-browser.d.ts"
|
||||||
|
},
|
||||||
|
"./sign": {
|
||||||
|
"import": "./dist/router-sign.js",
|
||||||
|
"require": "./dist/router-sign.js",
|
||||||
|
"types": "./dist/router-sign.d.ts"
|
||||||
|
},
|
||||||
|
"./simple": {
|
||||||
|
"import": "./dist/router-simple.js",
|
||||||
|
"require": "./dist/router-simple.js",
|
||||||
|
"types": "./dist/router-simple.d.ts"
|
||||||
|
},
|
||||||
|
"./define": {
|
||||||
|
"import": "./dist/router-define.js",
|
||||||
|
"require": "./dist/router-define.js",
|
||||||
|
"types": "./dist/router-define.d.ts"
|
||||||
|
},
|
||||||
|
"./simple-lib": {
|
||||||
|
"import": "./dist/router-simple-lib.js",
|
||||||
|
"require": "./dist/router-simple-lib.js",
|
||||||
|
"types": "./dist/router-simple-lib.d.ts"
|
||||||
|
},
|
||||||
|
"./mod.ts": {
|
||||||
|
"import": "./mod.ts",
|
||||||
|
"require": "./mod.ts",
|
||||||
|
"types": "./mod.d.ts"
|
||||||
|
},
|
||||||
|
"./src/*": {
|
||||||
|
"import": "./src/*",
|
||||||
|
"require": "./src/*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
25
readme.md
25
readme.md
@@ -1,23 +1,15 @@
|
|||||||
# router
|
# router
|
||||||
|
|
||||||
```
|
```ts
|
||||||
import { App } from '@kevisual/router';
|
import { App } from '@kevisual/router';
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
app.listen(4002);
|
app.listen(4002);
|
||||||
|
|
||||||
new app.Route('demo', '01')
|
|
||||||
.define(async (ctx) => {
|
|
||||||
ctx.body = '01';
|
|
||||||
return ctx;
|
|
||||||
})
|
|
||||||
.addTo(app);
|
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({path:'demo', key: '02})
|
.route({path:'demo', key: '02})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
ctx.body = '02';
|
ctx.body = '02';
|
||||||
return ctx;
|
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
@@ -25,7 +17,20 @@ app
|
|||||||
.route('demo', '03')
|
.route('demo', '03')
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
ctx.body = '03';
|
ctx.body = '03';
|
||||||
return ctx;
|
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
```
|
```
|
||||||
|
## 兼容服务器
|
||||||
|
```
|
||||||
|
import { App } from '@kevisual/router';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
app.listen(4002);
|
||||||
|
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
|
||||||
|
initProxy({
|
||||||
|
pagesDir: './demo',
|
||||||
|
watch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.onServerRequest(proxyRoute);
|
||||||
|
```
|
||||||
171
rollup.config.js
171
rollup.config.js
@@ -3,22 +3,173 @@
|
|||||||
import typescript from '@rollup/plugin-typescript';
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import { dts } from 'rollup-plugin-dts';
|
||||||
|
import alias from '@rollup/plugin-alias';
|
||||||
|
|
||||||
|
const createAlias = () => {
|
||||||
|
return alias({
|
||||||
|
entries: [
|
||||||
|
{ find: 'http', replacement: 'node:http' },
|
||||||
|
{ find: 'https', replacement: 'node:https' },
|
||||||
|
{ find: 'fs', replacement: 'node:fs' },
|
||||||
|
{ find: 'path', replacement: 'node:path' },
|
||||||
|
{ find: 'crypto', replacement: 'node:crypto' },
|
||||||
|
{ find: 'zlib', replacement: 'node:zlib' },
|
||||||
|
{ find: 'stream', replacement: 'node:stream' },
|
||||||
|
{ find: 'net', replacement: 'node:net' },
|
||||||
|
{ find: 'tty', replacement: 'node:tty' },
|
||||||
|
{ find: 'tls', replacement: 'node:tls' },
|
||||||
|
{ find: 'buffer', replacement: 'node:buffer' },
|
||||||
|
{ find: 'timers', replacement: 'node:timers' },
|
||||||
|
// { find: 'string_decoder', replacement: 'node:string_decoder' },
|
||||||
|
{ find: 'dns', replacement: 'node:dns' },
|
||||||
|
{ find: 'domain', replacement: 'node:domain' },
|
||||||
|
{ find: 'os', replacement: 'node:os' },
|
||||||
|
{ find: 'events', replacement: 'node:events' },
|
||||||
|
{ find: 'url', replacement: 'node:url' },
|
||||||
|
{ find: 'assert', replacement: 'node:assert' },
|
||||||
|
{ find: 'util', replacement: 'node:util' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* @type {import('rollup').RollupOptions}
|
* @type {import('rollup').RollupOptions}
|
||||||
*/
|
*/
|
||||||
export default {
|
export default [
|
||||||
|
{
|
||||||
input: 'src/index.ts', // TypeScript 入口文件
|
input: 'src/index.ts', // TypeScript 入口文件
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/index.js', // 输出文件
|
file: 'dist/router.js', // 输出文件
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
format: 'es', // 输出格式设置为 ES 模块
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
createAlias(),
|
||||||
// commonjs(),
|
resolve({
|
||||||
typescript({
|
browser: false,
|
||||||
allowImportingTsExtensions: true,
|
}), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
noEmit: true,
|
commonjs(),
|
||||||
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
],
|
],
|
||||||
external: ['ws']
|
},
|
||||||
};
|
{
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/app-browser.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-browser.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/app-browser.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-browser.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/sign.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-sign.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
createAlias(),
|
||||||
|
resolve({
|
||||||
|
browser: false,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/sign.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-sign.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/router-define.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-define.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/router-define.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-define.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
external: ['@kevisual/router'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/router-simple.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-simple.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
browser: false,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/router-simple.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-simple.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/router-simple-lib.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-simple-lib.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
browser: false,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript(),
|
||||||
|
],
|
||||||
|
external: ['xml2js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/router-simple-lib.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/router-simple-lib.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
5
src/app-browser.ts
Normal file
5
src/app-browser.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { QueryRouterServer } from './browser.ts';
|
||||||
|
|
||||||
|
export const App = QueryRouterServer;
|
||||||
|
|
||||||
|
export * from './browser.ts';
|
||||||
75
src/app.ts
75
src/app.ts
@@ -1,22 +1,27 @@
|
|||||||
import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
||||||
import { Server, Cors } from './server/server.ts';
|
import { Server, ServerOpts, HandleCtx } from './server/server.ts';
|
||||||
import { WsServer } from './server/ws-server.ts';
|
import { WsServer } from './server/ws-server.ts';
|
||||||
type RouterHandle = (msg: { path: string; [key: string]: any }) => { code: string; data?: any; message?: string; [key: string]: any };
|
import { CustomError } from './result/error.ts';
|
||||||
|
import { handleServer } from './server/handle-server.ts';
|
||||||
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
|
||||||
|
type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any };
|
||||||
type AppOptions<T = {}> = {
|
type AppOptions<T = {}> = {
|
||||||
router?: QueryRouter;
|
router?: QueryRouter;
|
||||||
server?: Server;
|
server?: Server;
|
||||||
/** handle msg 关联 */
|
/** handle msg 关联 */
|
||||||
routerHandle?: RouterHandle;
|
routerHandle?: RouterHandle;
|
||||||
routerContext?: RouteContext<T>;
|
routerContext?: RouteContext<T>;
|
||||||
serverOptions?: {
|
serverOptions?: ServerOpts;
|
||||||
path?: string;
|
|
||||||
cors?: Cors;
|
|
||||||
handle?: any;
|
|
||||||
};
|
|
||||||
io?: boolean;
|
io?: boolean;
|
||||||
ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string };
|
ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string };
|
||||||
};
|
};
|
||||||
export class App<T = {}> {
|
export type AppReqRes = HandleCtx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理
|
||||||
|
*/
|
||||||
|
export class App<T = {}, U = AppReqRes> {
|
||||||
router: QueryRouter;
|
router: QueryRouter;
|
||||||
server: Server;
|
server: Server;
|
||||||
io: WsServer;
|
io: WsServer;
|
||||||
@@ -24,7 +29,7 @@ export class App<T = {}> {
|
|||||||
const router = opts?.router || new QueryRouter();
|
const router = opts?.router || new QueryRouter();
|
||||||
const server = opts?.server || new Server(opts?.serverOptions || {});
|
const server = opts?.server || new Server(opts?.serverOptions || {});
|
||||||
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
|
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
|
||||||
|
router.setContext({ needSerialize: true, ...opts?.routerContext });
|
||||||
this.router = router;
|
this.router = router;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
if (opts?.io) {
|
if (opts?.io) {
|
||||||
@@ -57,10 +62,10 @@ export class App<T = {}> {
|
|||||||
add = this.addRoute;
|
add = this.addRoute;
|
||||||
|
|
||||||
Route = Route;
|
Route = Route;
|
||||||
route(opts: RouteOpts): Route;
|
route(opts: RouteOpts): Route<U>;
|
||||||
route(path: string, key?: string): Route;
|
route(path: string, key?: string): Route<U>;
|
||||||
route(path: string, opts?: RouteOpts): Route;
|
route(path: string, opts?: RouteOpts): Route<U>;
|
||||||
route(path: string, key?: string, opts?: RouteOpts): Route;
|
route(path: string, key?: string, opts?: RouteOpts): Route<U>;
|
||||||
route(...args: any[]) {
|
route(...args: any[]) {
|
||||||
const [path, key, opts] = args;
|
const [path, key, opts] = args;
|
||||||
if (typeof path === 'object') {
|
if (typeof path === 'object') {
|
||||||
@@ -77,8 +82,48 @@ export class App<T = {}> {
|
|||||||
}
|
}
|
||||||
return new Route(path, key, opts);
|
return new Route(path, key, opts);
|
||||||
}
|
}
|
||||||
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
prompt(description: string): Route<Required<RouteContext>>;
|
||||||
|
prompt(description: Function): Route<Required<RouteContext>>;
|
||||||
|
prompt(...args: any[]) {
|
||||||
|
const [desc] = args;
|
||||||
|
let description = ''
|
||||||
|
if (typeof desc === 'string') {
|
||||||
|
description = desc;
|
||||||
|
} else if (typeof desc === 'function') {
|
||||||
|
description = desc() || ''; // 如果是Promise,需要addTo App之前就要获取应有的函数了。
|
||||||
|
}
|
||||||
|
return new Route('', '', { description });
|
||||||
|
}
|
||||||
|
|
||||||
|
async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
const router = this.router;
|
const router = this.router;
|
||||||
return await router.parse(message, ctx);
|
return await router.call(message, ctx);
|
||||||
|
}
|
||||||
|
async queryRoute(path: string, key?: string, payload?: any, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
|
return await this.router.queryRoute({ path, key, payload }, ctx);
|
||||||
|
}
|
||||||
|
exportRoutes() {
|
||||||
|
return this.router.exportRoutes();
|
||||||
|
}
|
||||||
|
importRoutes(routes: any[]) {
|
||||||
|
this.router.importRoutes(routes);
|
||||||
|
}
|
||||||
|
importApp(app: App) {
|
||||||
|
this.importRoutes(app.exportRoutes());
|
||||||
|
}
|
||||||
|
throw(code?: number | string, message?: string, tips?: string): void;
|
||||||
|
throw(...args: any[]) {
|
||||||
|
throw new CustomError(...args);
|
||||||
|
}
|
||||||
|
static handleRequest(req: IncomingMessage, res: ServerResponse) {
|
||||||
|
return handleServer(req, res);
|
||||||
|
}
|
||||||
|
onServerRequest(fn: (req: IncomingMessage, res: ServerResponse) => void) {
|
||||||
|
if (!this.server) {
|
||||||
|
throw new Error('Server is not initialized');
|
||||||
|
}
|
||||||
|
this.server.on(fn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from './browser.ts';
|
||||||
|
|||||||
164
src/auto/call-sock.ts
Normal file
164
src/auto/call-sock.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { createConnection } from 'node:net';
|
||||||
|
|
||||||
|
type QueryData = {
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
payload?: any;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallSockOptions = {
|
||||||
|
socketPath?: string;
|
||||||
|
timeout?: number;
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const callSock = async (data: QueryData, options: CallSockOptions = {}): Promise<any> => {
|
||||||
|
const { socketPath = './app.sock', timeout = 10000, method = 'POST' } = options;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = createConnection(socketPath);
|
||||||
|
let responseData = '';
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
if (timeout > 0) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error(`Socket call timeout after ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
try {
|
||||||
|
let request: string;
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
// GET 请求:参数放在 URL 中
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (key === 'payload' && typeof value === 'object') {
|
||||||
|
searchParams.append(key, JSON.stringify(value));
|
||||||
|
} else {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
const url = queryString ? `/?${queryString}` : '/';
|
||||||
|
|
||||||
|
request = [`GET ${url} HTTP/1.1`, 'Host: localhost', 'Connection: close', '', ''].join('\r\n');
|
||||||
|
} else {
|
||||||
|
// POST 请求:数据放在 body 中
|
||||||
|
const body = JSON.stringify(data);
|
||||||
|
const contentLength = Buffer.byteLength(body, 'utf8');
|
||||||
|
|
||||||
|
request = [
|
||||||
|
'POST / HTTP/1.1',
|
||||||
|
'Host: localhost',
|
||||||
|
'Content-Type: application/json',
|
||||||
|
`Content-Length: ${contentLength}`,
|
||||||
|
'Connection: close',
|
||||||
|
'',
|
||||||
|
body,
|
||||||
|
].join('\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
client.write(request);
|
||||||
|
} catch (error) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
client.destroy();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('data', (chunk) => {
|
||||||
|
responseData += chunk.toString();
|
||||||
|
|
||||||
|
// 检查是否收到完整的HTTP响应
|
||||||
|
if (responseData.includes('\r\n\r\n')) {
|
||||||
|
const [headerSection] = responseData.split('\r\n\r\n');
|
||||||
|
const contentLengthMatch = headerSection.match(/content-length:\s*(\d+)/i);
|
||||||
|
|
||||||
|
if (contentLengthMatch) {
|
||||||
|
const expectedLength = parseInt(contentLengthMatch[1]);
|
||||||
|
const bodyStart = responseData.indexOf('\r\n\r\n') + 4;
|
||||||
|
const currentBodyLength = Buffer.byteLength(responseData.slice(bodyStart), 'utf8');
|
||||||
|
|
||||||
|
// 如果收到了完整的响应,主动关闭连接
|
||||||
|
if (currentBodyLength >= expectedLength) {
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
} else if (responseData.includes('\r\n0\r\n\r\n')) {
|
||||||
|
// 检查 chunked 编码结束标记
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('end', () => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析 HTTP 响应
|
||||||
|
const response = parseHttpResponse(responseData);
|
||||||
|
|
||||||
|
if (response.statusCode >= 400) {
|
||||||
|
reject(new Error(`HTTP ${response.statusCode}: ${response.body}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析 JSON 响应
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(response.body);
|
||||||
|
resolve(result);
|
||||||
|
} catch {
|
||||||
|
// 如果不是 JSON,直接返回文本
|
||||||
|
resolve(response.body);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (error) => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('timeout', () => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error('Socket connection timeout'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析 HTTP 响应的辅助函数
|
||||||
|
function parseHttpResponse(responseData: string) {
|
||||||
|
const [headerSection, ...bodyParts] = responseData.split('\r\n\r\n');
|
||||||
|
const body = bodyParts.join('\r\n\r\n');
|
||||||
|
|
||||||
|
const lines = headerSection.split('\r\n');
|
||||||
|
const statusLine = lines[0];
|
||||||
|
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
|
||||||
|
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const [key, ...valueParts] = lines[i].split(':');
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
headers[key.trim().toLowerCase()] = valueParts.join(':').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
headers,
|
||||||
|
body: body || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const autoCall = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
|
||||||
|
return callSock(data, { ...options, method: 'POST' });
|
||||||
|
};
|
||||||
274
src/auto/listen-sock.ts
Normal file
274
src/auto/listen-sock.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import type { IncomingMessage } from 'http';
|
||||||
|
import { QueryRouterServer } from '../route.ts';
|
||||||
|
import { getRuntime } from './runtime.ts';
|
||||||
|
import { runFirstCheck } from './listen/run-check.ts';
|
||||||
|
import { cleanup } from './listen/cleanup.ts';
|
||||||
|
import { ServerTimer } from './listen/server-time.ts';
|
||||||
|
|
||||||
|
type ListenSocketOptions = {
|
||||||
|
/**
|
||||||
|
* Unix socket path, defaults to './app.sock'
|
||||||
|
*/
|
||||||
|
path?: string;
|
||||||
|
app?: QueryRouterServer;
|
||||||
|
/**
|
||||||
|
* Unix socket path, defaults to './app.pid'
|
||||||
|
*/
|
||||||
|
pidPath?: string;
|
||||||
|
/**
|
||||||
|
* Timeout for the server, defaults to 15 minutes.
|
||||||
|
* If the server is not responsive for this duration, it will be terminated
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = async (req, app: QueryRouterServer) => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
let data;
|
||||||
|
if (!runtime.isNode) {
|
||||||
|
data = await getRequestParams(req);
|
||||||
|
} else {
|
||||||
|
data = await parseBody(req);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const serverTimer = app.serverTimer;
|
||||||
|
if (serverTimer) {
|
||||||
|
serverTimer?.run?.();
|
||||||
|
}
|
||||||
|
const result = await app.queryRoute(data as any);
|
||||||
|
const response = new Response(JSON.stringify(result));
|
||||||
|
response.headers.set('Content-Type', 'application/json');
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
export const closeListenSocket = () => {
|
||||||
|
console.log('Closing listen socket');
|
||||||
|
process.emit('SIGINT');
|
||||||
|
};
|
||||||
|
export const serverTimer = new ServerTimer();
|
||||||
|
export const listenSocket = async (options?: ListenSocketOptions) => {
|
||||||
|
const path = options?.path || './app.sock';
|
||||||
|
const pidPath = options?.pidPath || './app.pid';
|
||||||
|
const timeout = options?.timeout || 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
const runtime = getRuntime();
|
||||||
|
|
||||||
|
serverTimer.timeout = timeout;
|
||||||
|
serverTimer.startTimer();
|
||||||
|
serverTimer.onTimeout = closeListenSocket;
|
||||||
|
|
||||||
|
let app = options?.app || globalThis.context?.app;
|
||||||
|
if (!app) {
|
||||||
|
app = new QueryRouterServer();
|
||||||
|
}
|
||||||
|
app.serverTimer = serverTimer;
|
||||||
|
await runFirstCheck(path, pidPath);
|
||||||
|
let close = async () => {};
|
||||||
|
cleanup({ path, close });
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// 检查 Deno 版本是否支持 Unix domain socket
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const listener = Deno.listen({
|
||||||
|
transport: 'unix',
|
||||||
|
path: path,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理连接
|
||||||
|
(async () => {
|
||||||
|
for await (const conn of listener) {
|
||||||
|
(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const httpConn = Deno.serveHttp(conn);
|
||||||
|
for await (const requestEvent of httpConn) {
|
||||||
|
try {
|
||||||
|
const response = await server(requestEvent.request, app);
|
||||||
|
await requestEvent.respondWith(response);
|
||||||
|
} catch (error) {
|
||||||
|
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
close = async () => {
|
||||||
|
listener.close();
|
||||||
|
};
|
||||||
|
return listener;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果 Unix socket 不支持,回退到 HTTP 服务器
|
||||||
|
console.warn('Unix socket not supported in this Deno environment, falling back to HTTP server');
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const listener = Deno.listen({ port: 0 }); // 使用随机端口
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
console.log(`Deno server listening on port ${listener.addr.port}`);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for await (const conn of listener) {
|
||||||
|
(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const httpConn = Deno.serveHttp(conn);
|
||||||
|
for await (const requestEvent of httpConn) {
|
||||||
|
try {
|
||||||
|
const response = await server(requestEvent.request, app);
|
||||||
|
await requestEvent.respondWith(response);
|
||||||
|
} catch (error) {
|
||||||
|
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtime.isBun) {
|
||||||
|
// @ts-ignore
|
||||||
|
const bunServer = Bun.serve({
|
||||||
|
unix: path,
|
||||||
|
fetch(req) {
|
||||||
|
return server(req, app);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
close = async () => {
|
||||||
|
await bunServer.stop();
|
||||||
|
};
|
||||||
|
return bunServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js 环境
|
||||||
|
const http = await import('http');
|
||||||
|
|
||||||
|
const httpServer = http.createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await server(req, app);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
res.setHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置状态码
|
||||||
|
res.statusCode = response.status;
|
||||||
|
|
||||||
|
// 读取响应体并写入
|
||||||
|
const body = await response.text();
|
||||||
|
res.end(body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling request:', error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(path);
|
||||||
|
close = async () => {
|
||||||
|
httpServer.close();
|
||||||
|
};
|
||||||
|
return httpServer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestParams = async (req: Request) => {
|
||||||
|
let urlParams: Record<string, any> = {};
|
||||||
|
let bodyParams: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 获取URL参数
|
||||||
|
const url = new URL(req.url);
|
||||||
|
for (const [key, value] of url.searchParams.entries()) {
|
||||||
|
// 尝试解析JSON payload
|
||||||
|
if (key === 'payload') {
|
||||||
|
try {
|
||||||
|
urlParams[key] = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
urlParams[key] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urlParams[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取body参数
|
||||||
|
if (req.method.toLowerCase() === 'post' && req.body) {
|
||||||
|
const contentType = req.headers.get('content-type') || '';
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
try {
|
||||||
|
bodyParams = await req.json();
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,保持空对象
|
||||||
|
}
|
||||||
|
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = await req.text();
|
||||||
|
const params = new URLSearchParams(formData);
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
bodyParams[key] = value;
|
||||||
|
}
|
||||||
|
} else if (contentType.includes('multipart/form-data')) {
|
||||||
|
try {
|
||||||
|
const formData = await req.formData();
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
// @ts-ignore
|
||||||
|
bodyParams[key] = value instanceof File ? value : value.toString();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,保持空对象
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// body参数优先,合并数据
|
||||||
|
return {
|
||||||
|
...urlParams,
|
||||||
|
...bodyParams,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
arr.push(chunk);
|
||||||
|
});
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const body = Buffer.concat(arr).toString();
|
||||||
|
|
||||||
|
// 获取 Content-Type 头信息
|
||||||
|
const contentType = req.headers['content-type'] || '';
|
||||||
|
|
||||||
|
// 处理 application/json
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
resolve(JSON.parse(body) as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 处理 application/x-www-form-urlencoded
|
||||||
|
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = new URLSearchParams(body);
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
||||||
|
try {
|
||||||
|
result[key] = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(result as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认尝试 JSON 解析
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(body) as T);
|
||||||
|
} catch {
|
||||||
|
resolve({} as T);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({} as T);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
102
src/auto/listen/cleanup.ts
Normal file
102
src/auto/listen/cleanup.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { getRuntime } from '../runtime.ts';
|
||||||
|
|
||||||
|
let isClean = false;
|
||||||
|
export const deleteFileDetached = async (path: string, pidPath: string = './app.pid') => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// Deno 实现 - 启动后不等待结果
|
||||||
|
const process = new Deno.Command('sh', {
|
||||||
|
args: ['-c', `rm -f "${path}" & rm -f "${pidPath}"`],
|
||||||
|
stdout: 'null',
|
||||||
|
stderr: 'null',
|
||||||
|
});
|
||||||
|
process.spawn(); // 不等待结果
|
||||||
|
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { spawn } = await import('node:child_process');
|
||||||
|
const child = spawn('sh', ['-c', `rm -f "${path}" & rm -f "${pidPath}"`], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
child.unref(); // 完全分离
|
||||||
|
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CleanupOptions = {
|
||||||
|
path: string;
|
||||||
|
close?: () => Promise<void>;
|
||||||
|
pidPath?: string;
|
||||||
|
};
|
||||||
|
export const cleanup = async ({ path, close = async () => {}, pidPath = './app.pid' }: CleanupOptions) => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
|
||||||
|
// 检查文件是否存在并删除
|
||||||
|
const cleanupFile = async () => {
|
||||||
|
if (isClean) return;
|
||||||
|
isClean = true;
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
await deleteFileDetached(path, pidPath);
|
||||||
|
}
|
||||||
|
await close();
|
||||||
|
if (!runtime.isDeno) {
|
||||||
|
await deleteFileDetached(path, pidPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据运行时环境注册不同的退出监听器
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// Deno 环境
|
||||||
|
const handleSignal = () => {
|
||||||
|
cleanupFile();
|
||||||
|
Deno.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.addSignalListener('SIGINT', handleSignal);
|
||||||
|
Deno.addSignalListener('SIGTERM', handleSignal);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[DEBUG] Failed to add signal listeners:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于 beforeunload 和 unload,使用异步清理
|
||||||
|
const handleUnload = () => {
|
||||||
|
cleanupFile();
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.addEventListener('beforeunload', handleUnload);
|
||||||
|
globalThis.addEventListener('unload', handleUnload);
|
||||||
|
} else if (runtime.isNode || runtime.isBun) {
|
||||||
|
// Node.js 和 Bun 环境
|
||||||
|
import('process').then(({ default: process }) => {
|
||||||
|
// 信号处理使用同步清理,然后退出
|
||||||
|
const signalHandler = async (signal: string) => {
|
||||||
|
await cleanupFile();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', () => signalHandler('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => signalHandler('SIGTERM'));
|
||||||
|
process.on('SIGUSR1', () => signalHandler('SIGUSR1'));
|
||||||
|
process.on('SIGUSR2', () => signalHandler('SIGUSR2'));
|
||||||
|
|
||||||
|
process.on('exit', async () => {
|
||||||
|
await cleanupFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', async (error) => {
|
||||||
|
console.error('Uncaught Exception:', error);
|
||||||
|
await cleanupFile();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', async (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
await cleanupFile();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回手动清理函数,以便需要时主动调用
|
||||||
|
return cleanupFile;
|
||||||
|
};
|
||||||
51
src/auto/listen/run-check.ts
Normal file
51
src/auto/listen/run-check.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { getRuntime } from '../runtime.ts';
|
||||||
|
|
||||||
|
export const getPid = async () => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
|
||||||
|
let pid = 0;
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// @ts-ignore
|
||||||
|
pid = Deno.pid;
|
||||||
|
} else {
|
||||||
|
pid = process.pid;
|
||||||
|
}
|
||||||
|
return pid;
|
||||||
|
};
|
||||||
|
export const writeAppid = async (pidPath = './app.pid') => {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
const pid = await getPid();
|
||||||
|
fs.writeFileSync(pidPath, pid + '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPidFromFileAndStop = async () => {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
if (fs.existsSync('./app.pid')) {
|
||||||
|
const pid = parseInt(fs.readFileSync('./app.pid', 'utf-8'), 10);
|
||||||
|
if (!isNaN(pid)) {
|
||||||
|
if (pid === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
process.kill(pid);
|
||||||
|
console.log(`Stopped process with PID ${pid}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to stop process with PID ${pid}:`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runFirstCheck = async (path: string, pidPath: string) => {
|
||||||
|
await getPidFromFileAndStop();
|
||||||
|
await writeAppid(pidPath);
|
||||||
|
try {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
fs.unlinkSync(path);
|
||||||
|
console.log(`Socket file ${path} cleaned up during first check`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to clean up socket file ${path} during first check:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
33
src/auto/listen/server-time.ts
Normal file
33
src/auto/listen/server-time.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export class ServerTimer {
|
||||||
|
updatedAt: number;
|
||||||
|
timer: any;
|
||||||
|
timeout: number;
|
||||||
|
onTimeout: any;
|
||||||
|
interval = 10 * 1000;
|
||||||
|
constructor(opts?: { timeout?: number }) {
|
||||||
|
this.timeout = opts?.timeout || 15 * 60 * 1000;
|
||||||
|
this.run();
|
||||||
|
}
|
||||||
|
startTimer() {
|
||||||
|
const that = this;
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
const updatedAt = Date.now();
|
||||||
|
const timeout = that.timeout;
|
||||||
|
const onTimeout = that.onTimeout;
|
||||||
|
const isExpired = updatedAt - that.updatedAt > timeout;
|
||||||
|
if (isExpired) {
|
||||||
|
onTimeout?.();
|
||||||
|
clearInterval(that.timer);
|
||||||
|
that.timer = null;
|
||||||
|
}
|
||||||
|
}, that.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(): number {
|
||||||
|
this.updatedAt = Date.now();
|
||||||
|
return this.updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/auto/load-ts.ts
Normal file
38
src/auto/load-ts.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { getRuntime } from './runtime.ts';
|
||||||
|
import { glob } from './utils/glob.ts';
|
||||||
|
type GlobOptions = {
|
||||||
|
cwd?: string;
|
||||||
|
load?: (args?: any) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}): Promise<string[]> => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
if (runtime.isNode) {
|
||||||
|
console.error(`Node.js is not supported`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// Deno 环境下
|
||||||
|
return await glob(match);
|
||||||
|
}
|
||||||
|
if (runtime.isBun) {
|
||||||
|
// Bun 环境下
|
||||||
|
// @ts-ignore
|
||||||
|
const { Glob } = await import('bun');
|
||||||
|
const path = await import('path');
|
||||||
|
// @ts-ignore
|
||||||
|
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
|
||||||
|
const files: string[] = [];
|
||||||
|
for await (const file of glob.scan('.')) {
|
||||||
|
files.push(path.join(cwd, file));
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return Array.from(files);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTS = async (match: string = './*.ts', { cwd = process.cwd(), load }: GlobOptions = {}): Promise<any[]> => {
|
||||||
|
const files = await getMatchFiles(match, { cwd });
|
||||||
|
return Promise.all(files.map((file) => (load ? load(file) : import(file))));
|
||||||
|
};
|
||||||
19
src/auto/runtime.ts
Normal file
19
src/auto/runtime.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
type RuntimeEngine = 'node' | 'deno' | 'bun';
|
||||||
|
|
||||||
|
type Runtime = {
|
||||||
|
isNode?: boolean;
|
||||||
|
isDeno?: boolean;
|
||||||
|
isBun?: boolean;
|
||||||
|
engine: RuntimeEngine;
|
||||||
|
};
|
||||||
|
export const getRuntime = (): Runtime => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof Deno !== 'undefined') {
|
||||||
|
return { isDeno: true, engine: 'deno' };
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof Bun !== 'undefined') {
|
||||||
|
return { isBun: true, engine: 'bun' };
|
||||||
|
}
|
||||||
|
return { isNode: true, engine: 'node' };
|
||||||
|
};
|
||||||
83
src/auto/utils/glob.ts
Normal file
83
src/auto/utils/glob.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
type GlobOptions = {
|
||||||
|
cwd?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const glob = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}) => {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
const path = await import('node:path');
|
||||||
|
|
||||||
|
// 将 glob 模式转换为正则表达式
|
||||||
|
const globToRegex = (pattern: string): RegExp => {
|
||||||
|
const escaped = pattern
|
||||||
|
.replace(/\./g, '\\.')
|
||||||
|
.replace(/\*\*/g, '__DOUBLE_STAR__') // 临时替换 **
|
||||||
|
.replace(/\*/g, '[^/]*') // * 匹配除 / 外的任意字符
|
||||||
|
.replace(/__DOUBLE_STAR__/g, '.*') // ** 匹配任意字符包括 /
|
||||||
|
.replace(/\?/g, '[^/]'); // ? 匹配除 / 外的单个字符
|
||||||
|
return new RegExp(`^${escaped}$`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归读取目录
|
||||||
|
const readDirRecursive = async (dir: string): Promise<string[]> => {
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isFile()) {
|
||||||
|
files.push(fullPath);
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
// 递归搜索子目录
|
||||||
|
const subFiles = await readDirRecursive(fullPath);
|
||||||
|
files.push(...subFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略无法访问的目录
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析模式是否包含递归搜索
|
||||||
|
const hasRecursive = match.includes('**');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let allFiles: string[] = [];
|
||||||
|
|
||||||
|
if (hasRecursive) {
|
||||||
|
// 处理递归模式
|
||||||
|
const basePath = match.split('**')[0];
|
||||||
|
const startDir = path.resolve(cwd, basePath || '.');
|
||||||
|
allFiles = await readDirRecursive(startDir);
|
||||||
|
} else {
|
||||||
|
// 处理非递归模式
|
||||||
|
const dir = path.resolve(cwd, path.dirname(match));
|
||||||
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile()) {
|
||||||
|
allFiles.push(path.join(dir, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建相对于 cwd 的匹配模式
|
||||||
|
const normalizedMatch = path.resolve(cwd, match);
|
||||||
|
const regex = globToRegex(normalizedMatch);
|
||||||
|
|
||||||
|
// 过滤匹配的文件
|
||||||
|
const matchedFiles = allFiles.filter(file => {
|
||||||
|
const normalizedFile = path.resolve(file);
|
||||||
|
return regex.test(normalizedFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchedFiles;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in glob pattern "${match}":`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
15
src/browser.ts
Normal file
15
src/browser.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
|
||||||
|
|
||||||
|
export type { Rule, Schema } from './validator/index.ts';
|
||||||
|
|
||||||
|
export { createSchema } from './validator/index.ts';
|
||||||
|
|
||||||
|
export type { RouteContext, RouteOpts } from './route.ts';
|
||||||
|
|
||||||
|
export type { Run } from './route.ts';
|
||||||
|
|
||||||
|
export { CustomError } from './result/error.ts';
|
||||||
|
|
||||||
|
export * from './server/parse-body.ts';
|
||||||
|
|
||||||
|
export * from './router-define.ts';
|
||||||
40
src/chat.ts
Normal file
40
src/chat.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { QueryRouter } from "./route.ts";
|
||||||
|
|
||||||
|
type RouterChatOptions = {
|
||||||
|
router?: QueryRouter;
|
||||||
|
}
|
||||||
|
export class RouterChat {
|
||||||
|
router: QueryRouter;
|
||||||
|
prompt: string = '';
|
||||||
|
constructor(opts?: RouterChatOptions) {
|
||||||
|
this.router = opts?.router || new QueryRouter();
|
||||||
|
}
|
||||||
|
prefix(wrapperFn?: (routes: any[]) => string) {
|
||||||
|
if (this.prompt) {
|
||||||
|
return this.prompt;
|
||||||
|
}
|
||||||
|
let _prompt = `你是一个调用函数工具的助手,当用户询问时,如果拥有工具,请返回 JSON 数据,数据的值的内容是 id 和 payload 。如果有参数,请放到 payload 当中。
|
||||||
|
|
||||||
|
下面是你可以使用的工具列表:
|
||||||
|
|
||||||
|
`;
|
||||||
|
if (!wrapperFn) {
|
||||||
|
_prompt += this.router.routes.map(r => `工具名称: ${r.id}\n描述: ${r.description}\n`).join('\n');
|
||||||
|
} else {
|
||||||
|
_prompt += wrapperFn(this.router.exportRoutes());
|
||||||
|
}
|
||||||
|
_prompt += `当你需要使用工具时,请严格按照以下格式返回:
|
||||||
|
{
|
||||||
|
"id": "工具名称",
|
||||||
|
"payload": {
|
||||||
|
// 参数列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
如果你不需要使用工具,直接返回用户想要的内容即可,不要返回任何多余的信息。`;
|
||||||
|
return _prompt;
|
||||||
|
}
|
||||||
|
chat() {
|
||||||
|
const prompt = this.prefix();
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/index.ts
13
src/index.ts
@@ -1,7 +1,7 @@
|
|||||||
export { Route, QueryRouter, QueryRouterServer } from './route.ts';
|
export { Route, QueryRouter, QueryRouterServer, Mini } from './route.ts';
|
||||||
export { Connect, QueryConnect } from './connect.ts';
|
export { Connect, QueryConnect } from './connect.ts';
|
||||||
|
|
||||||
export type { RouteContext, RouteOpts } from './route.ts';
|
export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
|
||||||
|
|
||||||
export type { Run } from './route.ts';
|
export type { Run } from './route.ts';
|
||||||
|
|
||||||
@@ -11,11 +11,10 @@ export { Server, handleServer } from './server/index.ts';
|
|||||||
*/
|
*/
|
||||||
export { CustomError } from './result/error.ts';
|
export { CustomError } from './result/error.ts';
|
||||||
|
|
||||||
/**
|
export { createSchema } from './validator/index.ts';
|
||||||
* 返回结果
|
|
||||||
*/
|
|
||||||
export { Result } from './result/index.ts';
|
|
||||||
|
|
||||||
export { Rule, Schema, createSchema } from './validator/index.ts';
|
export type { Rule, Schema, } from './validator/index.ts';
|
||||||
|
|
||||||
export { App } from './app.ts';
|
export { App } from './app.ts';
|
||||||
|
|
||||||
|
export * from './router-define.ts';
|
||||||
|
|||||||
@@ -35,10 +35,22 @@ export class CustomError extends Error {
|
|||||||
tips: e?.tips,
|
tips: e?.tips,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 判断 throw 的错误是否不是当前这个错误
|
||||||
|
* @param err
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static isError(err: any) {
|
||||||
|
if (err instanceof CustomError || err?.code) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
parse(e?: CustomError) {
|
parse(e?: CustomError) {
|
||||||
if (e) {
|
if (e) {
|
||||||
return CustomError.parseError(e);
|
return CustomError.parseError(e);
|
||||||
} else {
|
} else {
|
||||||
|
const e = this;
|
||||||
return {
|
return {
|
||||||
code: e?.code,
|
code: e?.code,
|
||||||
data: e?.data,
|
data: e?.data,
|
||||||
|
|||||||
@@ -1,45 +1 @@
|
|||||||
export const Code400 = [
|
export * from './error.ts';
|
||||||
{
|
|
||||||
code: 400,
|
|
||||||
msg: 'Bad Request',
|
|
||||||
zn: '表示其他错误,就是4xx都无法描述的前端发生的错误',
|
|
||||||
},
|
|
||||||
{ code: 401, msg: 'Authentication', zn: '表示认证类型的错误' }, // token 无效 (无token, token无效, token 过期)
|
|
||||||
{
|
|
||||||
code: 403,
|
|
||||||
msg: 'Authorization',
|
|
||||||
zn: '表示授权的错误(认证和授权的区别在于:认证表示“识别前来访问的是谁”,而授权则是“赋予特定用户执行特定操作的权限”)',
|
|
||||||
},
|
|
||||||
{ code: 404, msg: 'Not Found', zn: '表示访问的数据不存在' },
|
|
||||||
{
|
|
||||||
code: 405,
|
|
||||||
msg: 'Method Not Allowd',
|
|
||||||
zn: '表示可以访问接口,但是使用的HTTP方法不允许',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ResultCode = [{ code: 200, msg: 'OK', zn: '请求成功。' }].concat(Code400);
|
|
||||||
type ResultProps = {
|
|
||||||
code?: number;
|
|
||||||
msg?: string;
|
|
||||||
userTip?: string;
|
|
||||||
};
|
|
||||||
export const Result = ({ code, msg, userTip, ...other }: ResultProps) => {
|
|
||||||
const Code = ResultCode.find((item) => item.code === code);
|
|
||||||
let _result = {
|
|
||||||
code: code || Code?.code,
|
|
||||||
msg: msg || Code?.msg,
|
|
||||||
userTip: undefined,
|
|
||||||
...other,
|
|
||||||
};
|
|
||||||
if (userTip) {
|
|
||||||
_result.userTip = userTip;
|
|
||||||
}
|
|
||||||
return _result;
|
|
||||||
};
|
|
||||||
Result.success = (data?: any) => {
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
384
src/route.ts
384
src/route.ts
@@ -1,10 +1,11 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid, random } from 'nanoid';
|
||||||
import { CustomError } from './result/error.ts';
|
import { CustomError } from './result/error.ts';
|
||||||
import { Schema, Rule, createSchema } from './validator/index.ts';
|
import { Schema, Rule, createSchema } from './validator/index.ts';
|
||||||
import { pick } from './utils/pick.ts';
|
import { pick } from './utils/pick.ts';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
|
import { listenProcess } from './utils/listen-process.ts';
|
||||||
|
|
||||||
export type RouterContextT = { code?: number; [key: string]: any };
|
export type RouterContextT = { code?: number;[key: string]: any };
|
||||||
export type RouteContext<T = { code?: number }, S = any> = {
|
export type RouteContext<T = { code?: number }, S = any> = {
|
||||||
// run first
|
// run first
|
||||||
query?: { [key: string]: any };
|
query?: { [key: string]: any };
|
||||||
@@ -18,25 +19,56 @@ export type RouteContext<T = { code?: number }, S = any> = {
|
|||||||
// 传递状态
|
// 传递状态
|
||||||
state?: S;
|
state?: S;
|
||||||
// transfer data
|
// transfer data
|
||||||
|
/**
|
||||||
|
* 当前路径
|
||||||
|
*/
|
||||||
currentPath?: string;
|
currentPath?: string;
|
||||||
|
/**
|
||||||
|
* 当前key
|
||||||
|
*/
|
||||||
currentKey?: string;
|
currentKey?: string;
|
||||||
|
/**
|
||||||
|
* 当前route
|
||||||
|
*/
|
||||||
currentRoute?: Route;
|
currentRoute?: Route;
|
||||||
progress?: [[string, string]][];
|
/**
|
||||||
|
* 进度
|
||||||
|
*/
|
||||||
|
progress?: [string, string][];
|
||||||
// onlyForNextRoute will be clear after next route
|
// onlyForNextRoute will be clear after next route
|
||||||
nextQuery?: { [key: string]: any };
|
nextQuery?: { [key: string]: any };
|
||||||
// end
|
// end
|
||||||
end?: boolean;
|
end?: boolean;
|
||||||
// 处理router manager
|
// 处理router manager
|
||||||
// TODO:
|
// TODO:
|
||||||
|
/**
|
||||||
|
* 请求 route的返回结果,包函ctx
|
||||||
|
*/
|
||||||
queryRouter?: QueryRouter;
|
queryRouter?: QueryRouter;
|
||||||
error?: any;
|
error?: any;
|
||||||
call?: (message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
|
/** 请求 route的返回结果,包函ctx */
|
||||||
|
call?: (
|
||||||
|
message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any },
|
||||||
|
ctx?: RouteContext & { [key: string]: any },
|
||||||
|
) => Promise<any>;
|
||||||
|
/** 请求 route的返回结果,不包函ctx */
|
||||||
|
queryRoute?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
throw?: (code?: number | string, message?: string, tips?: string) => void;
|
||||||
|
/** 是否需要序列化, 使用JSON.stringify和JSON.parse */
|
||||||
|
needSerialize?: boolean;
|
||||||
} & T;
|
} & T;
|
||||||
|
export type SimpleObject = Record<string, any>;
|
||||||
export type Run<T = any> = (ctx?: RouteContext<T>) => Promise<typeof ctx | null | void>;
|
export type Run<T extends SimpleObject = {}> = (ctx: RouteContext<T>) => Promise<typeof ctx | null | void>;
|
||||||
|
|
||||||
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
|
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
|
||||||
|
export type RouteMiddleware =
|
||||||
|
| {
|
||||||
|
path: string;
|
||||||
|
key?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
| string;
|
||||||
export type RouteOpts = {
|
export type RouteOpts = {
|
||||||
path?: string;
|
path?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
@@ -44,7 +76,8 @@ export type RouteOpts = {
|
|||||||
run?: Run;
|
run?: Run;
|
||||||
nextRoute?: NextRoute; // route to run after this route
|
nextRoute?: NextRoute; // route to run after this route
|
||||||
description?: string;
|
description?: string;
|
||||||
middleware?: Route[] | string[]; // middleware
|
metadata?: { [key: string]: any };
|
||||||
|
middleware?: RouteMiddleware[]; // middleware
|
||||||
type?: 'route' | 'middleware';
|
type?: 'route' | 'middleware';
|
||||||
/**
|
/**
|
||||||
* validator: {
|
* validator: {
|
||||||
@@ -55,32 +88,55 @@ export type RouteOpts = {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
validator?: { [key: string]: Rule };
|
validator?: { [key: string]: Rule };
|
||||||
schema?: { [key: string]: Schema<any> };
|
schema?: { [key: string]: any };
|
||||||
isVerify?: boolean;
|
isVerify?: boolean;
|
||||||
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
|
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
|
||||||
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
|
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
|
||||||
|
/**
|
||||||
|
* $#$ will be used to split path and key
|
||||||
|
*/
|
||||||
idUsePath?: boolean;
|
idUsePath?: boolean;
|
||||||
|
/**
|
||||||
|
* id 合并的分隔符,默认为 $#$
|
||||||
|
*/
|
||||||
|
delimiter?: string;
|
||||||
isDebug?: boolean;
|
isDebug?: boolean;
|
||||||
};
|
};
|
||||||
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>;
|
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>;
|
||||||
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware'] as const;
|
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware', 'metadata'] as const;
|
||||||
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
|
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
|
||||||
export class Route {
|
export class Route<U = { [key: string]: any }> {
|
||||||
|
/**
|
||||||
|
* 一级路径
|
||||||
|
*/
|
||||||
path?: string;
|
path?: string;
|
||||||
|
/**
|
||||||
|
* 二级路径
|
||||||
|
*/
|
||||||
key?: string;
|
key?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
share? = false;
|
share? = false;
|
||||||
run?: Run;
|
run?: Run;
|
||||||
nextRoute?: NextRoute; // route to run after this route
|
nextRoute?: NextRoute; // route to run after this route
|
||||||
description?: string;
|
description?: string;
|
||||||
middleware?: (Route | string)[]; // middleware
|
metadata?: { [key: string]: any };
|
||||||
|
middleware?: RouteMiddleware[]; // middleware
|
||||||
type? = 'route';
|
type? = 'route';
|
||||||
private _validator?: { [key: string]: Rule };
|
private _validator?: { [key: string]: Rule };
|
||||||
schema?: { [key: string]: Schema<any> };
|
schema?: { [key: string]: any };
|
||||||
data?: any;
|
data?: any;
|
||||||
|
/**
|
||||||
|
* 是否需要验证
|
||||||
|
*/
|
||||||
isVerify?: boolean;
|
isVerify?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否开启debug,开启后会打印错误信息
|
||||||
|
*/
|
||||||
isDebug?: boolean;
|
isDebug?: boolean;
|
||||||
constructor(path: string, key: string = '', opts?: RouteOpts) {
|
constructor(path: string = '', key: string = '', opts?: RouteOpts) {
|
||||||
|
if (!path) {
|
||||||
|
path = nanoid(8)
|
||||||
|
}
|
||||||
path = path.trim();
|
path = path.trim();
|
||||||
key = key.trim();
|
key = key.trim();
|
||||||
this.path = path;
|
this.path = path;
|
||||||
@@ -88,11 +144,13 @@ export class Route {
|
|||||||
if (opts) {
|
if (opts) {
|
||||||
this.id = opts.id || nanoid();
|
this.id = opts.id || nanoid();
|
||||||
if (!opts.id && opts.idUsePath) {
|
if (!opts.id && opts.idUsePath) {
|
||||||
this.id = path + '$#$' + key;
|
const delimiter = opts.delimiter ?? '$#$';
|
||||||
|
this.id = path + delimiter + key;
|
||||||
}
|
}
|
||||||
this.run = opts.run;
|
this.run = opts.run;
|
||||||
this.nextRoute = opts.nextRoute;
|
this.nextRoute = opts.nextRoute;
|
||||||
this.description = opts.description;
|
this.description = opts.description;
|
||||||
|
this.metadata = opts.metadata;
|
||||||
this.type = opts.type || 'route';
|
this.type = opts.type || 'route';
|
||||||
this.validator = opts.validator;
|
this.validator = opts.validator;
|
||||||
this.middleware = opts.middleware || [];
|
this.middleware = opts.middleware || [];
|
||||||
@@ -108,6 +166,7 @@ export class Route {
|
|||||||
this.isDebug = opts?.isDebug ?? false;
|
this.isDebug = opts?.isDebug ?? false;
|
||||||
}
|
}
|
||||||
private createSchema() {
|
private createSchema() {
|
||||||
|
try {
|
||||||
const validator = this.validator;
|
const validator = this.validator;
|
||||||
const keys = Object.keys(validator || {});
|
const keys = Object.keys(validator || {});
|
||||||
const schemaList = keys.map((key) => {
|
const schemaList = keys.map((key) => {
|
||||||
@@ -117,6 +176,9 @@ export class Route {
|
|||||||
return { ...prev, ...current };
|
return { ...prev, ...current };
|
||||||
}, {});
|
}, {});
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('createSchema error:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,16 +264,27 @@ export class Route {
|
|||||||
this.validator = validator;
|
this.validator = validator;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
prompt(description: string): this;
|
||||||
|
prompt(description: Function): this;
|
||||||
|
prompt(...args: any[]) {
|
||||||
|
const [description] = args;
|
||||||
|
if (typeof description === 'string') {
|
||||||
|
this.description = description;
|
||||||
|
} else if (typeof description === 'function') {
|
||||||
|
this.description = description() || ''; // 如果是Promise,需要addTo App之前就要获取应有的函数了。
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
define<T extends { [key: string]: any } = RouterContextT>(opts: DefineRouteOpts): this;
|
define<T extends { [key: string]: any } = RouterContextT>(opts: DefineRouteOpts): this;
|
||||||
define<T extends { [key: string]: any } = RouterContextT>(fn: Run<T>): this;
|
define<T extends { [key: string]: any } = RouterContextT>(fn: Run<T & U>): this;
|
||||||
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T>): this;
|
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T & U>): this;
|
||||||
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T>): this;
|
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T & U>): this;
|
||||||
define(...args: any[]) {
|
define(...args: any[]) {
|
||||||
const [path, key, opts] = args;
|
const [path, key, opts] = args;
|
||||||
// 全覆盖,所以opts需要准确,不能由idUsePath 需要check的变量
|
// 全覆盖,所以opts需要准确,不能由idUsePath 需要check的变量
|
||||||
const setOpts = (opts: DefineRouteOpts) => {
|
const setOpts = (opts: DefineRouteOpts) => {
|
||||||
const keys = Object.keys(opts);
|
const keys = Object.keys(opts);
|
||||||
const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'middleware', 'type', 'validator', 'isVerify', 'isDebug'];
|
const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug'];
|
||||||
for (let item of keys) {
|
for (let item of keys) {
|
||||||
if (!checkList.includes(item)) {
|
if (!checkList.includes(item)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -245,18 +318,44 @@ export class Route {
|
|||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
addTo(router: QueryRouter | { add: (route: Route) => void; [key: string]: any }) {
|
|
||||||
|
update(opts: DefineRouteOpts, checkList?: string[]): this {
|
||||||
|
const keys = Object.keys(opts);
|
||||||
|
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug'];
|
||||||
|
checkList = checkList || defaultCheckList;
|
||||||
|
for (let item of keys) {
|
||||||
|
if (!checkList.includes(item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item === 'validator') {
|
||||||
|
this.validator = opts[item];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item === 'middleware') {
|
||||||
|
this.middleware = this.middleware.concat(opts[item]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this[item] = opts[item];
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }) {
|
||||||
router.add(this);
|
router.add(this);
|
||||||
}
|
}
|
||||||
setData(data: any) {
|
setData(data: any) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
throw(code?: number | string, message?: string, tips?: string): void;
|
||||||
|
throw(...args: any[]) {
|
||||||
|
throw new CustomError(...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryRouter {
|
export class QueryRouter {
|
||||||
routes: Route[];
|
routes: Route[];
|
||||||
maxNextRoute = 40;
|
maxNextRoute = 40;
|
||||||
|
context?: RouteContext = {}; // default context for call
|
||||||
constructor() {
|
constructor() {
|
||||||
this.routes = [];
|
this.routes = [];
|
||||||
}
|
}
|
||||||
@@ -265,7 +364,7 @@ export class QueryRouter {
|
|||||||
const has = this.routes.find((r) => r.path === route.path && r.key === route.key);
|
const has = this.routes.find((r) => r.path === route.path && r.key === route.key);
|
||||||
if (has) {
|
if (has) {
|
||||||
// remove the old route
|
// remove the old route
|
||||||
this.routes = this.routes.filter((r) => r.path === route.path && r.key === route.key);
|
this.routes = this.routes.filter((r) => r.id !== has.id);
|
||||||
}
|
}
|
||||||
this.routes.push(route);
|
this.routes.push(route);
|
||||||
}
|
}
|
||||||
@@ -273,8 +372,8 @@ export class QueryRouter {
|
|||||||
* remove route by path and key
|
* remove route by path and key
|
||||||
* @param route
|
* @param route
|
||||||
*/
|
*/
|
||||||
remove(route: Route | { path: string; key: string }) {
|
remove(route: Route | { path: string; key?: string }) {
|
||||||
this.routes = this.routes.filter((r) => r.path === route.path && r.key === route.key);
|
this.routes = this.routes.filter((r) => r.path === route.path && r.key == route.key);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* remove route by id
|
* remove route by id
|
||||||
@@ -298,6 +397,12 @@ export class QueryRouter {
|
|||||||
ctx.currentKey = key;
|
ctx.currentKey = key;
|
||||||
ctx.currentRoute = route;
|
ctx.currentRoute = route;
|
||||||
ctx.index = (ctx.index || 0) + 1;
|
ctx.index = (ctx.index || 0) + 1;
|
||||||
|
const progress = [path, key] as [string, string];
|
||||||
|
if (ctx.progress) {
|
||||||
|
ctx.progress.push(progress);
|
||||||
|
} else {
|
||||||
|
ctx.progress = [progress];
|
||||||
|
}
|
||||||
if (ctx.index > maxNextRoute) {
|
if (ctx.index > maxNextRoute) {
|
||||||
ctx.code = 500;
|
ctx.code = 500;
|
||||||
ctx.message = 'Too many nextRoute';
|
ctx.message = 'Too many nextRoute';
|
||||||
@@ -307,19 +412,29 @@ export class QueryRouter {
|
|||||||
// run middleware
|
// run middleware
|
||||||
if (route && route.middleware && route.middleware.length > 0) {
|
if (route && route.middleware && route.middleware.length > 0) {
|
||||||
const errorMiddleware: { path?: string; key?: string; id?: string }[] = [];
|
const errorMiddleware: { path?: string; key?: string; id?: string }[] = [];
|
||||||
// TODO: 向上递归执行动作, 暂时不考虑
|
const getMiddleware = (m: Route) => {
|
||||||
const routeMiddleware = route.middleware.map((m) => {
|
if (!m.middleware || m.middleware.length === 0) return [];
|
||||||
|
const routeMiddleware: Route[] = [];
|
||||||
|
for (let i = 0; i < m.middleware.length; i++) {
|
||||||
|
const item = m.middleware[i];
|
||||||
let route: Route | undefined;
|
let route: Route | undefined;
|
||||||
const isString = typeof m === 'string';
|
const isString = typeof item === 'string';
|
||||||
if (typeof m === 'string') {
|
if (isString) {
|
||||||
route = this.routes.find((r) => r.id === m);
|
route = this.routes.find((r) => r.id === item);
|
||||||
} else {
|
} else {
|
||||||
route = this.routes.find((r) => r.path === m.path && r.key === m.key);
|
route = this.routes.find((r) => {
|
||||||
|
if (item.id) {
|
||||||
|
return r.id === item.id;
|
||||||
|
} else {
|
||||||
|
// key 可以是空,所以可以不严格验证
|
||||||
|
return r.path === item.path && r.key == item.key;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!route) {
|
if (!route) {
|
||||||
if (isString) {
|
if (isString) {
|
||||||
errorMiddleware.push({
|
errorMiddleware.push({
|
||||||
id: m as string,
|
id: item as string,
|
||||||
});
|
});
|
||||||
} else
|
} else
|
||||||
errorMiddleware.push({
|
errorMiddleware.push({
|
||||||
@@ -327,8 +442,15 @@ export class QueryRouter {
|
|||||||
key: m?.key,
|
key: m?.key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return route;
|
const routeMiddlewarePrevious = getMiddleware(route);
|
||||||
});
|
if (routeMiddlewarePrevious.length > 0) {
|
||||||
|
routeMiddleware.push(...routeMiddlewarePrevious);
|
||||||
|
}
|
||||||
|
routeMiddleware.push(route);
|
||||||
|
}
|
||||||
|
return routeMiddleware;
|
||||||
|
};
|
||||||
|
const routeMiddleware = getMiddleware(route);
|
||||||
if (errorMiddleware.length > 0) {
|
if (errorMiddleware.length > 0) {
|
||||||
console.error('middleware not found');
|
console.error('middleware not found');
|
||||||
ctx.body = errorMiddleware;
|
ctx.body = errorMiddleware;
|
||||||
@@ -357,10 +479,11 @@ export class QueryRouter {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (route?.isDebug) {
|
if (route?.isDebug) {
|
||||||
console.error('=====debug====:middlerware error');
|
console.error('=====debug====:middlerware error');
|
||||||
|
console.error('=====debug====:', e);
|
||||||
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
|
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
|
||||||
console.error('=====debug====:', e.message);
|
console.error('=====debug====:', e.message);
|
||||||
}
|
}
|
||||||
if (e instanceof CustomError) {
|
if (e instanceof CustomError || e?.code) {
|
||||||
ctx.code = e.code;
|
ctx.code = e.code;
|
||||||
ctx.message = e.message;
|
ctx.message = e.message;
|
||||||
ctx.body = null;
|
ctx.body = null;
|
||||||
@@ -434,12 +557,10 @@ export class QueryRouter {
|
|||||||
ctx.body = null;
|
ctx.body = null;
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
ctx.query = ctx.nextQuery;
|
ctx.query = { ...ctx.query, ...ctx.nextQuery };
|
||||||
ctx.nextQuery = {};
|
ctx.nextQuery = {};
|
||||||
return await this.runRoute(path, key, ctx);
|
return await this.runRoute(path, key, ctx);
|
||||||
}
|
}
|
||||||
// clear body
|
|
||||||
ctx.body = JSON.parse(JSON.stringify(ctx.body||''));
|
|
||||||
if (!ctx.code) ctx.code = 200;
|
if (!ctx.code) ctx.code = 200;
|
||||||
return ctx;
|
return ctx;
|
||||||
} else {
|
} else {
|
||||||
@@ -459,30 +580,96 @@ export class QueryRouter {
|
|||||||
*/
|
*/
|
||||||
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
if (!message?.path) {
|
if (!message?.path) {
|
||||||
return Promise.resolve({ code: 404, body: 'Not found path' });
|
return Promise.resolve({ code: 404, body: null, message: 'Not found path' });
|
||||||
}
|
}
|
||||||
const { path, key, payload = {}, ...query } = message;
|
const { path, key = '', payload = {}, ...query } = message;
|
||||||
ctx = ctx || {};
|
ctx = ctx || {};
|
||||||
ctx.query = { ...ctx.query, ...query, ...payload };
|
ctx.query = { ...ctx.query, ...query, ...payload };
|
||||||
ctx.state = {};
|
ctx.state = { ...ctx?.state };
|
||||||
|
ctx.throw = this.throw;
|
||||||
// put queryRouter to ctx
|
// put queryRouter to ctx
|
||||||
// TODO: 是否需要queryRouter,函数内部处理router路由执行,这应该是避免去内部去包含的功能过
|
// TODO: 是否需要queryRouter,函数内部处理router路由执行,这应该是避免去内部去包含的功能过
|
||||||
ctx.queryRouter = this;
|
ctx.queryRouter = this;
|
||||||
ctx.call = this.call.bind(this);
|
ctx.call = this.call.bind(this);
|
||||||
|
ctx.queryRoute = this.queryRoute.bind(this);
|
||||||
ctx.index = 0;
|
ctx.index = 0;
|
||||||
return await this.runRoute(path, key, ctx);
|
ctx.progress = ctx.progress || [];
|
||||||
|
const res = await this.runRoute(path, key, ctx);
|
||||||
|
const serialize = ctx.needSerialize ?? true; // 是否需要序列化
|
||||||
|
if (serialize) {
|
||||||
|
res.body = JSON.parse(JSON.stringify(res.body || ''));
|
||||||
}
|
}
|
||||||
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
return res;
|
||||||
return await this.parse(message, ctx);
|
}
|
||||||
|
/**
|
||||||
|
* 返回的数据包含所有的context的请求返回的内容,可做其他处理
|
||||||
|
* @param message
|
||||||
|
* @param ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
|
let path = message.path;
|
||||||
|
let key = message.key;
|
||||||
|
if (message.id) {
|
||||||
|
const route = this.routes.find((r) => r.id === message.id);
|
||||||
|
if (route) {
|
||||||
|
path = route.path;
|
||||||
|
key = route.key;
|
||||||
|
} else {
|
||||||
|
return { code: 404, body: null, message: 'Not found route' };
|
||||||
|
}
|
||||||
|
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
|
||||||
|
} else if (path) {
|
||||||
|
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
|
||||||
|
} else {
|
||||||
|
return { code: 404, body: null, message: 'Not found path' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求 result 的数据
|
||||||
|
* @param message
|
||||||
|
* @param ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async queryRoute(message: { id?: string; path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
|
const res = await this.parse(message, { ...this.context, ...ctx });
|
||||||
|
return {
|
||||||
|
code: res.code,
|
||||||
|
data: res.body,
|
||||||
|
message: res.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 设置上下文
|
||||||
|
* @description 这里的上下文是为了在handle函数中使用
|
||||||
|
* @param ctx
|
||||||
|
*/
|
||||||
|
setContext(ctx: RouteContext) {
|
||||||
|
this.context = ctx;
|
||||||
}
|
}
|
||||||
getList(): RouteInfo[] {
|
getList(): RouteInfo[] {
|
||||||
return this.routes.map((r) => {
|
return this.routes.map((r) => {
|
||||||
return pick(r, pickValue as any);
|
return pick(r, pickValue as any);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取handle函数, 这里会去执行parse函数
|
||||||
|
*/
|
||||||
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
|
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
|
||||||
return async (msg: { path: string; key?: string; [key: string]: any }) => {
|
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
|
||||||
const context = { ...ctx };
|
try {
|
||||||
|
const context = { ...ctx, ...handleContext };
|
||||||
|
if (msg.id) {
|
||||||
|
const route = router.routes.find((r) => r.id === msg.id);
|
||||||
|
if (route) {
|
||||||
|
msg.path = route.path;
|
||||||
|
msg.key = route.key;
|
||||||
|
} else {
|
||||||
|
return { code: 404, message: 'Not found route' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
const res = await router.parse(msg, context);
|
const res = await router.parse(msg, context);
|
||||||
if (wrapperFn) {
|
if (wrapperFn) {
|
||||||
res.data = res.body;
|
res.data = res.body;
|
||||||
@@ -490,8 +677,42 @@ export class QueryRouter {
|
|||||||
}
|
}
|
||||||
const { code, body, message } = res;
|
const { code, body, message } = res;
|
||||||
return { code, data: body, message };
|
return { code, data: body, message };
|
||||||
|
} catch (e) {
|
||||||
|
return { code: 500, message: e.message };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
exportRoutes() {
|
||||||
|
return this.routes.map((r) => {
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
importRoutes(routes: Route[]) {
|
||||||
|
for (let route of routes) {
|
||||||
|
this.add(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
importRouter(router: QueryRouter) {
|
||||||
|
this.importRoutes(router.routes);
|
||||||
|
}
|
||||||
|
throw(code?: number | string, message?: string, tips?: string): void;
|
||||||
|
throw(...args: any[]) {
|
||||||
|
throw new CustomError(...args);
|
||||||
|
}
|
||||||
|
hasRoute(path: string, key: string = '') {
|
||||||
|
return this.routes.find((r) => r.path === path && r.key === key);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 等待程序运行, 获取到message的数据,就执行
|
||||||
|
*
|
||||||
|
* emitter = process
|
||||||
|
* -- .exit
|
||||||
|
* -- .on
|
||||||
|
* -- .send
|
||||||
|
*/
|
||||||
|
wait(params?: { path?: string; key?: string; payload?: any }, opts?: { emitter?: any, timeout?: number }) {
|
||||||
|
return listenProcess({ app: this, params, ...opts });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryRouterServerOpts = {
|
type QueryRouterServerOpts = {
|
||||||
@@ -499,7 +720,7 @@ type QueryRouterServerOpts = {
|
|||||||
context?: RouteContext;
|
context?: RouteContext;
|
||||||
};
|
};
|
||||||
interface HandleFn<T = any> {
|
interface HandleFn<T = any> {
|
||||||
(msg: { path: string; [key: string]: any }, ctx?: any): { code: string; data?: any; message?: string; [key: string]: any };
|
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
|
||||||
(res: RouteContext<T>): any;
|
(res: RouteContext<T>): any;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -511,8 +732,83 @@ export class QueryRouterServer extends QueryRouter {
|
|||||||
constructor(opts?: QueryRouterServerOpts) {
|
constructor(opts?: QueryRouterServerOpts) {
|
||||||
super();
|
super();
|
||||||
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
|
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
|
||||||
|
this.setContext({ needSerialize: false, ...opts?.context });
|
||||||
}
|
}
|
||||||
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
||||||
this.handle = this.getHandle(this, wrapperFn, ctx);
|
this.handle = this.getHandle(this, wrapperFn, ctx);
|
||||||
}
|
}
|
||||||
|
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
|
||||||
|
const route = new Route(path, '', opts);
|
||||||
|
route.run = fn;
|
||||||
|
this.add(route);
|
||||||
|
}
|
||||||
|
addRoute(route: Route) {
|
||||||
|
this.add(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
Route = Route;
|
||||||
|
route(opts: RouteOpts): Route<Required<RouteContext>>;
|
||||||
|
route(path: string, key?: string): Route<Required<RouteContext>>;
|
||||||
|
route(path: string, opts?: RouteOpts): Route<Required<RouteContext>>;
|
||||||
|
route(path: string, key?: string, opts?: RouteOpts): Route<Required<RouteContext>>;
|
||||||
|
route(...args: any[]) {
|
||||||
|
const [path, key, opts] = args;
|
||||||
|
if (typeof path === 'object') {
|
||||||
|
return new Route(path.path, path.key, path);
|
||||||
|
}
|
||||||
|
if (typeof path === 'string') {
|
||||||
|
if (opts) {
|
||||||
|
return new Route(path, key, opts);
|
||||||
|
}
|
||||||
|
if (key && typeof key === 'object') {
|
||||||
|
return new Route(path, key?.key || '', key);
|
||||||
|
}
|
||||||
|
return new Route(path, key);
|
||||||
|
}
|
||||||
|
return new Route(path, key, opts);
|
||||||
|
}
|
||||||
|
prompt(description: string): Route<Required<RouteContext>>;
|
||||||
|
prompt(description: Function): Route<Required<RouteContext>>;
|
||||||
|
prompt(...args: any[]) {
|
||||||
|
const [desc] = args;
|
||||||
|
let description = ''
|
||||||
|
if (typeof desc === 'string') {
|
||||||
|
description = desc;
|
||||||
|
} else if (typeof desc === 'function') {
|
||||||
|
description = desc() || ''; // 如果是Promise,需要addTo App之前就要获取应有的函数了。
|
||||||
|
}
|
||||||
|
return new Route('', '', { description });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等于queryRoute,但是调用了handle
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async run({ path, key, payload }: { path: string; key?: string; payload?: any }) {
|
||||||
|
const handle = this.handle;
|
||||||
|
const resultError = (error: string, code = 500) => {
|
||||||
|
const r = {
|
||||||
|
code: code,
|
||||||
|
message: error,
|
||||||
|
};
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const end = handle({ path, key, ...payload });
|
||||||
|
return end;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code && typeof e.code === 'number') {
|
||||||
|
return {
|
||||||
|
code: e.code,
|
||||||
|
message: e.message,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return resultError('Router Server error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Mini = QueryRouterServer
|
||||||
154
src/router-define.ts
Normal file
154
src/router-define.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type { QueryRouterServer, RouteOpts, Run, RouteMiddleware } from '@kevisual/router';
|
||||||
|
import type { DataOpts, Query, Result } from '@kevisual/query/query';
|
||||||
|
// export type RouteObject<T extends readonly string[]> = {
|
||||||
|
// [K in T[number]]: RouteOpts;
|
||||||
|
// };
|
||||||
|
export type { RouteOpts };
|
||||||
|
export type RouteObject = {
|
||||||
|
[key: string]: RouteOpts;
|
||||||
|
};
|
||||||
|
type SimpleObject = Record<string, any>;
|
||||||
|
export function define<T extends Record<string, RouteOpts>>(
|
||||||
|
value: T,
|
||||||
|
): {
|
||||||
|
[K in keyof T]: T[K] & RouteOpts;
|
||||||
|
} {
|
||||||
|
return value as { [K in keyof T]: T[K] & RouteOpts };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteArray = RouteOpts[];
|
||||||
|
type ChainOptions = {
|
||||||
|
app: QueryRouterServer;
|
||||||
|
};
|
||||||
|
class Chain {
|
||||||
|
object: RouteOpts;
|
||||||
|
app?: QueryRouterServer;
|
||||||
|
constructor(object: RouteOpts, opts?: ChainOptions) {
|
||||||
|
this.object = object;
|
||||||
|
this.app = opts?.app;
|
||||||
|
}
|
||||||
|
get key() {
|
||||||
|
return this.object.key;
|
||||||
|
}
|
||||||
|
get path() {
|
||||||
|
return this.object.path;
|
||||||
|
}
|
||||||
|
setDescription(desc: string) {
|
||||||
|
this.object.description = desc;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setMeta(metadata: { [key: string]: any }) {
|
||||||
|
this.object.metadata = metadata;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setPath(path: string) {
|
||||||
|
this.object.path = path;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setMiddleware(middleware: RouteMiddleware[]) {
|
||||||
|
this.object.middleware = middleware;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setKey(key: string) {
|
||||||
|
this.object.key = key;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setId(key: string) {
|
||||||
|
this.object.id = key;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setRun<U extends SimpleObject = {}>(run: Run<U>) {
|
||||||
|
this.object.run = run;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
define<U extends SimpleObject = {}>(run: Run<U>) {
|
||||||
|
this.object.run = run;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
createRoute() {
|
||||||
|
this.app.route(this.object).addTo(this.app);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type QueryChainOptions = {
|
||||||
|
query?: Query;
|
||||||
|
omitKeys?: string[];
|
||||||
|
};
|
||||||
|
class QueryChain {
|
||||||
|
obj: SimpleObject = {};
|
||||||
|
query: Query;
|
||||||
|
omitKeys: string[] = ['metadata', 'description', 'validator'];
|
||||||
|
constructor(value?: SimpleObject, opts?: QueryChainOptions) {
|
||||||
|
this.obj = value || {};
|
||||||
|
this.query = opts?.query;
|
||||||
|
if (opts?.omitKeys) this.omitKeys = opts.omitKeys;
|
||||||
|
}
|
||||||
|
omit(obj: SimpleObject, key: string[] = []) {
|
||||||
|
const newObj = { ...obj };
|
||||||
|
key.forEach((k) => {
|
||||||
|
delete newObj[k];
|
||||||
|
});
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 生成
|
||||||
|
* @param queryData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getKey(queryData?: SimpleObject): Pick<RouteOpts, 'path' | 'key' | 'metadata' | 'description' | 'validator'> {
|
||||||
|
const obj = this.omit(this.obj, this.omitKeys);
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
...queryData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
post<R = SimpleObject, P = SimpleObject>(data: P, options?: DataOpts): Promise<Result<R>> {
|
||||||
|
const _queryData = this.getKey(data);
|
||||||
|
return this.query.post(_queryData, options);
|
||||||
|
}
|
||||||
|
get<R = SimpleObject, P = SimpleObject>(data: P, options?: DataOpts): Promise<Result<R>> {
|
||||||
|
const _queryData = this.getKey(data);
|
||||||
|
return this.query.get(_queryData, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const util = {
|
||||||
|
getChain: (obj: RouteOpts, opts?: ChainOptions) => {
|
||||||
|
return new Chain(obj, opts);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class QueryUtil<T extends RouteObject = RouteObject> {
|
||||||
|
obj: T;
|
||||||
|
app: QueryRouterServer;
|
||||||
|
query: Query;
|
||||||
|
constructor(object: T, opts?: ChainOptions & QueryChainOptions) {
|
||||||
|
this.obj = object;
|
||||||
|
this.app = opts?.app;
|
||||||
|
this.query = opts?.query;
|
||||||
|
}
|
||||||
|
static createFormObj<U extends RouteObject>(object: U, opts?: ChainOptions) {
|
||||||
|
return new QueryUtil<U>(object, opts);
|
||||||
|
}
|
||||||
|
static create<U extends Record<string, RouteOpts>>(value: U, opts?: ChainOptions) {
|
||||||
|
const obj = value as { [K in keyof U]: U[K] & RouteOpts };
|
||||||
|
return new QueryUtil<U>(obj, opts);
|
||||||
|
}
|
||||||
|
get<K extends keyof T>(key: K): RouteOpts {
|
||||||
|
return this.obj[key] as RouteOpts;
|
||||||
|
}
|
||||||
|
chain<K extends keyof T>(key: K, opts?: ChainOptions) {
|
||||||
|
const obj = this.obj[key];
|
||||||
|
let newOpts = { app: this.app, ...opts };
|
||||||
|
return new QueryUtil.Chain(obj, newOpts);
|
||||||
|
}
|
||||||
|
queryChain<K extends keyof T>(key: K, opts?: QueryChainOptions) {
|
||||||
|
const value = this.obj[key];
|
||||||
|
let newOpts = { query: this.query, ...opts };
|
||||||
|
return new QueryUtil.QueryChain(value, newOpts);
|
||||||
|
}
|
||||||
|
static Chain = Chain;
|
||||||
|
static QueryChain = QueryChain;
|
||||||
|
get routeObject() {
|
||||||
|
return this.obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/router-simple-lib.ts
Normal file
3
src/router-simple-lib.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { parseXml } from './server/parse-xml.ts';
|
||||||
|
|
||||||
|
export { parseXml };
|
||||||
268
src/router-simple.ts
Normal file
268
src/router-simple.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import { pathToRegexp, Key } from 'path-to-regexp';
|
||||||
|
import type { IncomingMessage, ServerResponse, Server } from 'node:http';
|
||||||
|
import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts';
|
||||||
|
import { ListenOptions } from 'node:net';
|
||||||
|
|
||||||
|
type Req = IncomingMessage & { params?: Record<string, string> };
|
||||||
|
type SimpleObject = {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
interface Route {
|
||||||
|
method: string;
|
||||||
|
regexp: RegExp;
|
||||||
|
keys: Key[];
|
||||||
|
handlers: Array<(req: Req, res: ServerResponse) => Promise<void> | void>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* SimpleRouter
|
||||||
|
*/
|
||||||
|
export class SimpleRouter {
|
||||||
|
routes: Route[] = [];
|
||||||
|
exclude: string[] = []; // 排除的请求
|
||||||
|
constructor(opts?: { exclude?: string[] }) {
|
||||||
|
this.exclude = opts?.exclude || ['/api/router'];
|
||||||
|
}
|
||||||
|
getBody(req: Req) {
|
||||||
|
return parseBody<Record<string, any>>(req);
|
||||||
|
}
|
||||||
|
getSearch(req: Req) {
|
||||||
|
return parseSearch(req);
|
||||||
|
}
|
||||||
|
parseSearchValue = parseSearchValue;
|
||||||
|
use(method: string, route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
||||||
|
const handlers = Array.isArray(fns) ? fns.flat() : [];
|
||||||
|
const pattern = pathToRegexp(route);
|
||||||
|
this.routes.push({ method: method.toLowerCase(), regexp: pattern.regexp, keys: pattern.keys, handlers });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
get(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
||||||
|
return this.use('get', route, ...fns);
|
||||||
|
}
|
||||||
|
post(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
||||||
|
return this.use('post', route, ...fns);
|
||||||
|
}
|
||||||
|
sse(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
||||||
|
return this.use('sse', route, ...fns);
|
||||||
|
}
|
||||||
|
all(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
||||||
|
this.use('post', route, ...fns);
|
||||||
|
this.use('get', route, ...fns);
|
||||||
|
this.use('sse', route, ...fns);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
getJson(v: string | number | boolean | SimpleObject) {
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(v as string);
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isSse(req: Req) {
|
||||||
|
const { headers } = req;
|
||||||
|
if (headers['accept'] && headers['accept'].includes('text/event-stream')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (headers['content-type'] && headers['content-type'].includes('text/event-stream')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 解析 req 和 res 请求
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
parse(req: Req, res: ServerResponse) {
|
||||||
|
const { pathname } = new URL(req.url, 'http://localhost');
|
||||||
|
let method = req.method.toLowerCase();
|
||||||
|
if (this.exclude.includes(pathname)) {
|
||||||
|
return 'is_exclude';
|
||||||
|
}
|
||||||
|
const isSse = this.isSse(req);
|
||||||
|
if (isSse) method = 'sse';
|
||||||
|
const route = this.routes.find((route) => {
|
||||||
|
const matchResult = route.regexp.exec(pathname);
|
||||||
|
if (matchResult && route.method === method) {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
route.keys.forEach((key, i) => {
|
||||||
|
params[key.name] = matchResult[i + 1];
|
||||||
|
});
|
||||||
|
req.params = params;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (route) {
|
||||||
|
const { handlers } = route;
|
||||||
|
return handlers.reduce((promiseChain, handler) => promiseChain.then(() => Promise.resolve(handler(req, res))), Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'not_found';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 创建一个新的 HttpChain 实例
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
chain(req?: Req, res?: ServerResponse) {
|
||||||
|
const chain = new HttpChain({ req, res, simpleRouter: this });
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
static Chain(opts?: HttpChainOpts) {
|
||||||
|
return new HttpChain(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpChainOpts = {
|
||||||
|
req?: Req;
|
||||||
|
res?: ServerResponse;
|
||||||
|
simpleRouter?: SimpleRouter;
|
||||||
|
};
|
||||||
|
export class HttpChain {
|
||||||
|
req: Req;
|
||||||
|
res: ServerResponse;
|
||||||
|
simpleRouter: SimpleRouter;
|
||||||
|
server: Server;
|
||||||
|
hasSetHeader: boolean = false;
|
||||||
|
isSseSet: boolean = false;
|
||||||
|
constructor(opts?: HttpChainOpts) {
|
||||||
|
this.req = opts?.req;
|
||||||
|
this.res = opts?.res;
|
||||||
|
this.simpleRouter = opts?.simpleRouter;
|
||||||
|
}
|
||||||
|
setReq(req: Req) {
|
||||||
|
this.req = req;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setRes(res: ServerResponse) {
|
||||||
|
this.res = res;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setRouter(router: SimpleRouter) {
|
||||||
|
this.simpleRouter = router;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setServer(server: Server) {
|
||||||
|
this.server = server;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 兼容 express 的一点功能
|
||||||
|
* @param status
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
status(status: number) {
|
||||||
|
if (!this.res) return this;
|
||||||
|
if (this.hasSetHeader) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.hasSetHeader = true;
|
||||||
|
this.res.writeHead(status);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
writeHead(status: number) {
|
||||||
|
if (!this.res) return this;
|
||||||
|
if (this.hasSetHeader) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.hasSetHeader = true;
|
||||||
|
this.res.writeHead(status);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
json(data: SimpleObject) {
|
||||||
|
if (!this.res) return this;
|
||||||
|
this.res.end(JSON.stringify(data));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 兼容 express 的一点功能
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
end(data: SimpleObject | string) {
|
||||||
|
if (!this.res) return this;
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
this.res.end(JSON.stringify(data));
|
||||||
|
} else if (typeof data === 'string') {
|
||||||
|
this.res.end(data);
|
||||||
|
} else {
|
||||||
|
this.res.end('nothing');
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(opts: ListenOptions, callback?: () => void) {
|
||||||
|
this.server.listen(opts, callback);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
parse() {
|
||||||
|
if (!this.server || !this.simpleRouter) {
|
||||||
|
throw new Error('Server and SimpleRouter must be set before calling parse');
|
||||||
|
}
|
||||||
|
const that = this;
|
||||||
|
const listener = (req: Req, res: ServerResponse) => {
|
||||||
|
try {
|
||||||
|
that.simpleRouter.parse(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing request:', error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ code: 500, message: 'Internal Server Error' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.server.on('request', listener);
|
||||||
|
return () => {
|
||||||
|
that.server.removeListener('request', listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getString(value: string | SimpleObject) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
sse(value: string | SimpleObject) {
|
||||||
|
const res = this.res;
|
||||||
|
const req = this.req;
|
||||||
|
if (!res || !req) return;
|
||||||
|
const data = this.getString(value);
|
||||||
|
if (this.isSseSet) {
|
||||||
|
res.write(`data: ${data}\n\n`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const headersMap = new Map<string, string>([
|
||||||
|
['Content-Type', 'text/event-stream'],
|
||||||
|
['Cache-Control', 'no-cache'],
|
||||||
|
['Connection', 'keep-alive'],
|
||||||
|
]);
|
||||||
|
this.isSseSet = true;
|
||||||
|
let intervalId: NodeJS.Timeout;
|
||||||
|
if (!this.hasSetHeader) {
|
||||||
|
this.hasSetHeader = true;
|
||||||
|
res.setHeaders(headersMap);
|
||||||
|
// 每隔 2 秒发送一个空行,保持连接
|
||||||
|
setInterval(() => {
|
||||||
|
res.write('\n'); // 发送一个空行,保持连接
|
||||||
|
}, 3000);
|
||||||
|
// 客户端断开连接时清理
|
||||||
|
req.on('close', () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.res.write(`data: ${data}\n\n`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
if (this.req?.destroy) {
|
||||||
|
this.req.destroy();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/server/deno-ws-server.ts
Normal file
0
src/server/deno-ws-server.ts
Normal file
@@ -1,9 +1,14 @@
|
|||||||
import http, { IncomingMessage, Server, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
import { parseBody } from './parse-body.ts';
|
import { parseBody } from './parse-body.ts';
|
||||||
import url from 'url';
|
import url from 'node:url';
|
||||||
|
import { createHandleCtx } from './server.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get params and body
|
* get params and body
|
||||||
|
* 优先原则
|
||||||
|
* 1. 请求参数中的 payload 的token 优先
|
||||||
|
* 2. 请求头中的 authorization 优先
|
||||||
|
* 3. 请求头中的 cookie 优先
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
@@ -20,6 +25,11 @@ export const handleServer = async (req: IncomingMessage, res: ServerResponse) =>
|
|||||||
const parsedUrl = url.parse(req.url, true);
|
const parsedUrl = url.parse(req.url, true);
|
||||||
// 获取token
|
// 获取token
|
||||||
let token = req.headers['authorization'] || '';
|
let token = req.headers['authorization'] || '';
|
||||||
|
const handle = createHandleCtx(req, res);
|
||||||
|
const cookies = handle.req.cookies;
|
||||||
|
if (!token) {
|
||||||
|
token = cookies.token; // cookie优先
|
||||||
|
}
|
||||||
if (token) {
|
if (token) {
|
||||||
token = token.replace('Bearer ', '');
|
token = token.replace('Bearer ', '');
|
||||||
}
|
}
|
||||||
@@ -41,6 +51,7 @@ export const handleServer = async (req: IncomingMessage, res: ServerResponse) =>
|
|||||||
token,
|
token,
|
||||||
...param,
|
...param,
|
||||||
...body,
|
...body,
|
||||||
|
cookies,
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as http from 'http';
|
import type { IncomingMessage } from 'node:http';
|
||||||
|
import url from 'node:url';
|
||||||
|
|
||||||
export const parseBody = async (req: http.IncomingMessage) => {
|
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const arr: any[] = [];
|
const arr: any[] = [];
|
||||||
req.on('data', (chunk) => {
|
req.on('data', (chunk) => {
|
||||||
arr.push(chunk);
|
arr.push(chunk);
|
||||||
@@ -9,10 +10,64 @@ export const parseBody = async (req: http.IncomingMessage) => {
|
|||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const body = Buffer.concat(arr).toString();
|
const body = Buffer.concat(arr).toString();
|
||||||
resolve(JSON.parse(body));
|
|
||||||
|
// 获取 Content-Type 头信息
|
||||||
|
const contentType = req.headers['content-type'] || '';
|
||||||
|
|
||||||
|
// 处理 application/json
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
resolve(JSON.parse(body) as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 处理 application/x-www-form-urlencoded
|
||||||
|
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = new URLSearchParams(body);
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
||||||
|
try {
|
||||||
|
result[key] = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(result as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认尝试 JSON 解析
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(body) as T);
|
||||||
|
} catch {
|
||||||
|
resolve({} as T);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
resolve({});
|
resolve({} as T);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseSearch = (req: IncomingMessage) => {
|
||||||
|
const parsedUrl = url.parse(req.url, true);
|
||||||
|
return parsedUrl.query;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把url当个key 的 value 的字符串转成json
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export const parseSearchValue = (value?: string, opts?: { decode?: boolean }) => {
|
||||||
|
if (!value) return {};
|
||||||
|
const decode = opts?.decode ?? false;
|
||||||
|
if (decode) {
|
||||||
|
value = decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
31
src/server/parse-xml.ts
Normal file
31
src/server/parse-xml.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import xml2js from 'xml2js';
|
||||||
|
|
||||||
|
export const parseXml = async (req: any): Promise<any> => {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
// 读取请求数据
|
||||||
|
let data = '';
|
||||||
|
req.setEncoding('utf8');
|
||||||
|
// 监听data事件,接收数据片段
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
// 当请求结束时处理数据
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
// 使用xml2js解析XML
|
||||||
|
xml2js.parseString(data, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
console.error('XML解析错误:', err);
|
||||||
|
resolve(null);
|
||||||
|
} else {
|
||||||
|
const jsonString = JSON.stringify(result);
|
||||||
|
resolve(jsonString);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理请求时出错:', error);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,20 +1,63 @@
|
|||||||
import http, { IncomingMessage, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
|
import http from 'node:http';
|
||||||
|
import https from 'node:https';
|
||||||
|
import http2 from 'node:http2';
|
||||||
import { handleServer } from './handle-server.ts';
|
import { handleServer } from './handle-server.ts';
|
||||||
|
import * as cookie from 'cookie';
|
||||||
export type Listener = (...args: any[]) => void;
|
export type Listener = (...args: any[]) => void;
|
||||||
|
|
||||||
|
type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void;
|
||||||
|
|
||||||
|
export type HandleCtx = {
|
||||||
|
req: IncomingMessage & { cookies: Record<string, string> };
|
||||||
|
res: ServerResponse & {
|
||||||
|
/**
|
||||||
|
* cookie 函数, end 参数用于设置是否立即设置到响应头,设置了后面的cookie再设置会覆盖前面的
|
||||||
|
*/
|
||||||
|
cookie: CookieFn; //
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// 实现函数
|
||||||
|
export function createHandleCtx(req: IncomingMessage, res: ServerResponse): HandleCtx {
|
||||||
|
// 用于存储所有的 Set-Cookie 字符串
|
||||||
|
const cookies: string[] = [];
|
||||||
|
let handReq = req as HandleCtx['req'];
|
||||||
|
let handRes = res as HandleCtx['res'];
|
||||||
|
// 扩展 res.cookie 方法
|
||||||
|
const cookieFn: CookieFn = (name, value, options = {}, end = true) => {
|
||||||
|
// 序列化新的 Cookie
|
||||||
|
const serializedCookie = cookie.serialize(name, value, options);
|
||||||
|
cookies.push(serializedCookie); // 将新的 Cookie 添加到数组
|
||||||
|
if (end) {
|
||||||
|
// 如果设置了 end 参数,则立即设置到响应头
|
||||||
|
res.setHeader('Set-Cookie', cookies);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 解析请求中的现有 Cookie
|
||||||
|
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
||||||
|
handReq.cookies = parsedCookies;
|
||||||
|
handRes.cookie = cookieFn;
|
||||||
|
// 返回扩展的上下文
|
||||||
|
return {
|
||||||
|
req: handReq,
|
||||||
|
res: handRes,
|
||||||
|
};
|
||||||
|
}
|
||||||
export type Cors = {
|
export type Cors = {
|
||||||
/**
|
/**
|
||||||
* @default '*''
|
* @default '*''
|
||||||
*/
|
*/
|
||||||
origin?: string | undefined;
|
origin?: string | undefined;
|
||||||
};
|
};
|
||||||
type ServerOpts = {
|
export type ServerOpts = {
|
||||||
/**path default `/api/router` */
|
/**path default `/api/router` */
|
||||||
path?: string;
|
path?: string;
|
||||||
/**handle Fn */
|
/**handle Fn */
|
||||||
handle?: (msg?: { path: string; key?: string; [key: string]: any }) => any;
|
handle?: (msg?: { path: string; key?: string; [key: string]: any }, ctx?: { req: http.IncomingMessage; res: http.ServerResponse }) => any;
|
||||||
cors?: Cors;
|
cors?: Cors;
|
||||||
|
httpType?: 'http' | 'https' | 'http2';
|
||||||
|
httpsKey?: string;
|
||||||
|
httpsCert?: string;
|
||||||
};
|
};
|
||||||
export const resultError = (error: string, code = 500) => {
|
export const resultError = (error: string, code = 500) => {
|
||||||
const r = {
|
const r = {
|
||||||
@@ -26,15 +69,25 @@ export const resultError = (error: string, code = 500) => {
|
|||||||
|
|
||||||
export class Server {
|
export class Server {
|
||||||
path = '/api/router';
|
path = '/api/router';
|
||||||
private _server: http.Server;
|
private _server: http.Server | https.Server | http2.Http2SecureServer;
|
||||||
public handle: ServerOpts['handle'];
|
public handle: ServerOpts['handle'];
|
||||||
private _callback: any;
|
private _callback: any;
|
||||||
private cors: Cors;
|
private cors: Cors;
|
||||||
private hasOn = false;
|
private hasOn = false;
|
||||||
|
private httpType = 'http';
|
||||||
|
private options = {
|
||||||
|
key: '',
|
||||||
|
cert: '',
|
||||||
|
};
|
||||||
constructor(opts?: ServerOpts) {
|
constructor(opts?: ServerOpts) {
|
||||||
this.path = opts?.path || '/api/router';
|
this.path = opts?.path || '/api/router';
|
||||||
this.handle = opts?.handle;
|
this.handle = opts?.handle;
|
||||||
this.cors = opts?.cors;
|
this.cors = opts?.cors;
|
||||||
|
this.httpType = opts?.httpType || 'http';
|
||||||
|
this.options = {
|
||||||
|
key: opts?.httpsKey || '',
|
||||||
|
cert: opts?.httpsCert || '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
||||||
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
||||||
@@ -45,11 +98,40 @@ export class Server {
|
|||||||
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
||||||
listen(handle: any, listeningListener?: () => void): void;
|
listen(handle: any, listeningListener?: () => void): void;
|
||||||
listen(...args: any[]) {
|
listen(...args: any[]) {
|
||||||
this._server = http.createServer();
|
this._server = this.createServer();
|
||||||
const callback = this.createCallback();
|
const callback = this.createCallback();
|
||||||
this._server.on('request', callback);
|
this._server.on('request', callback);
|
||||||
this._server.listen(...args);
|
this._server.listen(...args);
|
||||||
}
|
}
|
||||||
|
createServer() {
|
||||||
|
let server: http.Server | https.Server | http2.Http2SecureServer;
|
||||||
|
const httpType = this.httpType;
|
||||||
|
if (httpType === 'https') {
|
||||||
|
if (this.options.key && this.options.cert) {
|
||||||
|
server = https.createServer({
|
||||||
|
key: this.options.key,
|
||||||
|
cert: this.options.cert,
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
} else {
|
||||||
|
console.error('https key and cert is required');
|
||||||
|
console.log('downgrade to http');
|
||||||
|
}
|
||||||
|
} else if (httpType === 'http2') {
|
||||||
|
if (this.options.key && this.options.cert) {
|
||||||
|
server = http2.createSecureServer({
|
||||||
|
key: this.options.key,
|
||||||
|
cert: this.options.cert,
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
} else {
|
||||||
|
console.error('https key and cert is required');
|
||||||
|
console.log('downgrade to http');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server = http.createServer();
|
||||||
|
return server;
|
||||||
|
}
|
||||||
setHandle(handle?: any) {
|
setHandle(handle?: any) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
}
|
}
|
||||||
@@ -62,10 +144,10 @@ export class Server {
|
|||||||
const handle = this.handle;
|
const handle = this.handle;
|
||||||
const cors = this.cors;
|
const cors = this.cors;
|
||||||
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
|
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
// only handle /api/router
|
||||||
if (req.url === '/favicon.ico') {
|
if (req.url === '/favicon.ico') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
// 程序已经在其他地方响应了
|
// 程序已经在其他地方响应了
|
||||||
return;
|
return;
|
||||||
@@ -76,18 +158,15 @@ export class Server {
|
|||||||
// 交给其他监听处理
|
// 交给其他监听处理
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
||||||
if (cors) {
|
if (cors) {
|
||||||
res.setHeader('Access-Control-Allow-Origin', cors?.origin || '*'); // 允许所有域名的请求访问,可以根据需要设置具体的域名
|
res.setHeader('Access-Control-Allow-Origin', cors?.origin || '*'); // 允许所有域名的请求访问,可以根据需要设置具体的域名
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.writeHead(200); // 设置响应头,给予其他api知道headersSent,它已经被响应了
|
|
||||||
|
|
||||||
const url = req.url;
|
const url = req.url;
|
||||||
if (!url.startsWith(path)) {
|
if (!url.startsWith(path)) {
|
||||||
res.end(resultError(`not path:[${path}]`));
|
res.end(resultError(`not path:[${path}]`));
|
||||||
@@ -99,7 +178,12 @@ export class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const end = await handle(messages as any);
|
const end = await handle(messages as any, { req, res });
|
||||||
|
if (res.writableEnded) {
|
||||||
|
// 如果响应已经结束,则不进行任何操作
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
if (typeof end === 'string') {
|
if (typeof end === 'string') {
|
||||||
res.end(end);
|
res.end(end);
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +191,7 @@ export class Server {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
if (e.code && typeof e.code === 'number') {
|
if (e.code && typeof e.code === 'number') {
|
||||||
res.end(resultError(e.message || `Router Server error`, e.code));
|
res.end(resultError(e.message || `Router Server error`, e.code));
|
||||||
} else {
|
} else {
|
||||||
@@ -129,7 +214,7 @@ export class Server {
|
|||||||
* @param listener
|
* @param listener
|
||||||
*/
|
*/
|
||||||
on(listener: Listener | Listener[]) {
|
on(listener: Listener | Listener[]) {
|
||||||
this._server = this._server || http.createServer();
|
this._server = this._server || this.createServer();
|
||||||
this._server.removeAllListeners('request');
|
this._server.removeAllListeners('request');
|
||||||
this.hasOn = true;
|
this.hasOn = true;
|
||||||
if (Array.isArray(listener)) {
|
if (Array.isArray(listener)) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { WebSocketServer, WebSocket } from 'ws';
|
// @ts-type=ws
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import type { WebSocket } from 'ws';
|
||||||
import { Server } from './server.ts';
|
import { Server } from './server.ts';
|
||||||
import { parseIfJson } from '../utils/parse.ts';
|
import { parseIfJson } from '../utils/parse.ts';
|
||||||
|
|
||||||
|
|
||||||
export const createWsServer = (server: Server) => {
|
export const createWsServer = (server: Server) => {
|
||||||
// 将 WebSocket 服务器附加到 HTTP 服务器
|
// 将 WebSocket 服务器附加到 HTTP 服务器
|
||||||
const wss = new WebSocketServer({ server: server.server });
|
const wss = new WebSocketServer({ server: server.server as any });
|
||||||
return wss;
|
return wss;
|
||||||
};
|
};
|
||||||
type WsServerBaseOpts = {
|
type WsServerBaseOpts = {
|
||||||
@@ -23,7 +26,10 @@ export class WsServerBase {
|
|||||||
listeners: { type: string; listener: ListenerFn }[] = [];
|
listeners: { type: string; listener: ListenerFn }[] = [];
|
||||||
listening: boolean = false;
|
listening: boolean = false;
|
||||||
constructor(opts: WsServerBaseOpts) {
|
constructor(opts: WsServerBaseOpts) {
|
||||||
this.wss = opts.wss || new WebSocketServer();
|
this.wss = opts.wss;
|
||||||
|
if (!this.wss) {
|
||||||
|
throw new Error('wss is required');
|
||||||
|
}
|
||||||
this.path = opts.path || '';
|
this.path = opts.path || '';
|
||||||
}
|
}
|
||||||
setPath(path: string) {
|
setPath(path: string) {
|
||||||
@@ -37,10 +43,11 @@ export class WsServerBase {
|
|||||||
this.listening = true;
|
this.listening = true;
|
||||||
|
|
||||||
this.wss.on('connection', (ws) => {
|
this.wss.on('connection', (ws) => {
|
||||||
ws.on('message', async (message: string) => {
|
ws.on('message', async (message: string | Buffer) => {
|
||||||
const data = parseIfJson(message);
|
const data = parseIfJson(message);
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
ws.emit('string', data);
|
const cleanMessage = data.trim().replace(/^["']|["']$/g, '');
|
||||||
|
ws.emit('string', cleanMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { type, data: typeData, ...rest } = data;
|
const { type, data: typeData, ...rest } = data;
|
||||||
@@ -77,7 +84,7 @@ export class WsServerBase {
|
|||||||
if (message === 'close') {
|
if (message === 'close') {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
if (message === 'ping') {
|
if (message == 'ping') {
|
||||||
ws.send('pong');
|
ws.send('pong');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
59
src/sign.ts
Normal file
59
src/sign.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { generate } from 'selfsigned';
|
||||||
|
|
||||||
|
export type Attributes = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
export type AltNames = {
|
||||||
|
type: number;
|
||||||
|
value?: string;
|
||||||
|
ip?: string;
|
||||||
|
};
|
||||||
|
export const createCert = (attrs: Attributes[] = [], altNames: AltNames[] = []) => {
|
||||||
|
let attributes = [
|
||||||
|
{ name: 'countryName', value: 'CN' }, // 国家代码
|
||||||
|
{ name: 'stateOrProvinceName', value: 'ZheJiang' }, // 州名
|
||||||
|
{ name: 'localityName', value: 'HangZhou' }, // 城市名
|
||||||
|
{ name: 'organizationName', value: 'kevisual' }, // 组织名
|
||||||
|
{ name: 'organizationalUnitName', value: 'kevisual' }, // 组织单位
|
||||||
|
...attrs,
|
||||||
|
];
|
||||||
|
// attribute 根据name去重复, 后面的覆盖前面的
|
||||||
|
attributes = Object.values(
|
||||||
|
attributes.reduce(
|
||||||
|
(acc, attr) => ({
|
||||||
|
...acc,
|
||||||
|
[attr.name]: attr,
|
||||||
|
}),
|
||||||
|
{} as Record<string, Attributes>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
days: 365, // 证书有效期(天)
|
||||||
|
extensions: [
|
||||||
|
{
|
||||||
|
name: 'subjectAltName',
|
||||||
|
altNames: [
|
||||||
|
{ type: 2, value: '*' }, // DNS 名称
|
||||||
|
{ type: 2, value: 'localhost' }, // DNS
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
value: '[::1]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 7,
|
||||||
|
ip: 'fe80::1',
|
||||||
|
},
|
||||||
|
{ type: 7, ip: '127.0.0.1' }, // IP 地址
|
||||||
|
...altNames,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const pems = generate(attributes, options);
|
||||||
|
return {
|
||||||
|
key: pems.private,
|
||||||
|
cert: pems.cert,
|
||||||
|
};
|
||||||
|
};
|
||||||
17
src/test/chat.ts
Normal file
17
src/test/chat.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { App } from '../app.ts'
|
||||||
|
import { RouterChat } from '@/chat.ts';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
app.prompt(`获取时间的工具`).define(async (ctx) => {
|
||||||
|
ctx.body = '123'
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
app.prompt('获取天气的工具。\n参数是 city 为对应的城市').define(async (ctx) => {
|
||||||
|
ctx.body = '晴天'
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
|
||||||
|
export const chat = new RouterChat({ router: app.router });
|
||||||
|
|
||||||
|
console.log(chat.chat());
|
||||||
14
src/test/define.ts
Normal file
14
src/test/define.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { App } from '@/app.ts';
|
||||||
|
import { QueryUtil } from '@/router-define.ts';
|
||||||
|
const v = QueryUtil.create({
|
||||||
|
a: {
|
||||||
|
path: 'a',
|
||||||
|
key: 'b',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const app = new App();
|
||||||
|
app.route(v.get('a'));
|
||||||
|
|
||||||
|
v.chain('a').define<{ f: () => {} }>(async (ctx) => {
|
||||||
|
// ctx.f = 'sdf';
|
||||||
|
});
|
||||||
22
src/test/static.ts
Normal file
22
src/test/static.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
|
||||||
|
initProxy({
|
||||||
|
pagesDir: './demo',
|
||||||
|
watch: true,
|
||||||
|
});
|
||||||
|
import { App } from '../app.ts';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'a',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '1';
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app.listen(2233, () => {
|
||||||
|
console.log('Server is running on http://localhost:2233');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.onServerRequest(proxyRoute);
|
||||||
25
src/test/ws.ts
Normal file
25
src/test/ws.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { App } from "../app.ts";
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
io: true
|
||||||
|
});
|
||||||
|
|
||||||
|
app
|
||||||
|
.route('demo', '03')
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '03';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app
|
||||||
|
.route('test', 'test')
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'test';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
console.log(`http://localhost:4002/api/router?path=demo&key=03`);
|
||||||
|
|
||||||
|
app.listen(4002, () => {
|
||||||
|
console.log("Server started on http://localhost:4002");
|
||||||
|
});
|
||||||
15
src/utils/is-engine.ts
Normal file
15
src/utils/is-engine.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
||||||
|
export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof document.createElement === 'function';
|
||||||
|
// @ts-ignore
|
||||||
|
export const isDeno = typeof Deno !== 'undefined' && typeof Deno.version === 'object' && typeof Deno.version.deno === 'string';
|
||||||
|
|
||||||
|
export const getEngine = () => {
|
||||||
|
if (isNode) {
|
||||||
|
return 'node';
|
||||||
|
} else if (isBrowser) {
|
||||||
|
return 'browser';
|
||||||
|
} else if (isDeno) {
|
||||||
|
return 'deno';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
50
src/utils/listen-process.ts
Normal file
50
src/utils/listen-process.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export type ListenProcessOptions = {
|
||||||
|
app?: any; // 传入的应用实例
|
||||||
|
emitter?: any; // 可选的事件发射器
|
||||||
|
params?: any; // 可选的参数
|
||||||
|
timeout?: number; // 可选的超时时间 (单位: 毫秒)
|
||||||
|
};
|
||||||
|
export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
|
||||||
|
const process = emitter || globalThis.process;
|
||||||
|
let isEnd = false;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (isEnd) return;
|
||||||
|
isEnd = true;
|
||||||
|
process.send?.({ success: false, error: 'Timeout' });
|
||||||
|
process.exit?.(1);
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
// 监听来自主进程的消息
|
||||||
|
const getParams = async (): Promise<any> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
process.on('message', (msg) => {
|
||||||
|
if (isEnd) return;
|
||||||
|
isEnd = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(msg)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { path = 'main', ...rest } = await getParams()
|
||||||
|
// 执行主要逻辑
|
||||||
|
const result = await app.queryRoute({ path, ...rest, ...params })
|
||||||
|
// 发送结果回主进程
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.send?.(response, (error) => {
|
||||||
|
process.exit?.(0)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
process.send?.({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
process.exit?.(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
export const parseIfJson = (input: string): { [key: string]: any } | string => {
|
export const parseIfJson = (input: string|Buffer): { [key: string]: any } | string => {
|
||||||
|
const str = typeof input === 'string' ? input : input.toString();
|
||||||
try {
|
try {
|
||||||
// 尝试解析 JSON
|
// 尝试解析 JSON
|
||||||
const parsed = JSON.parse(input);
|
const parsed = JSON.parse(str);
|
||||||
// 检查解析结果是否为对象(数组或普通对象)
|
// 检查解析结果是否为对象(数组或普通对象)
|
||||||
if (typeof parsed === 'object' && parsed !== null) {
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -9,5 +10,5 @@ export const parseIfJson = (input: string): { [key: string]: any } | string => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果解析失败,直接返回原始字符串
|
// 如果解析失败,直接返回原始字符串
|
||||||
}
|
}
|
||||||
return input;
|
return str;
|
||||||
};
|
};
|
||||||
|
|||||||
47
src/utils/route-map.ts
Normal file
47
src/utils/route-map.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export type RouteMapInfo = {
|
||||||
|
pathKey?: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RouteMap {
|
||||||
|
private keyMap: Map<string, RouteMapInfo> = new Map(); // 通过 path key 查找
|
||||||
|
private idMap: Map<string, RouteMapInfo> = new Map(); // 通过 id 查找
|
||||||
|
// 添加数据
|
||||||
|
add(info: RouteMapInfo) {
|
||||||
|
if (!info.pathKey && !info.id) {
|
||||||
|
console.error('appKey 和 appId 不能同时为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.keyMap.set(info.pathKey, info);
|
||||||
|
if (info.id) {
|
||||||
|
this.idMap.set(info.id, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除数据
|
||||||
|
removeByKey(key: string) {
|
||||||
|
const info = this.keyMap.get(key);
|
||||||
|
if (info) {
|
||||||
|
this.keyMap.delete(info.pathKey);
|
||||||
|
this.idMap.delete(info.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeByAppId(appId: string) {
|
||||||
|
const info = this.idMap.get(appId);
|
||||||
|
if (info) {
|
||||||
|
this.keyMap.delete(info.pathKey);
|
||||||
|
this.idMap.delete(info.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 查询数据
|
||||||
|
getByKey(key: string): RouteMapInfo | undefined {
|
||||||
|
return this.keyMap.get(key);
|
||||||
|
}
|
||||||
|
getByAppId(appId: string): RouteMapInfo | undefined {
|
||||||
|
return this.idMap.get(appId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,6 @@
|
|||||||
export * from './rule.ts';
|
import { z } from 'zod';
|
||||||
|
export { schemaFormRule, createSchema, createSchemaList } from './rule.ts';
|
||||||
|
|
||||||
|
export type { Rule } from './rule.ts';
|
||||||
|
|
||||||
|
export type Schema = z.ZodType<any, any, any>;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { z, ZodError, Schema } from 'zod';
|
import { z, ZodError } from 'zod';
|
||||||
export { Schema };
|
|
||||||
type BaseRule = {
|
type BaseRule = {
|
||||||
value?: any;
|
value?: any;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -8,8 +7,8 @@ type BaseRule = {
|
|||||||
|
|
||||||
type RuleString = {
|
type RuleString = {
|
||||||
type: 'string';
|
type: 'string';
|
||||||
minLength?: number;
|
min?: number;
|
||||||
maxLength?: number;
|
max?: number;
|
||||||
regex?: string;
|
regex?: string;
|
||||||
} & BaseRule;
|
} & BaseRule;
|
||||||
|
|
||||||
@@ -26,8 +25,6 @@ type RuleBoolean = {
|
|||||||
type RuleArray = {
|
type RuleArray = {
|
||||||
type: 'array';
|
type: 'array';
|
||||||
items: Rule;
|
items: Rule;
|
||||||
minItems?: number;
|
|
||||||
maxItems?: number;
|
|
||||||
} & BaseRule;
|
} & BaseRule;
|
||||||
|
|
||||||
type RuleObject = {
|
type RuleObject = {
|
||||||
@@ -45,8 +42,8 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
|||||||
switch (rule.type) {
|
switch (rule.type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
let stringSchema = z.string();
|
let stringSchema = z.string();
|
||||||
if (rule.minLength) stringSchema = stringSchema.min(rule.minLength, `String must be at least ${rule.minLength} characters long.`);
|
if (rule.min) stringSchema = stringSchema.min(rule.min, `String must be at least ${rule.min} characters long.`);
|
||||||
if (rule.maxLength) stringSchema = stringSchema.max(rule.maxLength, `String must not exceed ${rule.maxLength} characters.`);
|
if (rule.max) stringSchema = stringSchema.max(rule.max, `String must not exceed ${rule.max} characters.`);
|
||||||
if (rule.regex) stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
if (rule.regex) stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
||||||
return stringSchema;
|
return stringSchema;
|
||||||
case 'number':
|
case 'number':
|
||||||
@@ -66,11 +63,14 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
|||||||
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const createSchema = (rule: Rule): Schema => {
|
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
|
||||||
try {
|
try {
|
||||||
rule.required = rule.required || false;
|
rule.required = rule.required ?? false;
|
||||||
if (!rule.required) {
|
if (!rule.required) {
|
||||||
return schemaFormRule(rule).nullable();
|
// nullable is null
|
||||||
|
// nullish is null or undefined
|
||||||
|
// optional is undefined
|
||||||
|
return schemaFormRule(rule).optional();
|
||||||
}
|
}
|
||||||
return schemaFormRule(rule);
|
return schemaFormRule(rule);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types",
|
"node_modules/@types",
|
||||||
],
|
],
|
||||||
"declaration": true,
|
"declaration": false,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
@@ -22,10 +22,14 @@
|
|||||||
"@/*": [
|
"@/*": [
|
||||||
"src/*"
|
"src/*"
|
||||||
],
|
],
|
||||||
|
"@kevisual/router": [
|
||||||
|
"src/index.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts",
|
||||||
|
"mod.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
Reference in New Issue
Block a user