Compare commits

...

55 Commits

Author SHA1 Message Date
87068cd626 update 2025-10-24 21:15:02 +08:00
ac32ff9d4a udpate 2025-10-24 03:04:06 +08:00
cc74dc6803 update add no path and random path 2025-10-22 17:05:20 +08:00
24166f9899 feat: add Mini 2025-10-15 21:13:37 +08:00
10506503eb update: add listen process 2025-10-15 20:48:20 +08:00
2483205a22 update 2025-10-14 19:17:00 +08:00
cd96b53f6e recorver code 2025-10-14 19:15:18 +08:00
19c4cc2e06 update 2025-10-14 15:17:56 +08:00
27b63e0a2b update 2025-10-14 15:16:41 +08:00
e8794912b6 add introduce 2025-07-26 20:38:49 +08:00
187900ad55 update: fix docs 2025-06-20 16:20:56 +08:00
d2ebb5f488 fix: add app-barowser for browser 2025-06-12 15:02:16 +08:00
324c4e9862 update sign 2025-06-06 19:23:52 +08:00
51305b71c3 fix: cert fix 2025-06-05 19:24:14 +08:00
adaf954ae7 bump: and export src 2025-06-05 00:47:53 +08:00
d2a03fc959 temp 2025-05-25 00:05:20 +08:00
e31b19f931 fix: update 2025-05-18 02:49:45 +08:00
eede990ec8 perf: 优化 2025-05-15 22:32:05 +08:00
51dafe0f9a perf 2025-05-14 21:12:08 +08:00
aeeb205e1e fix: fix types 2025-05-14 21:04:25 +08:00
fc35b531e1 feat: add define tools 2025-05-14 20:18:58 +08:00
6d148e47f1 add parse search value 2025-05-12 04:32:26 +08:00
896f2f5412 fix 2025-04-18 18:27:19 +08:00
afad59e0ab feat: update to 0.0.12 2025-04-18 01:16:06 +08:00
17e515ad32 add deno mod 2025-04-17 23:54:21 +08:00
4c11a44b48 update package 2025-04-03 19:37:21 +08:00
06c3cc4236 router writeHead修改 2025-03-30 00:37:31 +08:00
8823c15aba feat: 更新注释 2025-03-21 12:44:57 +08:00
0e30dfc6ea test path-to-regexp 2025-03-16 23:54:57 +08:00
e8f7f61e09 fix: add json needSerialize 2025-03-06 22:45:11 +08:00
5911f29c8f fix; fix add serialize 2025-03-03 23:20:18 +08:00
ba7e00bd7a router 更新 2025-03-02 16:51:07 +08:00
bac3e5b393 feat: add router 2025-03-02 02:18:45 +08:00
de3187f5f3 temp 2025-02-26 12:33:34 +08:00
1c3e65df8a feat: add metadata and middleware add preivew middleware 2025-02-26 11:26:52 +08:00
986b5687c4 bump: 0.0.6 2025-02-20 23:35:53 +08:00
803687b219 style 2025-02-19 07:03:09 +08:00
871aac104a fix: import fix 2025-01-03 21:23:23 +08:00
50c87043c8 type 定义修改 2024-12-14 08:52:42 +08:00
dc2f282f4b feat: 更新cookie模块和添加res和req在app的请求当中 2024-12-11 14:28:46 +08:00
c99d03550e fix: add http2 2024-11-27 18:49:55 +08:00
e0c7d40a9c add queryRoute for only return code data message 2024-11-26 19:25:43 +08:00
7f369b7b07 fix: 更新router报错 2024-11-17 16:43:24 +08:00
2393dfe273 fix: remove default res 2024-11-16 23:16:41 +08:00
5e38740c7b feat: add simple router 2024-11-16 23:10:54 +08:00
926c0a09cd feat: 更新sign的模块 2024-11-16 12:34:08 +08:00
780d744a16 feat: 设置默认的context 2024-11-12 11:19:11 +08:00
303c579e92 router添加 throw 2024-11-10 19:03:56 +08:00
df737e5f27 feat: 修改打包方式 2024-11-02 22:23:06 +08:00
c462dc31f8 feat: browser router add custom error 2024-11-01 00:23:21 +08:00
d6eb8393e0 feat: remove options context 2024-10-31 00:56:57 +08:00
52f5f58baf feat: add QueryRouterServer 2024-10-31 00:45:12 +08:00
158b12d811 feat: add export and import 2024-10-25 19:41:01 +08:00
6f0faba703 fix: change cors 2024-10-18 01:26:04 +08:00
041432baea fix: add default key 2024-10-17 01:01:49 +08:00
67 changed files with 2934 additions and 1001 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
node_modules
src/app.config.json5
dist
.turbo
https-cert.pem
https-key.pem

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

20
auto.ts Normal file
View 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
View File

