init router
This commit is contained in:
commit
733677f3f3
38
.github/workflows/publish.yml
vendored
Normal file
38
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
name: Publish to npm
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*' # 当推送带有版本号的 tag 时触发,例如 v1.0.0
|
||||||
|
workflow_dispatch: # 添加手动触发器
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Step 1: Clone current Git repository
|
||||||
|
- name: Checkout this repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
# Step 3: Setup Node.js and install dependencies
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '20.6'
|
||||||
|
registry-url: 'https://registry.npmjs.org/'
|
||||||
|
cache: 'npm' # 启用 npm 缓存,提高安装速度
|
||||||
|
- name: Configure npm authentication
|
||||||
|
run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
- name: Build project
|
||||||
|
run: npm run build
|
||||||
|
# Step 6: 发布到 npm
|
||||||
|
- name: Publish package
|
||||||
|
run: npm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
# Step 7: 发布成功后,更新版本标签
|
||||||
|
# - name: Create Git tag
|
||||||
|
# run: |
|
||||||
|
# TAG="v$(node -p -e "require('./package.json').version")"
|
||||||
|
# git tag $TAG
|
||||||
|
# git push origin $TAG
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
src/app.config.json5
|
||||||
|
dist
|
268
demo/simple/package-lock.json
generated
Normal file
268
demo/simple/package-lock.json
generated
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
{
|
||||||
|
"name": "simple",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "simple",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@abearxiong/router": "../.."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../..": {
|
||||||
|
"name": "@kevisual/router",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^26.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.4",
|
||||||
|
"@rollup/plugin-typescript": "^12.1.0",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^22.5.5",
|
||||||
|
"@types/ws": "^8.5.12",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
|
"rollup": "^4.22.4",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tslib": "^2.7.0",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.5.1.tgz",
|
||||||
|
"integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.12.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz",
|
||||||
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.3.tgz",
|
||||||
|
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.5.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.5.4.tgz",
|
||||||
|
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
demo/simple/package.json
Normal file
20
demo/simple/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "simple",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"app": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/app.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@abearxiong/router": "../.."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
|
}
|
||||||
|
}
|
74
demo/simple/simple-ws/index.html
Normal file
74
demo/simple/simple-ws/index.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebSocket with Redis</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Real-time Data Updates</h1>
|
||||||
|
<p id="output">Waiting for updates...</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// const ws = new WebSocket('ws://localhost:4002/api/router');
|
||||||
|
const ws = new WebSocket('ws://192.168.31.220:4002/api/router');
|
||||||
|
|
||||||
|
// 当连接成功时
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('Connected to WebSocket server');
|
||||||
|
|
||||||
|
// 订阅数据 ID 为 1 的更新
|
||||||
|
// const message = JSON.stringify({ type: 'subscribe', dataId: '1' });
|
||||||
|
// ws.send(message);
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'router',
|
||||||
|
data: {
|
||||||
|
path: 'demo',
|
||||||
|
key: '01',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws.send(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收服务器的消息
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const parseIfJson = (data) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const appendChild = (text) => {
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.innerText = text;
|
||||||
|
document.body.appendChild(t);
|
||||||
|
};
|
||||||
|
console.log('Received:', event.data);
|
||||||
|
const message = parseIfJson(event.data);
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
appendChild(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'router') {
|
||||||
|
const res = message.data;
|
||||||
|
const text = `Data Updated: ${JSON.stringify(res)}`;
|
||||||
|
appendChild(text);
|
||||||
|
console.log('Data updated:', res);
|
||||||
|
} else {
|
||||||
|
document.querySelector('#output').innerText = event.data;
|
||||||
|
console.log('Unknown message type:', message.type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 WebSocket 关闭
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('Disconnected from WebSocket server');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
41
demo/simple/src/app-02.ts
Normal file
41
demo/simple/src/app-02.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { App } from '@abearxiong/router';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
app.listen(4002, () => {
|
||||||
|
console.log('Server is running at http://localhost:4002');
|
||||||
|
});
|
||||||
|
const callback = (req, res) => {
|
||||||
|
if (req.url.startsWith('/api/v')) {
|
||||||
|
// 在这里处理 /api/v 的请求
|
||||||
|
// res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
setTimeout(() => {
|
||||||
|
res.end('Intercepted /api/v request');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.server.on(callback);
|
||||||
|
|
||||||
|
new app.Route('demo', '01')
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '01';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route('demo')
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '02';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route('demo', '03')
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '03';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
27
demo/simple/src/app-ws.ts
Normal file
27
demo/simple/src/app-ws.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Route, App } from '@abearxiong/router';
|
||||||
|
|
||||||
|
const app = new App({ io: true });
|
||||||
|
app.listen(4002);
|
||||||
|
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);
|
||||||
|
|
||||||
|
console.log(`http://localhost:4002/api/router?path=demo&key=02`);
|
||||||
|
console.log(`http://localhost:4002/api/router?path=demo&key=01`);
|
27
demo/simple/src/app.ts
Normal file
27
demo/simple/src/app.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Route, App } from '@abearxiong/router';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
app.listen(4003);
|
||||||
|
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);
|
||||||
|
|
||||||
|
console.log(`http://localhost:4003/api/router?path=demo&key=02`);
|
||||||
|
console.log(`http://localhost:4003/api/router?path=demo&key=01`);
|
36
demo/simple/src/index.ts
Normal file
36
demo/simple/src/index.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { QueryRouter, Route, Server } from '@abearxiong/router';
|
||||||
|
|
||||||
|
const router = new QueryRouter();
|
||||||
|
|
||||||
|
const route01 = new Route('demo', '01');
|
||||||
|
route01.run = async (ctx) => {
|
||||||
|
ctx.body = '01';
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
router.add(route01);
|
||||||
|
|
||||||
|
const server = new Server({
|
||||||
|
handle: async (msg) => {
|
||||||
|
const res = await router.parse(msg);
|
||||||
|
const { code, body, message } = res;
|
||||||
|
// console.log('response', res);
|
||||||
|
return { code, data: body, message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// server.setHandle(async (msg) => {
|
||||||
|
// const res = await router.parse(msg);
|
||||||
|
// const { code, body, message } = res;
|
||||||
|
// // console.log('response', res);
|
||||||
|
// return { code, data: body, message };
|
||||||
|
// });
|
||||||
|
|
||||||
|
server.listen(3000);
|
||||||
|
|
||||||
|
const route02 = new Route('demo', '02');
|
||||||
|
route02.run = async (ctx) => {
|
||||||
|
ctx.body = '02';
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
router.add(route02);
|
||||||
|
|
85
demo/simple/src/validator/app.ts
Normal file
85
demo/simple/src/validator/app.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Route, QueryRouter, RouteContext } from '@abearxiong/router';
|
||||||
|
const qr = new QueryRouter();
|
||||||
|
|
||||||
|
qr.add(
|
||||||
|
new Route('project', 'getList', {
|
||||||
|
description: 'get project list',
|
||||||
|
run: async (ctx) => {
|
||||||
|
ctx!.body = 'project list';
|
||||||
|
return ctx;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
qr.add(
|
||||||
|
new Route('project', 'getDetail', {
|
||||||
|
description: 'get project detail',
|
||||||
|
run: async (ctx) => {
|
||||||
|
ctx!.body = 'project detail';
|
||||||
|
return ctx;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
qr.add(
|
||||||
|
new Route('project', 'getDetail2', {
|
||||||
|
description: 'get project detail2',
|
||||||
|
run: async (ctx: RouteContext) => {
|
||||||
|
ctx!.body = 'project detail2';
|
||||||
|
return ctx;
|
||||||
|
},
|
||||||
|
validator: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
message: 'id is required',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
// @ts-ignore
|
||||||
|
type: 'object',
|
||||||
|
message: 'data query is error',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: 'name is required',
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
message: 'age is error',
|
||||||
|
},
|
||||||
|
friends: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
hair: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: 'hair is required',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
// 调用要测试的函数
|
||||||
|
const res = await qr.parse({
|
||||||
|
path: 'project',
|
||||||
|
key: 'getDetail2',
|
||||||
|
id: 4,
|
||||||
|
data: {
|
||||||
|
name: 'john',
|
||||||
|
age: 's'+13,
|
||||||
|
friends: {
|
||||||
|
hair: 'black',
|
||||||
|
messages: 'hello',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
console.log('test===', res);
|
||||||
|
};
|
||||||
|
main();
|
43
demo/simple/tsconfig.json
Normal file
43
demo/simple/tsconfig.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"target": "esnext",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"sourceMap": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"newLine": "LF",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types",
|
||||||
|
],
|
||||||
|
"declaration": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
"*": [
|
||||||
|
"types/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"typings.d.ts",
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"rollup.config.js",
|
||||||
|
],
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package",
|
||||||
|
"name": "@kevisual/router",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run clean && rollup -c",
|
||||||
|
"watch": "rollup -c -w",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^26.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.4",
|
||||||
|
"@rollup/plugin-typescript": "^12.1.0",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^22.5.5",
|
||||||
|
"@types/ws": "^8.5.12",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
|
"rollup": "^4.22.4",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tslib": "^2.7.0",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/abearxiong/kevisual-router.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
}
|
||||||
|
}
|
31
readme.md
Normal file
31
readme.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# router
|
||||||
|
|
||||||
|
```
|
||||||
|
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);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route('demo', '03')
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = '03';
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
```
|
24
rollup.config.js
Normal file
24
rollup.config.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// rollup.config.js
|
||||||
|
|
||||||
|
import typescript from '@rollup/plugin-typescript';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
/**
|
||||||
|
* @type {import('rollup').RollupOptions}
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
input: 'src/index.ts', // TypeScript 入口文件
|
||||||
|
output: {
|
||||||
|
file: 'dist/index.js', // 输出文件
|
||||||
|
format: 'es', // 输出格式设置为 ES 模块
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
|
// commonjs(),
|
||||||
|
typescript({
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
noEmit: true,
|
||||||
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
|
],
|
||||||
|
external: ['ws']
|
||||||
|
};
|
84
src/app.ts
Normal file
84
src/app.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
||||||
|
import { Server, Cors } 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 };
|
||||||
|
type AppOptions<T = {}> = {
|
||||||
|
router?: QueryRouter;
|
||||||
|
server?: Server;
|
||||||
|
/** handle msg 关联 */
|
||||||
|
routerHandle?: RouterHandle;
|
||||||
|
routerContext?: RouteContext<T>;
|
||||||
|
serverOptions?: {
|
||||||
|
path?: string;
|
||||||
|
cors?: Cors;
|
||||||
|
handle?: any;
|
||||||
|
};
|
||||||
|
io?: boolean;
|
||||||
|
ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string };
|
||||||
|
};
|
||||||
|
export class App<T = {}> {
|
||||||
|
router: QueryRouter;
|
||||||
|
server: Server;
|
||||||
|
io: WsServer;
|
||||||
|
constructor(opts?: AppOptions<T>) {
|
||||||
|
const router = opts?.router || new QueryRouter();
|
||||||
|
const server = opts?.server || new Server(opts?.serverOptions || {});
|
||||||
|
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
|
||||||
|
|
||||||
|
this.router = router;
|
||||||
|
this.server = server;
|
||||||
|
if (opts?.io) {
|
||||||
|
this.io = new WsServer(server, opts?.ioOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
||||||
|
listen(port: number, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(port: number, listeningListener?: () => void): void;
|
||||||
|
listen(path: string, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(path: string, listeningListener?: () => void): void;
|
||||||
|
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(handle: any, listeningListener?: () => void): void;
|
||||||
|
listen(...args: any[]) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.server.listen(...args);
|
||||||
|
if (this.io) {
|
||||||
|
this.io.listen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
|
||||||
|
const route = new Route(path, '', opts);
|
||||||
|
route.run = fn;
|
||||||
|
this.router.add(route);
|
||||||
|
}
|
||||||
|
addRoute(route: Route) {
|
||||||
|
this.router.add(route);
|
||||||
|
}
|
||||||
|
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(...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);
|
||||||
|
}
|
||||||
|
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
|
const router = this.router;
|
||||||
|
return await router.parse(message, ctx);
|
||||||
|
}
|
||||||
|
}
|
67
src/connect.ts
Normal file
67
src/connect.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { RouteContext } from './route.ts';
|
||||||
|
|
||||||
|
export class Connect {
|
||||||
|
path: string;
|
||||||
|
key?: string;
|
||||||
|
_fn?: (ctx?: RouteContext) => Promise<RouteContext>;
|
||||||
|
description?: string;
|
||||||
|
connects: { path: string; key?: string }[];
|
||||||
|
share = false;
|
||||||
|
|
||||||
|
constructor(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
this.key = nanoid();
|
||||||
|
}
|
||||||
|
use(path: string) {
|
||||||
|
this.connects.push({ path });
|
||||||
|
}
|
||||||
|
useList(paths: string[]) {
|
||||||
|
paths.forEach((path) => {
|
||||||
|
this.connects.push({ path });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
useConnect(connect: Connect) {
|
||||||
|
this.connects.push({ path: connect.path, key: connect.key });
|
||||||
|
}
|
||||||
|
useConnectList(connects: Connect[]) {
|
||||||
|
connects.forEach((connect) => {
|
||||||
|
this.connects.push({ path: connect.path, key: connect.key });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getPathList() {
|
||||||
|
return this.connects.map((c) => c.path).filter(Boolean);
|
||||||
|
}
|
||||||
|
set fn(fn: (ctx?: RouteContext) => Promise<RouteContext>) {
|
||||||
|
this._fn = fn;
|
||||||
|
}
|
||||||
|
get fn() {
|
||||||
|
return this._fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class QueryConnect {
|
||||||
|
connects: Connect[];
|
||||||
|
constructor() {
|
||||||
|
this.connects = [];
|
||||||
|
}
|
||||||
|
add(connect: Connect) {
|
||||||
|
const has = this.connects.find((c) => c.path === connect.path && c.key === connect.key);
|
||||||
|
if (has) {
|
||||||
|
// remove the old connect
|
||||||
|
console.log('[replace connect]:', connect.path, connect.key);
|
||||||
|
this.connects = this.connects.filter((c) => c.path !== connect.path && c.key !== connect.key);
|
||||||
|
}
|
||||||
|
this.connects.push(connect);
|
||||||
|
}
|
||||||
|
remove(connect: Connect) {
|
||||||
|
this.connects = this.connects.filter((c) => c.path !== connect.path && c.key !== connect.key);
|
||||||
|
}
|
||||||
|
getList() {
|
||||||
|
return this.connects.map((c) => {
|
||||||
|
return {
|
||||||
|
path: c.path,
|
||||||
|
key: c.key,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
21
src/index.ts
Normal file
21
src/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export { Route, QueryRouter, QueryRouterServer } from './route.ts';
|
||||||
|
export { Connect, QueryConnect } from './connect.ts';
|
||||||
|
|
||||||
|
export type { RouteContext, RouteOpts } from './route.ts';
|
||||||
|
|
||||||
|
export type { Run } from './route.ts';
|
||||||
|
|
||||||
|
export { Server, handleServer } from './server/index.ts';
|
||||||
|
/**
|
||||||
|
* 自定义错误
|
||||||
|
*/
|
||||||
|
export { CustomError } from './result/error.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回结果
|
||||||
|
*/
|
||||||
|
export { Result } from './result/index.ts';
|
||||||
|
|
||||||
|
export { Rule, Schema, createSchema } from './validator/index.ts';
|
||||||
|
|
||||||
|
export { App } from './app.ts';
|
6
src/io.ts
Normal file
6
src/io.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// TODO: Implement IOApp
|
||||||
|
export class IOApp {
|
||||||
|
constructor() {
|
||||||
|
console.log('IoApp');
|
||||||
|
}
|
||||||
|
}
|
67
src/result/error.ts
Normal file
67
src/result/error.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/** 自定义错误 */
|
||||||
|
export class CustomError extends Error {
|
||||||
|
code?: number;
|
||||||
|
data?: any;
|
||||||
|
message: string;
|
||||||
|
tips?: string;
|
||||||
|
constructor(code?: number | string, message?: string, tips?: string) {
|
||||||
|
super(message || String(code));
|
||||||
|
this.name = 'CustomError';
|
||||||
|
if (typeof code === 'number') {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
} else {
|
||||||
|
this.code = 500;
|
||||||
|
this.message = code;
|
||||||
|
}
|
||||||
|
this.tips = tips;
|
||||||
|
// 这一步可不写,默认会保存堆栈追踪信息到自定义错误构造函数之前,
|
||||||
|
// 而如果写成 `Error.captureStackTrace(this)` 则自定义错误的构造函数也会被保存到堆栈追踪信息
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
static fromCode(code?: number) {
|
||||||
|
return new this(code);
|
||||||
|
}
|
||||||
|
static fromErrorData(code?: number, data?: any) {
|
||||||
|
const error = new this(code);
|
||||||
|
error.data = data;
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
static parseError(e: CustomError) {
|
||||||
|
return {
|
||||||
|
code: e?.code,
|
||||||
|
data: e?.data,
|
||||||
|
message: e?.message,
|
||||||
|
tips: e?.tips,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
parse(e?: CustomError) {
|
||||||
|
if (e) {
|
||||||
|
return CustomError.parseError(e);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: e?.code,
|
||||||
|
data: e?.data,
|
||||||
|
message: e?.message,
|
||||||
|
tips: e?.tips,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
try {
|
||||||
|
//
|
||||||
|
} catch(e) {
|
||||||
|
if (e instanceof CustomError) {
|
||||||
|
const errorInfo = e.parse();
|
||||||
|
if (dev) {
|
||||||
|
return {
|
||||||
|
error: errorInfo,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return errorInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
45
src/result/index.ts
Normal file
45
src/result/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
518
src/route.ts
Normal file
518
src/route.ts
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
import { nanoid } 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';
|
||||||
|
|
||||||
|
export type RouterContextT = { code?: number; [key: string]: any };
|
||||||
|
export type RouteContext<T = { code?: number }, S = any> = {
|
||||||
|
// run first
|
||||||
|
query?: { [key: string]: any };
|
||||||
|
// response body
|
||||||
|
/** return body */
|
||||||
|
body?: number | string | Object;
|
||||||
|
/** return code */
|
||||||
|
code?: number;
|
||||||
|
/** return msg */
|
||||||
|
message?: string;
|
||||||
|
// 传递状态
|
||||||
|
state?: S;
|
||||||
|
// transfer data
|
||||||
|
currentPath?: string;
|
||||||
|
currentKey?: string;
|
||||||
|
currentRoute?: Route;
|
||||||
|
progress?: [[string, string]][];
|
||||||
|
// onlyForNextRoute will be clear after next route
|
||||||
|
nextQuery?: { [key: string]: any };
|
||||||
|
// end
|
||||||
|
end?: boolean;
|
||||||
|
// 处理router manager
|
||||||
|
// TODO:
|
||||||
|
queryRouter?: QueryRouter;
|
||||||
|
error?: any;
|
||||||
|
call?: (message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise<any>;
|
||||||
|
index?: number;
|
||||||
|
} & T;
|
||||||
|
|
||||||
|
export type Run<T = any> = (ctx?: RouteContext<T>) => Promise<typeof ctx | null | void>;
|
||||||
|
|
||||||
|
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
|
||||||
|
export type RouteOpts = {
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
id?: string;
|
||||||
|
run?: Run;
|
||||||
|
nextRoute?: NextRoute; // route to run after this route
|
||||||
|
description?: string;
|
||||||
|
middleware?: Route[] | string[]; // middleware
|
||||||
|
type?: 'route' | 'middleware';
|
||||||
|
/**
|
||||||
|
* validator: {
|
||||||
|
* packageName: {
|
||||||
|
* type: 'string',
|
||||||
|
* required: true,
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
validator?: { [key: string]: Rule };
|
||||||
|
schema?: { [key: string]: Schema<any> };
|
||||||
|
isVerify?: boolean;
|
||||||
|
verify?: (ctx?: RouteContext, dev?: boolean) => boolean;
|
||||||
|
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
|
||||||
|
idUsePath?: boolean;
|
||||||
|
isDebug?: boolean;
|
||||||
|
};
|
||||||
|
export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>;
|
||||||
|
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware'] as const;
|
||||||
|
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
|
||||||
|
export class Route {
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
id?: string;
|
||||||
|
share? = false;
|
||||||
|
run?: Run;
|
||||||
|
nextRoute?: NextRoute; // route to run after this route
|
||||||
|
description?: string;
|
||||||
|
middleware?: (Route | string)[]; // middleware
|
||||||
|
type? = 'route';
|
||||||
|
private _validator?: { [key: string]: Rule };
|
||||||
|
schema?: { [key: string]: Schema<any> };
|
||||||
|
data?: any;
|
||||||
|
isVerify?: boolean;
|
||||||
|
isDebug?: boolean;
|
||||||
|
constructor(path: string, key: string = '', opts?: RouteOpts) {
|
||||||
|
path = path.trim();
|
||||||
|
key = key.trim();
|
||||||
|
this.path = path;
|
||||||
|
this.key = key;
|
||||||
|
if (opts) {
|
||||||
|
this.id = opts.id || nanoid();
|
||||||
|
if (!opts.id && opts.idUsePath) {
|
||||||
|
this.id = path + '$#$' + key;
|
||||||
|
}
|
||||||
|
this.run = opts.run;
|
||||||
|
this.nextRoute = opts.nextRoute;
|
||||||
|
this.description = opts.description;
|
||||||
|
this.type = opts.type || 'route';
|
||||||
|
this.validator = opts.validator;
|
||||||
|
this.middleware = opts.middleware || [];
|
||||||
|
this.key = opts.key || key;
|
||||||
|
this.path = opts.path || path;
|
||||||
|
this.isVerify = opts.isVerify ?? true;
|
||||||
|
this.createSchema();
|
||||||
|
} else {
|
||||||
|
this.isVerify = true;
|
||||||
|
this.middleware = [];
|
||||||
|
this.id = nanoid();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set validator and create schema
|
||||||
|
* @param validator
|
||||||
|
*/
|
||||||
|
set validator(validator: { [key: string]: Rule }) {
|
||||||
|
this._validator = validator;
|
||||||
|
this.createSchema();
|
||||||
|
}
|
||||||
|
get validator() {
|
||||||
|
return this._validator || {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* has code, body, message in ctx, return ctx if has error
|
||||||
|
* @param ctx
|
||||||
|
* @param dev
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
verify(ctx: RouteContext, dev = false) {
|
||||||
|
const query = ctx.query || {};
|
||||||
|
const schema = this.schema || {};
|
||||||
|
const validator = this.validator;
|
||||||
|
const check = () => {
|
||||||
|
const queryKeys = Object.keys(validator);
|
||||||
|
for (let i = 0; i < queryKeys.length; i++) {
|
||||||
|
const key = queryKeys[i];
|
||||||
|
const value = query[key];
|
||||||
|
if (schema[key]) {
|
||||||
|
const result = schema[key].safeParse(value);
|
||||||
|
if (!result.success) {
|
||||||
|
const path = result.error.errors[0]?.path?.join?.('.properties.');
|
||||||
|
let message = 'Invalid params';
|
||||||
|
if (path) {
|
||||||
|
const keyS = `${key}.properties.${path}.message`;
|
||||||
|
message = get(validator, keyS, 'Invalid params') as any;
|
||||||
|
}
|
||||||
|
throw new CustomError(500, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Need to manully call return ctx fn and configure body, code, message
|
||||||
|
* @param key
|
||||||
|
* @param ctx
|
||||||
|
* @param dev
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
verifyKey(key: string, ctx: RouteContext, dev = false) {
|
||||||
|
const query = ctx.query || {};
|
||||||
|
const schema = this.schema || {};
|
||||||
|
const validator = this.validator;
|
||||||
|
const check = () => {
|
||||||
|
const value = query[key];
|
||||||
|
if (schema[key]) {
|
||||||
|
try {
|
||||||
|
schema[key].parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
if (dev) {
|
||||||
|
return {
|
||||||
|
message: validator[key].message || 'Invalid params',
|
||||||
|
path: this.path,
|
||||||
|
key: this.key,
|
||||||
|
error: e.message.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message: validator[key].message || 'Invalid params',
|
||||||
|
path: this.path,
|
||||||
|
key: this.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const checkRes = check();
|
||||||
|
return checkRes;
|
||||||
|
}
|
||||||
|
setValidator(validator: { [key: string]: Rule }) {
|
||||||
|
this.validator = validator;
|
||||||
|
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(...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'];
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (typeof path === 'object') {
|
||||||
|
setOpts(path);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (typeof path === 'function') {
|
||||||
|
this.run = path;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (typeof path === 'string' && typeof key === 'function') {
|
||||||
|
setOpts({ path, run: key });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (typeof path === 'string' && typeof key === 'string' && typeof opts === 'function') {
|
||||||
|
setOpts({ path, key, run: opts });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
addTo(router: QueryRouter | { add: (route: Route) => void; [key: string]: any }) {
|
||||||
|
router.add(this);
|
||||||
|
}
|
||||||
|
setData(data: any) {
|
||||||
|
this.data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryRouter {
|
||||||
|
routes: Route[];
|
||||||
|
maxNextRoute = 40;
|
||||||
|
constructor() {
|
||||||
|
this.routes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
add(route: Route) {
|
||||||
|
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.push(route);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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 by id
|
||||||
|
* @param uniqueId
|
||||||
|
*/
|
||||||
|
removeById(unique: string) {
|
||||||
|
this.routes = this.routes.filter((r) => r.id !== unique);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 执行route
|
||||||
|
* @param path
|
||||||
|
* @param key
|
||||||
|
* @param ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async runRoute(path: string, key: string, ctx?: RouteContext) {
|
||||||
|
const route = this.routes.find((r) => r.path === path && r.key === key);
|
||||||
|
const maxNextRoute = this.maxNextRoute;
|
||||||
|
ctx = (ctx || {}) as RouteContext;
|
||||||
|
ctx.currentPath = path;
|
||||||
|
ctx.currentKey = key;
|
||||||
|
ctx.currentRoute = route;
|
||||||
|
ctx.index = (ctx.index || 0) + 1;
|
||||||
|
if (ctx.index > maxNextRoute) {
|
||||||
|
ctx.code = 500;
|
||||||
|
ctx.message = 'Too many nextRoute';
|
||||||
|
ctx.body = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
|
if (isString) {
|
||||||
|
errorMiddleware.push({
|
||||||
|
id: m as string,
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
errorMiddleware.push({
|
||||||
|
path: m?.path,
|
||||||
|
key: m?.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
});
|
||||||
|
if (errorMiddleware.length > 0) {
|
||||||
|
console.error('middleware not found');
|
||||||
|
ctx.body = errorMiddleware;
|
||||||
|
ctx.message = 'middleware not found';
|
||||||
|
ctx.code = 404;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < routeMiddleware.length; i++) {
|
||||||
|
const middleware = routeMiddleware[i];
|
||||||
|
if (middleware) {
|
||||||
|
if (middleware?.isVerify) {
|
||||||
|
try {
|
||||||
|
middleware.verify(ctx);
|
||||||
|
} catch (e) {
|
||||||
|
if (middleware?.isDebug) {
|
||||||
|
console.error('=====debug====:', 'middleware verify error:', e.message);
|
||||||
|
}
|
||||||
|
ctx.message = e.message;
|
||||||
|
ctx.code = 500;
|
||||||
|
ctx.body = null;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await middleware.run(ctx);
|
||||||
|
} catch (e) {
|
||||||
|
if (route?.isDebug) {
|
||||||
|
console.error('=====debug====:middlerware error');
|
||||||
|
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
|
||||||
|
console.error('=====debug====:', e.message);
|
||||||
|
}
|
||||||
|
if (e instanceof CustomError) {
|
||||||
|
ctx.code = e.code;
|
||||||
|
ctx.message = e.message;
|
||||||
|
ctx.body = null;
|
||||||
|
} else {
|
||||||
|
console.error(`fn:${route.path}-${route.key}:${route.id}`);
|
||||||
|
console.error(`middleware:${middleware.path}-${middleware.key}:${middleware.id}`);
|
||||||
|
ctx.code = 500;
|
||||||
|
ctx.message = 'Internal Server Error';
|
||||||
|
ctx.body = null;
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
if (ctx.end) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// run route
|
||||||
|
if (route) {
|
||||||
|
if (route.run) {
|
||||||
|
if (route?.isVerify) {
|
||||||
|
try {
|
||||||
|
route.verify(ctx);
|
||||||
|
} catch (e) {
|
||||||
|
if (route?.isDebug) {
|
||||||
|
console.error('=====debug====:', 'verify error:', e.message);
|
||||||
|
}
|
||||||
|
ctx.message = e.message;
|
||||||
|
ctx.code = 500;
|
||||||
|
ctx.body = null;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await route.run(ctx);
|
||||||
|
} catch (e) {
|
||||||
|
if (route?.isDebug) {
|
||||||
|
console.error('=====debug====:', 'router run error:', e.message);
|
||||||
|
}
|
||||||
|
if (e instanceof CustomError) {
|
||||||
|
ctx.code = e.code;
|
||||||
|
ctx.message = e.message;
|
||||||
|
} else {
|
||||||
|
console.error(`[error]fn:${route.path}-${route.key}:${route.id}`);
|
||||||
|
console.error('error', e.message);
|
||||||
|
ctx.code = 500;
|
||||||
|
ctx.message = 'Internal Server Error';
|
||||||
|
}
|
||||||
|
ctx.body = null;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
if (ctx.end) {
|
||||||
|
// TODO: 提前结束, 以及错误情况
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route.nextRoute) {
|
||||||
|
let path: string, key: string;
|
||||||
|
if (route.nextRoute.path || route.nextRoute.key) {
|
||||||
|
path = route.nextRoute.path;
|
||||||
|
key = route.nextRoute.key;
|
||||||
|
} else if (route.nextRoute.id) {
|
||||||
|
const nextRoute = this.routes.find((r) => r.id === route.nextRoute.id);
|
||||||
|
if (nextRoute) {
|
||||||
|
path = nextRoute.path;
|
||||||
|
key = nextRoute.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!path || !key) {
|
||||||
|
ctx.message = 'nextRoute not found';
|
||||||
|
ctx.code = 404;
|
||||||
|
ctx.body = null;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
// return Promise.resolve({ code: 404, body: 'Not found runing' });
|
||||||
|
// 可以不需要run的route,因为不一定是错误
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有找到route,返回404,这是因为出现了错误
|
||||||
|
return Promise.resolve({ code: 404, body: 'Not found' });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 第一次执行
|
||||||
|
* @param message
|
||||||
|
* @param ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
const { path, key, payload = {}, ...query } = message;
|
||||||
|
ctx = ctx || {};
|
||||||
|
ctx.query = { ...ctx.query, ...query, ...payload };
|
||||||
|
ctx.state = {};
|
||||||
|
// put queryRouter to ctx
|
||||||
|
// TODO: 是否需要queryRouter,函数内部处理router路由执行,这应该是避免去内部去包含的功能过
|
||||||
|
ctx.queryRouter = this;
|
||||||
|
ctx.call = this.call.bind(this);
|
||||||
|
ctx.index = 0;
|
||||||
|
return await this.runRoute(path, key, ctx);
|
||||||
|
}
|
||||||
|
async call(message: { path: string; key: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
|
||||||
|
return await this.parse(message, ctx);
|
||||||
|
}
|
||||||
|
getList(): RouteInfo[] {
|
||||||
|
return this.routes.map((r) => {
|
||||||
|
return pick(r, pickValue as any);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
const { code, body, message } = res;
|
||||||
|
return { code, data: body, message };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryRouterServerOpts = {
|
||||||
|
handleFn?: HandleFn;
|
||||||
|
context?: RouteContext;
|
||||||
|
};
|
||||||
|
interface HandleFn<T = any> {
|
||||||
|
(msg: { path: string; [key: string]: any }, ctx?: any): { code: string; data?: any; message?: string; [key: string]: any };
|
||||||
|
(res: RouteContext<T>): any;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* QueryRouterServer
|
||||||
|
* @description 移除server相关的功能,只保留router相关的功能,和http.createServer不相关,独立
|
||||||
|
*/
|
||||||
|
export class QueryRouterServer extends QueryRouter {
|
||||||
|
handle: any;
|
||||||
|
constructor(opts?: QueryRouterServerOpts) {
|
||||||
|
super();
|
||||||
|
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
|
||||||
|
}
|
||||||
|
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
|
||||||
|
this.handle = this.getHandle(this, wrapperFn, ctx);
|
||||||
|
}
|
||||||
|
}
|
46
src/server/handle-server.ts
Normal file
46
src/server/handle-server.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import http, { IncomingMessage, Server, ServerResponse } from 'http';
|
||||||
|
import { parseBody } from './parse-body.ts';
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get params and body
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const handleServer = async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
if (req.url === '/favicon.ico') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const can = ['get', 'post'];
|
||||||
|
const method = req.method.toLocaleLowerCase();
|
||||||
|
if (!can.includes(method)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsedUrl = url.parse(req.url, true);
|
||||||
|
// 获取token
|
||||||
|
let token = req.headers['authorization'] || '';
|
||||||
|
if (token) {
|
||||||
|
token = token.replace('Bearer ', '');
|
||||||
|
}
|
||||||
|
// 获取查询参数
|
||||||
|
const param = parsedUrl.query;
|
||||||
|
let body: Record<any, any>;
|
||||||
|
if (method === 'post') {
|
||||||
|
body = await parseBody(req);
|
||||||
|
}
|
||||||
|
if (param?.payload && typeof param.payload === 'string') {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(param.payload as string);
|
||||||
|
param.payload = payload;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
token,
|
||||||
|
...param,
|
||||||
|
...body,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
};
|
2
src/server/index.ts
Normal file
2
src/server/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Server } from './server.ts';
|
||||||
|
export { handleServer } from './handle-server.ts';
|
18
src/server/parse-body.ts
Normal file
18
src/server/parse-body.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
|
export const parseBody = async (req: http.IncomingMessage) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
arr.push(chunk);
|
||||||
|
});
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const body = Buffer.concat(arr).toString();
|
||||||
|
resolve(JSON.parse(body));
|
||||||
|
} catch (e) {
|
||||||
|
resolve({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
148
src/server/server.ts
Normal file
148
src/server/server.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import http, { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { handleServer } from './handle-server.ts';
|
||||||
|
|
||||||
|
export type Listener = (...args: any[]) => void;
|
||||||
|
|
||||||
|
export type Cors = {
|
||||||
|
/**
|
||||||
|
* @default '*''
|
||||||
|
*/
|
||||||
|
origin?: string | undefined;
|
||||||
|
};
|
||||||
|
type ServerOpts = {
|
||||||
|
/**path default `/api/router` */
|
||||||
|
path?: string;
|
||||||
|
/**handle Fn */
|
||||||
|
handle?: (msg?: { path: string; key?: string; [key: string]: any }) => any;
|
||||||
|
cors?: Cors;
|
||||||
|
};
|
||||||
|
export const resultError = (error: string, code = 500) => {
|
||||||
|
const r = {
|
||||||
|
code: code,
|
||||||
|
message: error,
|
||||||
|
};
|
||||||
|
return JSON.stringify(r);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Server {
|
||||||
|
path = '/api/router';
|
||||||
|
private _server: http.Server;
|
||||||
|
public handle: ServerOpts['handle'];
|
||||||
|
private _callback: any;
|
||||||
|
private cors: Cors;
|
||||||
|
private hasOn = false;
|
||||||
|
constructor(opts?: ServerOpts) {
|
||||||
|
this.path = opts?.path || '/api/router';
|
||||||
|
this.handle = opts?.handle;
|
||||||
|
this.cors = opts?.cors;
|
||||||
|
}
|
||||||
|
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
||||||
|
listen(port: number, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(port: number, listeningListener?: () => void): void;
|
||||||
|
listen(path: string, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(path: string, listeningListener?: () => void): void;
|
||||||
|
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
||||||
|
listen(handle: any, listeningListener?: () => void): void;
|
||||||
|
listen(...args: any[]) {
|
||||||
|
this._server = http.createServer();
|
||||||
|
const callback = this.createCallback();
|
||||||
|
this._server.on('request', callback);
|
||||||
|
this._server.listen(...args);
|
||||||
|
}
|
||||||
|
setHandle(handle?: any) {
|
||||||
|
this.handle = handle;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get callback
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
createCallback() {
|
||||||
|
const path = this.path;
|
||||||
|
const handle = this.handle;
|
||||||
|
const cors = this.cors;
|
||||||
|
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
if (req.url === '/favicon.ico') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.headersSent) {
|
||||||
|
// 程序已经在其他地方响应了
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.hasOn && !req.url.startsWith(path)) {
|
||||||
|
// 其他监听存在,不判断不是当前路径的请求,
|
||||||
|
// 也就是不处理!url.startsWith(path)这个请求了
|
||||||
|
// 交给其他监听处理
|
||||||
|
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-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}]`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messages = await handleServer(req, res);
|
||||||
|
if (!handle) {
|
||||||
|
res.end(resultError('no handle'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const end = await handle(messages as any);
|
||||||
|
if (typeof end === 'string') {
|
||||||
|
res.end(end);
|
||||||
|
} else {
|
||||||
|
res.end(JSON.stringify(end));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (e.code && typeof e.code === 'number') {
|
||||||
|
res.end(resultError(e.message || `Router Server error`, e.code));
|
||||||
|
} else {
|
||||||
|
res.end(resultError('Router Server error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._callback = _callback;
|
||||||
|
return _callback;
|
||||||
|
}
|
||||||
|
get handleServer() {
|
||||||
|
return this._callback;
|
||||||
|
}
|
||||||
|
set handleServer(fn: any) {
|
||||||
|
this._callback = fn;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 兜底监听,当除开 `/api/router` 之外的请求,框架只监听一个api,所以有其他的请求都执行其他的监听
|
||||||
|
* @description 主要是为了兼容其他的监听
|
||||||
|
* @param listener
|
||||||
|
*/
|
||||||
|
on(listener: Listener | Listener[]) {
|
||||||
|
this._server = this._server || http.createServer();
|
||||||
|
this._server.removeAllListeners('request');
|
||||||
|
this.hasOn = true;
|
||||||
|
if (Array.isArray(listener)) {
|
||||||
|
listener.forEach((l) => this._server.on('request', l));
|
||||||
|
} else {
|
||||||
|
this._server.on('request', listener);
|
||||||
|
}
|
||||||
|
this._server.on('request', this._callback || this.createCallback());
|
||||||
|
}
|
||||||
|
get callback() {
|
||||||
|
return this._callback || this.createCallback();
|
||||||
|
}
|
||||||
|
get server() {
|
||||||
|
return this._server;
|
||||||
|
}
|
||||||
|
}
|
156
src/server/ws-server.ts
Normal file
156
src/server/ws-server.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { WebSocketServer, 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 });
|
||||||
|
return wss;
|
||||||
|
};
|
||||||
|
type WsServerBaseOpts = {
|
||||||
|
wss?: WebSocketServer;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
export type ListenerFn = (message: { data: Record<string, any>; ws: WebSocket; end: (data: any) => any }) => Promise<any>;
|
||||||
|
export type Listener<T = 'router' | 'chat' | 'ai'> = {
|
||||||
|
type: T;
|
||||||
|
listener: ListenerFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WsServerBase {
|
||||||
|
wss: WebSocketServer;
|
||||||
|
path: string;
|
||||||
|
listeners: { type: string; listener: ListenerFn }[] = [];
|
||||||
|
listening: boolean = false;
|
||||||
|
constructor(opts: WsServerBaseOpts) {
|
||||||
|
this.wss = opts.wss || new WebSocketServer();
|
||||||
|
this.path = opts.path || '';
|
||||||
|
}
|
||||||
|
setPath(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
listen() {
|
||||||
|
if (this.listening) {
|
||||||
|
console.error('WsServer is listening');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.listening = true;
|
||||||
|
|
||||||
|
this.wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', async (message: string) => {
|
||||||
|
const data = parseIfJson(message);
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
ws.emit('string', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { type, data: typeData, ...rest } = data;
|
||||||
|
if (!type) {
|
||||||
|
ws.send(JSON.stringify({ code: 500, message: 'type is required' }));
|
||||||
|
}
|
||||||
|
const listeners = this.listeners.find((item) => item.type === type);
|
||||||
|
const res = {
|
||||||
|
type,
|
||||||
|
data: {} as any,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
const end = (data: any, all?: Record<string, any>) => {
|
||||||
|
const result = {
|
||||||
|
...res,
|
||||||
|
data,
|
||||||
|
...all,
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(result));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!listeners) {
|
||||||
|
const data = { code: 500, message: `${type} server is error` };
|
||||||
|
end(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listeners.listener({
|
||||||
|
data: typeData,
|
||||||
|
ws,
|
||||||
|
end: end,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ws.on('string', (message: string) => {
|
||||||
|
if (message === 'close') {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
if (message === 'ping') {
|
||||||
|
ws.send('pong');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws.send('connected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addListener(type: string, listener: ListenerFn) {
|
||||||
|
if (!type || !listener) {
|
||||||
|
throw new Error('type and listener is required');
|
||||||
|
}
|
||||||
|
const find = this.listeners.find((item) => item.type === type);
|
||||||
|
if (find) {
|
||||||
|
this.listeners = this.listeners.filter((item) => item.type !== type);
|
||||||
|
}
|
||||||
|
this.listeners.push({ type, listener });
|
||||||
|
}
|
||||||
|
removeListener(type: string) {
|
||||||
|
this.listeners = this.listeners.filter((item) => item.type !== type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: ws handle and path and routerContext
|
||||||
|
export class WsServer extends WsServerBase {
|
||||||
|
server: Server;
|
||||||
|
constructor(server: Server, opts?: any) {
|
||||||
|
const wss = new WebSocketServer({ noServer: true });
|
||||||
|
const path = server.path;
|
||||||
|
super({ wss });
|
||||||
|
this.server = server;
|
||||||
|
this.setPath(opts?.path || path);
|
||||||
|
this.initListener();
|
||||||
|
}
|
||||||
|
initListener() {
|
||||||
|
const server = this.server;
|
||||||
|
const listener: Listener = {
|
||||||
|
type: 'router',
|
||||||
|
listener: async ({ data, ws, end }) => {
|
||||||
|
if (!server) {
|
||||||
|
end({ code: 500, message: 'server handle is error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handle = this.server.handle;
|
||||||
|
try {
|
||||||
|
const result = await handle(data as any);
|
||||||
|
end(result);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code && typeof e.code === 'number') {
|
||||||
|
end({
|
||||||
|
code: e.code,
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
end({ code: 500, message: 'Router Server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.addListener(listener.type, listener.listener);
|
||||||
|
}
|
||||||
|
listen() {
|
||||||
|
super.listen();
|
||||||
|
const server = this.server;
|
||||||
|
const wss = this.wss;
|
||||||
|
// HTTP 服务器的 upgrade 事件
|
||||||
|
server.server.on('upgrade', (req, socket, head) => {
|
||||||
|
if (req.url === this.path) {
|
||||||
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||||
|
// 这里手动触发 connection 事件
|
||||||
|
// @ts-ignore
|
||||||
|
wss.emit('connection', ws, req);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
97
src/static.ts
Normal file
97
src/static.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const fetch = require('node-fetch'); // 如果使用 Node.js 18 以上版本,可以改用内置 fetch
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
// 配置远端静态文件服务器和本地缓存目录
|
||||||
|
const remoteServer = 'https://example.com/static'; // 远端服务器的 URL
|
||||||
|
const cacheDir = path.join(__dirname, 'cache'); // 本地缓存目录
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// 确保本地缓存目录存在
|
||||||
|
fs.mkdir(cacheDir, { recursive: true }).catch(console.error);
|
||||||
|
|
||||||
|
// 获取文件的 content-type
|
||||||
|
function getContentType(filePath) {
|
||||||
|
const extname = path.extname(filePath);
|
||||||
|
const contentType = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'text/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.wav': 'audio/wav',
|
||||||
|
'.mp4': 'video/mp4'
|
||||||
|
};
|
||||||
|
return contentType[extname] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理请求文件
|
||||||
|
async function serveFile(filePath, remoteUrl, res) {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在于本地缓存中
|
||||||
|
const fileContent = await fs.readFile(filePath);
|
||||||
|
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
|
||||||
|
res.end(fileContent, 'utf-8');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
// 本地缓存中不存在,向远端服务器请求文件
|
||||||
|
try {
|
||||||
|
const response = await fetch(remoteUrl);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// 远端请求成功,获取文件内容
|
||||||
|
const data = await response.buffer();
|
||||||
|
|
||||||
|
// 将文件缓存到本地
|
||||||
|
await fs.writeFile(filePath, data);
|
||||||
|
|
||||||
|
// 返回文件内容
|
||||||
|
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
|
||||||
|
res.end(data, 'utf-8');
|
||||||
|
} else {
|
||||||
|
// 远端文件未找到或错误,返回 404
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end(`Error 404: File not found at ${remoteUrl}`);
|
||||||
|
}
|
||||||
|
} catch (fetchErr) {
|
||||||
|
// 处理请求错误
|
||||||
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end(`Server Error: Unable to fetch ${remoteUrl}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他文件系统错误
|
||||||
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end(`Server Error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTTP 服务器
|
||||||
|
http.createServer(async (req, res) => {
|
||||||
|
let reqPath = req.url;
|
||||||
|
|
||||||
|
// 如果路径是根路径 `/`,将其设置为 `index.html`
|
||||||
|
if (reqPath === '/') reqPath = '/index.html';
|
||||||
|
|
||||||
|
// 构建本地缓存路径和远端 URL
|
||||||
|
const localFilePath = path.join(cacheDir, reqPath); // 本地文件路径
|
||||||
|
const remoteFileUrl = url.resolve(remoteServer, reqPath); // 远端文件 URL
|
||||||
|
|
||||||
|
// 根据请求路径处理文件或返回 index.html(单页面应用处理)
|
||||||
|
await serveFile(localFilePath, remoteFileUrl, res);
|
||||||
|
|
||||||
|
// 单页面应用的路由处理
|
||||||
|
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||||
|
|
||||||
|
// 如果未匹配到任何文件,返回 index.html
|
||||||
|
const indexFilePath = path.join(cacheDir, 'index.html');
|
||||||
|
const indexRemoteUrl = url.resolve(remoteServer, '/index.html');
|
||||||
|
await serveFile(indexFilePath, indexRemoteUrl, res);
|
||||||
|
}).listen(PORT, () => {
|
||||||
|
console.log(`Server running at http://localhost:${PORT}`);
|
||||||
|
});
|
13
src/utils/parse.ts
Normal file
13
src/utils/parse.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const parseIfJson = (input: string): { [key: string]: any } | string => {
|
||||||
|
try {
|
||||||
|
// 尝试解析 JSON
|
||||||
|
const parsed = JSON.parse(input);
|
||||||
|
// 检查解析结果是否为对象(数组或普通对象)
|
||||||
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 如果解析失败,直接返回原始字符串
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
};
|
9
src/utils/pick.ts
Normal file
9
src/utils/pick.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||||
|
const result = {} as Pick<T, K>;
|
||||||
|
keys.forEach((key) => {
|
||||||
|
if (key in obj) {
|
||||||
|
result[key] = obj[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
1
src/validator/index.ts
Normal file
1
src/validator/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './rule.ts';
|
92
src/validator/rule.ts
Normal file
92
src/validator/rule.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { z, ZodError, Schema } from 'zod';
|
||||||
|
export { Schema };
|
||||||
|
type BaseRule = {
|
||||||
|
value?: any;
|
||||||
|
required?: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RuleString = {
|
||||||
|
type: 'string';
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
regex?: string;
|
||||||
|
} & BaseRule;
|
||||||
|
|
||||||
|
type RuleNumber = {
|
||||||
|
type: 'number';
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
} & BaseRule;
|
||||||
|
|
||||||
|
type RuleBoolean = {
|
||||||
|
type: 'boolean';
|
||||||
|
} & BaseRule;
|
||||||
|
|
||||||
|
type RuleArray = {
|
||||||
|
type: 'array';
|
||||||
|
items: Rule;
|
||||||
|
minItems?: number;
|
||||||
|
maxItems?: number;
|
||||||
|
} & BaseRule;
|
||||||
|
|
||||||
|
type RuleObject = {
|
||||||
|
type: 'object';
|
||||||
|
properties: { [key: string]: Rule };
|
||||||
|
} & BaseRule;
|
||||||
|
|
||||||
|
type RuleAny = {
|
||||||
|
type: 'any';
|
||||||
|
} & BaseRule;
|
||||||
|
|
||||||
|
export type Rule = RuleString | RuleNumber | RuleBoolean | RuleArray | RuleObject | RuleAny;
|
||||||
|
|
||||||
|
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.regex) stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
||||||
|
return stringSchema;
|
||||||
|
case 'number':
|
||||||
|
let numberSchema = z.number();
|
||||||
|
if (rule.min) numberSchema = numberSchema.min(rule.min, `Number must be at least ${rule.min}.`);
|
||||||
|
if (rule.max) numberSchema = numberSchema.max(rule.max, `Number must not exceed ${rule.max}.`);
|
||||||
|
return numberSchema;
|
||||||
|
case 'boolean':
|
||||||
|
return z.boolean();
|
||||||
|
case 'array':
|
||||||
|
return z.array(createSchema(rule.items));
|
||||||
|
case 'object':
|
||||||
|
return z.object(Object.fromEntries(Object.entries(rule.properties).map(([key, value]) => [key, createSchema(value)])));
|
||||||
|
case 'any':
|
||||||
|
return z.any();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const createSchema = (rule: Rule): Schema => {
|
||||||
|
try {
|
||||||
|
rule.required = rule.required || false;
|
||||||
|
if (!rule.required) {
|
||||||
|
return schemaFormRule(rule).nullable();
|
||||||
|
}
|
||||||
|
return schemaFormRule(rule);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ZodError) {
|
||||||
|
console.error(e.format());
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSchemaList = (rules: Rule[]) => {
|
||||||
|
try {
|
||||||
|
return rules.map((rule) => createSchema(rule));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ZodError) {
|
||||||
|
console.error(e.format());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"target": "esnext",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"sourceMap": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"newLine": "LF",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types",
|
||||||
|
],
|
||||||
|
"declaration": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"rollup.config.js",
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user