@@ -0,0 +1 @@
abc

1
demo/simple/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
pem

View File

@@ -9,41 +9,49 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@abearxiong/router": "../.."
"@kevisual/router": "../.."
},
"devDependencies": {
"cookie": "^1.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
},
"../..": {
"name": "@kevisual/router",
"version": "0.0.2",
"license": "ISC",
"version": "0.0.23",
"license": "MIT",
"dependencies": {
"ws": "^8.18.0"
"path-to-regexp": "^8.2.0",
"selfsigned": "^2.4.1",
"send": "^1.2.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.4",
"@rollup/plugin-typescript": "^12.1.0",
"@kevisual/local-proxy": "^0.0.3",
"@kevisual/query": "^0.0.29",
"@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/node": "^22.5.5",
"@types/ws": "^8.5.12",
"@types/node": "^24.0.3",
"@types/send": "^0.17.5",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"cookie": "^1.0.2",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.7",
"rollup": "^4.22.4",
"ts-loader": "^9.5.1",
"nanoid": "^5.1.5",
"rollup": "^4.44.0",
"rollup-plugin-dts": "^6.2.1",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"zod": "^3.23.8"
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"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": {
"version": "0.8.1",
"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"
}
},
"node_modules/@kevisual/router": {
"resolved": "../..",
"link": true
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.11.tgz",
@@ -157,6 +169,16 @@
"dev": true,
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",

View File

@@ -11,9 +11,10 @@
"license": "ISC",
"description": "",
"dependencies": {
"@abearxiong/router": "../.."
"@kevisual/router": "../.."
},
"devDependencies": {
"cookie": "^1.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}

View File

@@ -1,4 +1,4 @@
import { App } from '@abearxiong/router';
import { App } from '@kevisual/router';
const app = new App();

View File

@@ -1,4 +1,4 @@
import { Route, App } from '@abearxiong/router';
import { Route, App } from '@kevisual/router';
const app = new App({ io: true });
app.listen(4002);

View File

@@ -1,4 +1,4 @@
import { Route, App } from '@abearxiong/router';
import { Route, App } from '@kevisual/router';
const app = new App();
app.listen(4003);

View 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);

View 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);

View 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);
});

View 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,
),
);

View 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');
});

View 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);

View 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'));

View File

@@ -1,4 +1,4 @@
import { QueryRouter, Route, Server } from '@abearxiong/router';
import { QueryRouter, Route, Server } from '@kevisual/router';
const router = new QueryRouter();

28
demo/simple/src/nopath.ts Normal file
View 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);
})

View 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);

View 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'));

View 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`);
});

View 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);
});

View File

@@ -1,4 +1,4 @@
import { Route, QueryRouter, RouteContext } from '@abearxiong/router';
import { Route, QueryRouter, RouteContext } from '@kevisual/router';
const qr = new QueryRouter();
qr.add(

View File

@@ -8,6 +8,7 @@
"allowJs": true,
"newLine": "LF",
"baseUrl": "./",
"rootDir": "./",
"typeRoots": [
"node_modules/@types",
],

13
mod.ts Normal file
View 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';

View File

@@ -1,47 +1,101 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router",
"version": "0.0.2",
"version": "0.0.31",
"description": "",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"main": "./dist/router.js",
"types": "./dist/router.d.ts",
"scripts": {
"build": "npm run clean && rollup -c",
"build:app": "npm run build && rsync dist/*browser* ../deploy/dist",
"watch": "rollup -c -w",
"clean": "rm -rf dist"
},
"files": [
"dist"
"dist",
"src",
"mod.ts"
],
"keywords": [],
"author": "",
"license": "ISC",
"author": "abearxiong",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.4",
"@rollup/plugin-typescript": "^12.1.0",
"@kevisual/local-proxy": "^0.0.6",
"@kevisual/query": "^0.0.29",
"@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/node": "^22.5.5",
"@types/ws": "^8.5.12",
"@types/node": "^24.9.1",
"@types/send": "^1.2.0",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
"cookie": "^1.0.2",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.7",
"rollup": "^4.22.4",
"ts-loader": "^9.5.1",
"nanoid": "^5.1.6",
"rollup": "^4.52.5",
"rollup-plugin-dts": "^6.2.3",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"zod": "^3.23.8"
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"ws": "npm:@kevisual/ws",
"xml2js": "^0.6.2",
"zod": "^4.1.12"
},
"repository": {
"type": "git",
"url": "git+https://github.com/abearxiong/kevisual-router.git"
},
"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/*"
}
}
}

View File

@@ -1,23 +1,15 @@
# router
```
```ts
import { App } from '@kevisual/router';
const app = new App();
app.listen(4002);
new app.Route('demo', '01')
.define(async (ctx) => {
ctx.body = '01';
return ctx;
})
.addTo(app);
app
.route({path:'demo', key: '02})
.define(async (ctx) => {
ctx.body = '02';
return ctx;
})
.addTo(app);
@@ -25,7 +17,20 @@ app
.route('demo', '03')
.define(async (ctx) => {
ctx.body = '03';
return ctx;
})
.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);
```

View File

@@ -3,22 +3,173 @@
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
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}
*/
export default {
input: 'src/index.ts', // TypeScript 入口文件
output: {
file: 'dist/index.js', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
export default [
{
input: 'src/index.ts', // TypeScript 入口文件
output: {
file: 'dist/router.js', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [
createAlias(),
resolve({
browser: false,
}), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
commonjs(),
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
],
},
plugins: [
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
// commonjs(),
typescript({
allowImportingTsExtensions: true,
noEmit: true,
}), // 使用 @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
View File

@@ -0,0 +1,5 @@
import { QueryRouterServer } from './browser.ts';
export const App = QueryRouterServer;
export * from './browser.ts';

View File

@@ -1,22 +1,27 @@
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';
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 = {}> = {
router?: QueryRouter;
server?: Server;
/** handle msg 关联 */
routerHandle?: RouterHandle;
routerContext?: RouteContext<T>;
serverOptions?: {
path?: string;
cors?: Cors;
handle?: any;
};
serverOptions?: ServerOpts;
io?: boolean;
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;
server: Server;
io: WsServer;
@@ -24,7 +29,7 @@ export class App<T = {}> {
const router = opts?.router || new QueryRouter();
const server = opts?.server || new Server(opts?.serverOptions || {});
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
router.setContext({ needSerialize: true, ...opts?.routerContext });
this.router = router;
this.server = server;
if (opts?.io) {
@@ -57,10 +62,10 @@ export class App<T = {}> {
add = this.addRoute;
Route = Route;
route(opts: RouteOpts): Route;
route(path: string, key?: string): Route;
route(path: string, opts?: RouteOpts): Route;
route(path: string, key?: string, opts?: RouteOpts): Route;
route(opts: RouteOpts): Route<U>;
route(path: string, key?: string): Route<U>;
route(path: string, opts?: RouteOpts): Route<U>;
route(path: string, key?: string, opts?: RouteOpts): Route<U>;
route(...args: any[]) {
const [path, key, opts] = args;
if (typeof path === 'object') {
@@ -77,8 +82,48 @@ export class App<T = {}> {
}
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;
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
View 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
View 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
View 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;
};

View 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);
}
};

View 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
View 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
View 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
View 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
View 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
View 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;
}
}

View File

@@ -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 type { RouteContext, RouteOpts } from './route.ts';
export type { RouteContext, RouteOpts, RouteMiddleware } 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 { Result } from './result/index.ts';
export { createSchema } from './validator/index.ts';
export { Rule, Schema, createSchema } from './validator/index.ts';
export type { Rule, Schema, } from './validator/index.ts';
export { App } from './app.ts';
export * from './router-define.ts';

View File

@@ -35,10 +35,22 @@ export class CustomError extends Error {
tips: e?.tips,
};
}
/**
* 判断 throw 的错误是否不是当前这个错误
* @param err
* @returns
*/
static isError(err: any) {
if (err instanceof CustomError || err?.code) {
return true;
}
return false;
}
parse(e?: CustomError) {
if (e) {
return CustomError.parseError(e);
} else {
const e = this;
return {
code: e?.code,
data: e?.data,

View File

@@ -1,45 +1 @@
export const Code400 = [
{
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,
};
};
export * from './error.ts';

View File

@@ -1,10 +1,11 @@
import { nanoid } from 'nanoid';
import { nanoid, random } from 'nanoid';
import { CustomError } from './result/error.ts';
import { Schema, Rule, createSchema } from './validator/index.ts';
import { pick } from './utils/pick.ts';
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> = {
// run first
query?: { [key: string]: any };
@@ -18,25 +19,56 @@ export type RouteContext<T = { code?: number }, S = any> = {
// 传递状态
state?: S;
// transfer data
/**
* 当前路径
*/
currentPath?: string;
/**
* 当前key
*/
currentKey?: string;
/**
* 当前route
*/
currentRoute?: Route;
progress?: [[string, string]][];
/**
* 进度
*/
progress?: [string, string][];
// onlyForNextRoute will be clear after next route
nextQuery?: { [key: string]: any };
// end
end?: boolean;
// 处理router manager
// TODO:
/**
* 请求 route的返回结果包函ctx
*/
queryRouter?: QueryRouter;
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;
throw?: (code?: number | string, message?: string, tips?: string) => void;
/** 是否需要序列化, 使用JSON.stringify和JSON.parse */
needSerialize?: boolean;
} & T;
export type Run<T = any> = (ctx?: RouteContext<T>) => Promise<typeof ctx | null | void>;
export type SimpleObject = Record<string, any>;
export type Run<T extends SimpleObject = {}> = (ctx: RouteContext<T>) => Promise<typeof ctx | null | void>;
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
export type RouteMiddleware =
| {
path: string;
key?: string;
id?: string;
}
| string;
export type RouteOpts = {
path?: string;
key?: string;
@@ -44,7 +76,8 @@ export type RouteOpts = {
run?: Run;
nextRoute?: NextRoute; // route to run after this route
description?: string;
middleware?: Route[] | string[]; // middleware
metadata?: { [key: string]: any };
middleware?: RouteMiddleware[]; // middleware
type?: 'route' | 'middleware';
/**
* validator: {
@@ -55,32 +88,55 @@ export type RouteOpts = {
* }
*/
validator?: { [key: string]: Rule };
schema?: { [key: string]: Schema<any> };
schema?: { [key: string]: any };
isVerify?: boolean;
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
/**
* $#$ will be used to split path and key
*/
idUsePath?: boolean;
/**
* id 合并的分隔符,默认为 $#$
*/
delimiter?: string;
isDebug?: boolean;
};
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 class Route {
export class Route<U = { [key: string]: any }> {
/**
* 一级路径
*/
path?: string;
/**
* 二级路径
*/
key?: string;
id?: string;
share? = false;
run?: Run;
nextRoute?: NextRoute; // route to run after this route
description?: string;
middleware?: (Route | string)[]; // middleware
metadata?: { [key: string]: any };
middleware?: RouteMiddleware[]; // middleware
type? = 'route';
private _validator?: { [key: string]: Rule };
schema?: { [key: string]: Schema<any> };
schema?: { [key: string]: any };
data?: any;
/**
* 是否需要验证
*/
isVerify?: boolean;
/**
* 是否开启debug开启后会打印错误信息
*/
isDebug?: boolean;
constructor(path: string, key: string = '', opts?: RouteOpts) {
constructor(path: string = '', key: string = '', opts?: RouteOpts) {
if (!path) {
path = nanoid(8)
}
path = path.trim();
key = key.trim();
this.path = path;
@@ -88,11 +144,13 @@ export class Route {
if (opts) {
this.id = opts.id || nanoid();
if (!opts.id && opts.idUsePath) {
this.id = path + '$#$' + key;
const delimiter = opts.delimiter ?? '$#$';
this.id = path + delimiter + key;
}
this.run = opts.run;
this.nextRoute = opts.nextRoute;
this.description = opts.description;
this.metadata = opts.metadata;
this.type = opts.type || 'route';
this.validator = opts.validator;
this.middleware = opts.middleware || [];
@@ -108,15 +166,19 @@ export class Route {
this.isDebug = opts?.isDebug ?? false;
}
private createSchema() {
const validator = this.validator;
const keys = Object.keys(validator || {});
const schemaList = keys.map((key) => {
return { [key]: createSchema(validator[key]) };
});
const schema = schemaList.reduce((prev, current) => {
return { ...prev, ...current };
}, {});
this.schema = schema;
try {
const validator = this.validator;
const keys = Object.keys(validator || {});
const schemaList = keys.map((key) => {
return { [key]: createSchema(validator[key]) };
});
const schema = schemaList.reduce((prev, current) => {
return { ...prev, ...current };
}, {});
this.schema = schema;
} catch (e) {
console.error('createSchema error:', e);
}
}
/**
@@ -202,16 +264,27 @@ export class Route {
this.validator = validator;
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>(fn: Run<T>): this;
define<T extends { [key: string]: any } = RouterContextT>(key: string, fn: Run<T>): this;
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, 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 & U>): this;
define<T extends { [key: string]: any } = RouterContextT>(path: string, key: string, fn: Run<T & U>): this;
define(...args: any[]) {
const [path, key, opts] = args;
// 全覆盖所以opts需要准确不能由idUsePath 需要check的变量
const setOpts = (opts: DefineRouteOpts) => {
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) {
if (!checkList.includes(item)) {
continue;
@@ -245,18 +318,44 @@ export class Route {
}
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);
}
setData(data: any) {
this.data = data;
return this;
}
throw(code?: number | string, message?: string, tips?: string): void;
throw(...args: any[]) {
throw new CustomError(...args);
}
}
export class QueryRouter {
routes: Route[];
maxNextRoute = 40;
context?: RouteContext = {}; // default context for call
constructor() {
this.routes = [];
}
@@ -265,7 +364,7 @@ export class QueryRouter {
const has = this.routes.find((r) => r.path === route.path && r.key === route.key);
if (has) {
// 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);
}
@@ -273,8 +372,8 @@ export class QueryRouter {
* remove route by path and key
* @param route
*/
remove(route: Route | { path: string; key: string }) {
this.routes = this.routes.filter((r) => r.path === route.path && r.key === route.key);
remove(route: Route | { path: string; key?: string }) {
this.routes = this.routes.filter((r) => r.path === route.path && r.key == route.key);
}
/**
* remove route by id
@@ -298,6 +397,12 @@ export class QueryRouter {
ctx.currentKey = key;
ctx.currentRoute = route;
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) {
ctx.code = 500;
ctx.message = 'Too many nextRoute';
@@ -307,28 +412,45 @@ export class QueryRouter {
// run middleware
if (route && route.middleware && route.middleware.length > 0) {
const errorMiddleware: { path?: string; key?: string; id?: string }[] = [];
// TODO: 向上递归执行动作, 暂时不考虑
const routeMiddleware = route.middleware.map((m) => {
let route: Route | undefined;
const isString = typeof m === 'string';
if (typeof m === 'string') {
route = this.routes.find((r) => r.id === m);
} else {
route = this.routes.find((r) => r.path === m.path && r.key === m.key);
}
if (!route) {
const getMiddleware = (m: Route) => {
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;
const isString = typeof item === 'string';
if (isString) {
errorMiddleware.push({
id: m as string,
});
} else
errorMiddleware.push({
path: m?.path,
key: m?.key,
route = this.routes.find((r) => r.id === item);
} else {
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 (isString) {
errorMiddleware.push({
id: item as string,
});
} else
errorMiddleware.push({
path: m?.path,
key: m?.key,
});
}
const routeMiddlewarePrevious = getMiddleware(route);
if (routeMiddlewarePrevious.length > 0) {
routeMiddleware.push(...routeMiddlewarePrevious);
}
routeMiddleware.push(route);
}
return route;
});
return routeMiddleware;
};
const routeMiddleware = getMiddleware(route);
if (errorMiddleware.length > 0) {
console.error('middleware not found');
ctx.body = errorMiddleware;
@@ -357,10 +479,11 @@ export class QueryRouter {
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:middlerware error');
console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
console.error('=====debug====:', e.message);
}
if (e instanceof CustomError) {
if (e instanceof CustomError || e?.code) {
ctx.code = e.code;
ctx.message = e.message;
ctx.body = null;
@@ -434,12 +557,10 @@ export class QueryRouter {
ctx.body = null;
return ctx;
}
ctx.query = ctx.nextQuery;
ctx.query = { ...ctx.query, ...ctx.nextQuery };
ctx.nextQuery = {};
return await this.runRoute(path, key, ctx);
}
// clear body
ctx.body = JSON.parse(JSON.stringify(ctx.body||''));
if (!ctx.code) ctx.code = 200;
return ctx;
} else {
@@ -459,39 +580,139 @@ export class QueryRouter {
*/
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
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.query = { ...ctx.query, ...query, ...payload };
ctx.state = {};
ctx.state = { ...ctx?.state };
ctx.throw = this.throw;
// put queryRouter to ctx
// TODO: 是否需要queryRouter函数内部处理router路由执行这应该是避免去内部去包含的功能过
ctx.queryRouter = this;
ctx.call = this.call.bind(this);
ctx.queryRoute = this.queryRoute.bind(this);
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 || ''));
}
return res;
}
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
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[] {
return this.routes.map((r) => {
return pick(r, pickValue as any);
});
}
/**
* 获取handle函数, 这里会去执行parse函数
*/
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
return async (msg: { path: string; key?: string; [key: string]: any }) => {
const context = { ...ctx };
const res = await router.parse(msg, context);
if (wrapperFn) {
res.data = res.body;
return wrapperFn(res, context);
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
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);
if (wrapperFn) {
res.data = res.body;
return wrapperFn(res, context);
}
const { code, body, message } = res;
return { code, data: body, message };
} catch (e) {
return { code: 500, message: e.message };
}
const { code, body, message } = res;
return { code, data: body, 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 = {
@@ -499,7 +720,7 @@ type QueryRouterServerOpts = {
context?: RouteContext;
};
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;
}
/**
@@ -511,8 +732,83 @@ export class QueryRouterServer extends QueryRouter {
constructor(opts?: QueryRouterServerOpts) {
super();
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
this.setContext({ needSerialize: false, ...opts?.context });
}
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
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
View 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
View File

@@ -0,0 +1,3 @@
import { parseXml } from './server/parse-xml.ts';
export { parseXml };

268
src/router-simple.ts Normal file
View 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;
}
}

View File

View 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 url from 'url';
import url from 'node:url';
import { createHandleCtx } from './server.ts';
/**
* get params and body
* 优先原则
* 1. 请求参数中的 payload 的token 优先
* 2. 请求头中的 authorization 优先
* 3. 请求头中的 cookie 优先
* @param req
* @param res
* @returns
@@ -20,6 +25,11 @@ export const handleServer = async (req: IncomingMessage, res: ServerResponse) =>
const parsedUrl = url.parse(req.url, true);
// 获取token
let token = req.headers['authorization'] || '';
const handle = createHandleCtx(req, res);
const cookies = handle.req.cookies;
if (!token) {
token = cookies.token; // cookie优先
}
if (token) {
token = token.replace('Bearer ', '');
}
@@ -41,6 +51,7 @@ export const handleServer = async (req: IncomingMessage, res: ServerResponse) =>
token,
...param,
...body,
cookies,
};
return data;
};

View File

@@ -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) => {
return new Promise((resolve, reject) => {
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);
@@ -9,10 +10,64 @@ export const parseBody = async (req: http.IncomingMessage) => {
req.on('end', () => {
try {
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) {
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
View 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);
}
});
});
};

View File

@@ -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 * as cookie from 'cookie';
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 = {
/**
* @default '*''
*/
origin?: string | undefined;
};
type ServerOpts = {
export type ServerOpts = {
/**path default `/api/router` */
path?: string;
/**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;
httpType?: 'http' | 'https' | 'http2';
httpsKey?: string;
httpsCert?: string;
};
export const resultError = (error: string, code = 500) => {
const r = {
@@ -26,15 +69,25 @@ export const resultError = (error: string, code = 500) => {
export class Server {
path = '/api/router';
private _server: http.Server;
private _server: http.Server | https.Server | http2.Http2SecureServer;
public handle: ServerOpts['handle'];
private _callback: any;
private cors: Cors;
private hasOn = false;
private httpType = 'http';
private options = {
key: '',
cert: '',
};
constructor(opts?: ServerOpts) {
this.path = opts?.path || '/api/router';
this.handle = opts?.handle;
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, listeningListener?: () => void): void;
@@ -45,11 +98,40 @@ export class Server {
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
listen(handle: any, listeningListener?: () => void): void;
listen(...args: any[]) {
this._server = http.createServer();
this._server = this.createServer();
const callback = this.createCallback();
this._server.on('request', callback);
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) {
this.handle = handle;
}
@@ -62,10 +144,10 @@ export class Server {
const handle = this.handle;
const cors = this.cors;
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
// only handle /api/router
if (req.url === '/favicon.ico') {
return;
}
if (res.headersSent) {
// 程序已经在其他地方响应了
return;
@@ -76,18 +158,15 @@ export class Server {
// 交给其他监听处理
return;
}
// res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (cors) {
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');
if (req.method === 'OPTIONS') {
res.end();
return;
}
}
res.writeHead(200); // 设置响应头给予其他api知道headersSent它已经被响应了
const url = req.url;
if (!url.startsWith(path)) {
res.end(resultError(`not path:[${path}]`));
@@ -99,7 +178,12 @@ export class Server {
return;
}
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') {
res.end(end);
} else {
@@ -107,6 +191,7 @@ export class Server {
}
} catch (e) {
console.error(e);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (e.code && typeof e.code === 'number') {
res.end(resultError(e.message || `Router Server error`, e.code));
} else {
@@ -129,7 +214,7 @@ export class Server {
* @param listener
*/
on(listener: Listener | Listener[]) {
this._server = this._server || http.createServer();
this._server = this._server || this.createServer();
this._server.removeAllListeners('request');
this.hasOn = true;
if (Array.isArray(listener)) {

View File

@@ -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 { parseIfJson } from '../utils/parse.ts';
export const createWsServer = (server: Server) => {
// 将 WebSocket 服务器附加到 HTTP 服务器
const wss = new WebSocketServer({ server: server.server });
const wss = new WebSocketServer({ server: server.server as any });
return wss;
};
type WsServerBaseOpts = {
@@ -23,7 +26,10 @@ export class WsServerBase {
listeners: { type: string; listener: ListenerFn }[] = [];
listening: boolean = false;
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 || '';
}
setPath(path: string) {
@@ -37,10 +43,11 @@ export class WsServerBase {
this.listening = true;
this.wss.on('connection', (ws) => {
ws.on('message', async (message: string) => {
ws.on('message', async (message: string | Buffer) => {
const data = parseIfJson(message);
if (typeof data === 'string') {
ws.emit('string', data);
const cleanMessage = data.trim().replace(/^["']|["']$/g, '');
ws.emit('string', cleanMessage);
return;
}
const { type, data: typeData, ...rest } = data;
@@ -77,7 +84,7 @@ export class WsServerBase {
if (message === 'close') {
ws.close();
}
if (message === 'ping') {
if (message == 'ping') {
ws.send('pong');
}
});

59
src/sign.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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';
};

View 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)
}
}

View File

@@ -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 {
// 尝试解析 JSON
const parsed = JSON.parse(input);
const parsed = JSON.parse(str);
// 检查解析结果是否为对象(数组或普通对象)
if (typeof parsed === 'object' && parsed !== null) {
return parsed;
@@ -9,5 +10,5 @@ export const parseIfJson = (input: string): { [key: string]: any } | string => {
} catch (e) {
// 如果解析失败,直接返回原始字符串
}
return input;
return str;
};

47
src/utils/route-map.ts Normal file
View 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);
}
}

View File

@@ -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>;

View File

@@ -1,5 +1,4 @@
import { z, ZodError, Schema } from 'zod';
export { Schema };
import { z, ZodError } from 'zod';
type BaseRule = {
value?: any;
required?: boolean;
@@ -8,8 +7,8 @@ type BaseRule = {
type RuleString = {
type: 'string';
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
regex?: string;
} & BaseRule;
@@ -26,8 +25,6 @@ type RuleBoolean = {
type RuleArray = {
type: 'array';
items: Rule;
minItems?: number;
maxItems?: number;
} & BaseRule;
type RuleObject = {
@@ -45,8 +42,8 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
switch (rule.type) {
case 'string':
let stringSchema = z.string();
if (rule.minLength) stringSchema = stringSchema.min(rule.minLength, `String must be at least ${rule.minLength} characters long.`);
if (rule.maxLength) stringSchema = stringSchema.max(rule.maxLength, `String must not exceed ${rule.maxLength} characters.`);
if (rule.min) stringSchema = stringSchema.min(rule.min, `String must be at least ${rule.min} characters long.`);
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');
return stringSchema;
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}`);
}
};
export const createSchema = (rule: Rule): Schema => {
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
try {
rule.required = rule.required || false;
rule.required = rule.required ?? false;
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);
} catch (e) {

View File

@@ -11,7 +11,7 @@
"typeRoots": [
"node_modules/@types",
],
"declaration": true,
"declaration": false,
"noEmit": true,
"allowImportingTsExtensions": true,
"moduleResolution": "NodeNext",
@@ -22,10 +22,14 @@
"@/*": [
"src/*"
],
"@kevisual/router": [
"src/index.ts"
]
}
},
"include": [
"src/**/*.ts"
"src/**/*.ts",
"mod.ts"
],
"exclude": [
"node_modules",

751
yarn.lock
View File

@@ -1,751 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
dependencies:
string-width "^5.1.2"
string-width-cjs "npm:string-width@^4.2.0"
strip-ansi "^7.0.1"
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@jridgewell/resolve-uri@^3.0.3":
version "3.1.2"
resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@rollup/plugin-commonjs@^26.0.1":
version "26.0.3"
resolved "https://registry.npmmirror.com/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.3.tgz#085ffb49818e43e4a2a96816a37affcc8a8cbaca"
integrity sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ==
dependencies:
"@rollup/pluginutils" "^5.0.1"
commondir "^1.0.1"
estree-walker "^2.0.2"
glob "^10.4.1"
is-reference "1.2.1"
magic-string "^0.30.3"
"@rollup/plugin-node-resolve@^15.2.4":
version "15.3.0"
resolved "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz#efbb35515c9672e541c08d59caba2eff492a55d5"
integrity sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==
dependencies:
"@rollup/pluginutils" "^5.0.1"
"@types/resolve" "1.20.2"
deepmerge "^4.2.2"
is-module "^1.0.0"
resolve "^1.22.1"
"@rollup/plugin-typescript@^12.1.0":
version "12.1.0"
resolved "https://registry.npmmirror.com/@rollup/plugin-typescript/-/plugin-typescript-12.1.0.tgz#2b357972091d1a8f71b8c9ce5a6e95fe1473bf77"
integrity sha512-Kzs8KGJofe7cfTRODsnG1jNGxSvU8gVoNNd7Z/QaY25AYwe2LSSUpx/kPxqF38NYkpR8de3m51r9uwJpDlz6dg==
dependencies:
"@rollup/pluginutils" "^5.1.0"
resolve "^1.22.1"
"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0":
version "5.1.2"
resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.2.tgz#d3bc9f0fea4fd4086aaac6aa102f3fa587ce8bd9"
integrity sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^2.3.1"
"@rollup/rollup-android-arm-eabi@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54"
integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==
"@rollup/rollup-android-arm64@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e"
integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==
"@rollup/rollup-darwin-arm64@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f"
integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==
"@rollup/rollup-darwin-x64@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724"
integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==
"@rollup/rollup-linux-arm-gnueabihf@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb"
integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==
"@rollup/rollup-linux-arm-musleabihf@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3"
integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==
"@rollup/rollup-linux-arm64-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496"
integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==
"@rollup/rollup-linux-arm64-musl@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065"
integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==
"@rollup/rollup-linux-powerpc64le-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d"
integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==
"@rollup/rollup-linux-riscv64-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983"
integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==
"@rollup/rollup-linux-s390x-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58"
integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==
"@rollup/rollup-linux-x64-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b"
integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==
"@rollup/rollup-linux-x64-musl@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127"
integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==
"@rollup/rollup-win32-arm64-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5"
integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==
"@rollup/rollup-win32-ia32-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2"
integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==
"@rollup/rollup-win32-x64-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818"
integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==
"@tsconfig/node10@^1.0.7":
version "1.0.11"
resolved "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==
"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.4"
resolved "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/estree@*", "@types/estree@1.0.6", "@types/estree@^1.0.0":
version "1.0.6"
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/lodash-es@^4.17.12":
version "4.17.12"
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.17.10"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6"
integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==
"@types/node@*", "@types/node@^22.5.5":
version "22.7.5"
resolved "https://registry.npmmirror.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b"
integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==
dependencies:
undici-types "~6.19.2"
"@types/resolve@1.20.2":
version "1.20.2"
resolved "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
"@types/ws@^8.5.12":
version "8.5.12"
resolved "https://registry.npmmirror.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
dependencies:
"@types/node" "*"
acorn-walk@^8.1.1:
version "8.3.4"
resolved "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7"
integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==
dependencies:
acorn "^8.11.0"
acorn@^8.11.0, acorn@^8.4.1:
version "8.12.1"
resolved "https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-regex@^6.0.1:
version "6.1.0"
resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
ansi-styles@^6.1.0:
version "6.2.1"
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
chalk@^4.1.0:
version "4.1.2"
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-spawn@^7.0.0:
version "7.0.3"
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
deepmerge@^4.2.2:
version "4.3.1"
resolved "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
enhanced-resolve@^5.0.0:
version "5.17.1"
resolved "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
foreground-child@^3.1.0:
version "3.3.0"
resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==
dependencies:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
glob@^10.4.1:
version "10.4.5"
resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
minimatch "^9.0.4"
minipass "^7.1.2"
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
graceful-fs@^4.2.4:
version "4.2.11"
resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
is-core-module@^2.13.0:
version "2.15.1"
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
dependencies:
hasown "^2.0.2"
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-reference@1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
dependencies:
"@types/estree" "*"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jackspeak@^3.1.2:
version "3.4.3"
resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lru-cache@^10.2.0:
version "10.4.3"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
magic-string@^0.30.3:
version "0.30.12"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.12.tgz#9eb11c9d072b9bcb4940a5b2c2e1a217e4ee1a60"
integrity sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
micromatch@^4.0.0:
version "4.0.8"
resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
minimatch@^9.0.4:
version "9.0.5"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
nanoid@^5.0.7:
version "5.0.7"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz#6452e8c5a816861fd9d2b898399f7e5fd6944cc6"
integrity sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==
package-json-from-dist@^1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-scurry@^1.11.1:
version "1.11.1"
resolved "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
dependencies:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
resolve@^1.22.1:
version "1.22.8"
resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
rollup@^4.22.4:
version "4.24.0"
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05"
integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.24.0"
"@rollup/rollup-android-arm64" "4.24.0"
"@rollup/rollup-darwin-arm64" "4.24.0"
"@rollup/rollup-darwin-x64" "4.24.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.24.0"
"@rollup/rollup-linux-arm-musleabihf" "4.24.0"
"@rollup/rollup-linux-arm64-gnu" "4.24.0"
"@rollup/rollup-linux-arm64-musl" "4.24.0"
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.0"
"@rollup/rollup-linux-riscv64-gnu" "4.24.0"
"@rollup/rollup-linux-s390x-gnu" "4.24.0"
"@rollup/rollup-linux-x64-gnu" "4.24.0"
"@rollup/rollup-linux-x64-musl" "4.24.0"
"@rollup/rollup-win32-arm64-msvc" "4.24.0"
"@rollup/rollup-win32-ia32-msvc" "4.24.0"
"@rollup/rollup-win32-x64-msvc" "4.24.0"
fsevents "~2.3.2"
semver@^7.3.4:
version "7.6.3"
resolved "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^4.0.1:
version "4.1.0"
resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
source-map@^0.7.4:
version "0.7.4"
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
dependencies:
eastasianwidth "^0.2.0"
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
ansi-regex "^6.0.1"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
ts-loader@^9.5.1:
version "9.5.1"
resolved "https://registry.npmmirror.com/ts-loader/-/ts-loader-9.5.1.tgz#63d5912a86312f1fbe32cef0859fb8b2193d9b89"
integrity sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==
dependencies:
chalk "^4.1.0"
enhanced-resolve "^5.0.0"
micromatch "^4.0.0"
semver "^7.3.4"
source-map "^0.7.4"
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tslib@^2.7.0:
version "2.7.0"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
typescript@^5.6.2:
version "5.6.3"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
ws@^8.18.0:
version "8.18.0"
resolved "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.npmmirror.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==