diff --git a/demo/simple/simple-ws/index.html b/demo/simple/simple-ws/index.html index e8e8d70..54f82b8 100644 --- a/demo/simple/simple-ws/index.html +++ b/demo/simple/simple-ws/index.html @@ -13,7 +13,9 @@ diff --git a/demo/simple/src/app-ws.ts b/demo/simple/src/app-ws.ts index df53398..bc40309 100644 --- a/demo/simple/src/app-ws.ts +++ b/demo/simple/src/app-ws.ts @@ -1,6 +1,6 @@ -import { Route, App } from '@kevisual/router'; +import { Route, App } from '@kevisual/router/src/app.ts'; -const app = new App({ io: true }); +const app = new App({ serverOptions: { io: true } }); app.listen(4002); const route01 = new Route('demo', '01'); route01.run = async (ctx) => { @@ -25,3 +25,14 @@ 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`); + +app.server.on({ + id: 'abc', + path: '/ws', + io: true, + fun: async ({ data }, { end }) => { + console.log('Custom middleware for /ws'); + console.log('Data received:', data); + end({ message: 'Hello from /ws middleware' }); + } +}) \ No newline at end of file diff --git a/demo/simple/src/simple-router/a.ts b/demo/simple/src/simple-router/a.ts index 5d72b51..64b231b 100644 --- a/demo/simple/src/simple-router/a.ts +++ b/demo/simple/src/simple-router/a.ts @@ -1,6 +1,6 @@ -import { SimpleRouter } from '@kevisual/router/simple'; +import { SimpleRouter } from '@kevisual/router/src/router-simple.ts'; -const router = new SimpleRouter(); +export const router = new SimpleRouter(); router.get('/', async (req, res) => { console.log('get /'); @@ -8,21 +8,28 @@ router.get('/', async (req, res) => { router.post('/post', async (req, res) => { console.log('post /post'); + console.log('req body:', req, res); + res.end('post response'); }); router.get('/user/:id', async (req, res) => { console.log('get /user/:id', req.params); + res.end(`user id is ${req.params.id}`); }); router.post('/user/:id', async (req, res) => { - console.log('post /user/:id', req.params); + console.log('post /user/:id params', req.params); + const body = await router.getBody(req); + console.log('post body:', body); + res.end(`post user id is ${req.params.id}`); }); router.post('/user/:id/a', async (req, res) => { console.log('post /user/:id', req.params); + res.end(`post user id is ${req.params.id} a`); }); -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); +// 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); diff --git a/demo/simple/src/simple-router/b.ts b/demo/simple/src/simple-router/b.ts new file mode 100644 index 0000000..8da9093 --- /dev/null +++ b/demo/simple/src/simple-router/b.ts @@ -0,0 +1,56 @@ +import { App } from '@kevisual/router/src/app.ts'; + +import { router } from './a.ts'; + +export const app = new App(); + +app.server.on([{ + fun: async (req, res) => { + console.log('Received request:', req.method, req.url); + const p = await router.parse(req, res); + if (p) { + console.log('Router parse result:', p); + } + } +}, { + id: 'abc', + path: '/ws', + io: true, + fun: async (data, end) => { + console.log('Custom middleware for /ws'); + console.log('Data received:', data); + end({ message: 'Hello from /ws middleware' }); + } +}]); + +app.server.listen(3004, () => { + console.log('Server is running on http://localhost:3004'); + + // fetch('http://localhost:3004/', { method: 'GET' }).then(async (res) => { + // const text = await res.text(); + // console.log('Response for GET /:', text); + // }); + + // fetch('http://localhost:3004/post', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ message: 'Hello, server!' }), + // }).then(async (res) => { + // const text = await res.text(); + // console.log('Response for POST /post:', text); + // }); + + // fetch('http://localhost:3004/user/123', { method: 'GET' }).then(async (res) => { + // const text = await res.text(); + // console.log('Response for GET /user/123:', text); + // }); + + fetch('http://localhost:3004/user/456', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'User456' }), + }).then(async (res) => { + const text = await res.text(); + console.log('Response for POST /user/456:', text); + }); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc61513..6434428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@kevisual/router", - "version": "0.0.39", + "version": "0.0.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kevisual/router", - "version": "0.0.39", + "version": "0.0.41", "license": "MIT", "dependencies": { "path-to-regexp": "^8.3.0", @@ -20,6 +20,7 @@ "@rollup/plugin-commonjs": "29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", + "@types/bun": "^1.3.5", "@types/node": "^25.0.3", "@types/send": "^1.2.1", "@types/ws": "^8.18.1", @@ -729,6 +730,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bun": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.5.tgz", + "integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.5" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "dev": true, @@ -1132,6 +1143,16 @@ "license": "MIT", "peer": true }, + "node_modules/bun-types": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.5.tgz", + "integrity": "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/bytestreamjs": { "version": "2.0.1", "license": "BSD-3-Clause", diff --git a/package.json b/package.json index 8bec419..3c5170d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@kevisual/router", - "version": "0.0.41", + "version": "0.0.42", "description": "", "type": "module", "main": "./dist/router.js", @@ -20,6 +20,7 @@ "keywords": [], "author": "abearxiong", "license": "MIT", + "packageManager": "pnpm@10.26.0", "devDependencies": { "@kevisual/local-proxy": "^0.0.8", "@kevisual/query": "^0.0.32", @@ -27,6 +28,7 @@ "@rollup/plugin-commonjs": "29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", + "@types/bun": "^1.3.5", "@types/node": "^25.0.3", "@types/send": "^1.2.1", "@types/ws": "^8.18.1", @@ -38,6 +40,7 @@ "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tslib": "^2.8.1", + "tsx": "^4.21.0", "typescript": "^5.9.3", "ws": "npm:@kevisual/ws", "xml2js": "^0.6.2", @@ -96,4 +99,4 @@ "require": "./src/*" } } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa66aef..c0b5de6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,15 +15,15 @@ importers: specifier: ^5.2.0 version: 5.2.0 send: - specifier: ^1.2.0 - version: 1.2.0 + specifier: ^1.2.1 + version: 1.2.1 devDependencies: '@kevisual/local-proxy': specifier: ^0.0.8 version: 0.0.8 '@kevisual/query': - specifier: ^0.0.31 - version: 0.0.31 + specifier: ^0.0.32 + version: 0.0.32 '@rollup/plugin-alias': specifier: ^6.0.0 version: 6.0.0(rollup@4.53.5) @@ -36,12 +36,12 @@ importers: '@rollup/plugin-typescript': specifier: ^12.3.0 version: 12.3.0(rollup@4.53.5)(tslib@2.8.1)(typescript@5.9.3) - '@types/lodash-es': - specifier: ^4.17.12 - version: 4.17.12 + '@types/bun': + specifier: ^1.3.5 + version: 1.3.5 '@types/node': - specifier: ^24.10.2 - version: 24.10.4 + specifier: ^25.0.3 + version: 25.0.3 '@types/send': specifier: ^1.2.1 version: 1.2.1 @@ -54,27 +54,27 @@ importers: cookie: specifier: ^1.1.1 version: 1.1.1 - lodash-es: - specifier: ^4.17.21 - version: 4.17.21 nanoid: specifier: ^5.1.6 version: 5.1.6 rollup: - specifier: ^4.53.3 + specifier: ^4.53.5 version: 4.53.5 rollup-plugin-dts: specifier: ^6.3.0 version: 6.3.0(rollup@4.53.5)(typescript@5.9.3) ts-loader: specifier: ^9.5.4 - version: 9.5.4(typescript@5.9.3)(webpack@5.102.1) + version: 9.5.4(typescript@5.9.3)(webpack@5.104.1) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@24.10.4)(typescript@5.9.3) + version: 10.9.2(@types/node@25.0.3)(typescript@5.9.3) tslib: specifier: ^2.8.1 version: 2.8.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -85,9 +85,25 @@ importers: specifier: ^0.6.2 version: 0.6.2 zod: - specifier: ^4.1.13 + specifier: ^4.2.1 version: 4.2.1 + demo/simple: + dependencies: + '@kevisual/router': + specifier: ../.. + version: link:../.. + devDependencies: + cookie: + specifier: ^1.0.2 + version: 1.1.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@25.0.3)(typescript@5.9.3) + typescript: + specifier: ^5.5.4 + version: 5.9.3 + packages: '@babel/code-frame@7.27.1': @@ -102,6 +118,162 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -124,8 +296,8 @@ packages: '@kevisual/local-proxy@0.0.8': resolution: {integrity: sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q==} - '@kevisual/query@0.0.31': - resolution: {integrity: sha512-bBdepjmMICLpcj/a9fnn82/0CGGYUZiCV+usWsJZKAwVlZcnj+WtKmbgKT09KpP6g3jjYzYOaXHiNFB8N0bQAQ==} + '@kevisual/query@0.0.32': + resolution: {integrity: sha512-9WN9cjmwSW8I5A0SqITdts9oxlLBGdPP7kJ8vwrxkaQteHS9FzxKuMBJxZzGKZdyte/zJDvdrE+lMf254BGbbg==} '@kevisual/ws@8.0.0': resolution: {integrity: sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg==} @@ -340,6 +512,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/bun@1.3.5': + resolution: {integrity: sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -352,14 +527,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/lodash-es@4.17.12': - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} - - '@types/node@24.10.4': - resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + '@types/node@25.0.3': + resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -466,28 +635,31 @@ packages: resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} engines: {node: '>=12.0.0'} - baseline-browser-mapping@2.8.28: - resolution: {integrity: sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==} + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bun-types@1.3.5: + resolution: {integrity: sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==} + bytestreamjs@2.0.1: resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} engines: {node: '>=6.0.0'} - caniuse-lite@1.0.30001754: - resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} + caniuse-lite@1.0.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -541,19 +713,24 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.252: - resolution: {integrity: sha512-53uTpjtRgS7gjIxZ4qCgFdNO2q+wJt/Z8+xAvxbCqXPJrY6h7ighUkadQmNMXH96crtpa6gPFNP7BF4UBGDuaA==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.18.3: - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -620,6 +797,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -634,8 +814,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} inherits@2.0.4: @@ -672,9 +852,6 @@ packages: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} - lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -700,9 +877,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -764,6 +941,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -800,8 +980,8 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} serialize-javascript@6.0.2: @@ -821,10 +1001,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -845,8 +1021,8 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.14: - resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + terser-webpack-plugin@5.3.16: + resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -901,6 +1077,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tsyringe@4.10.0: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} engines: {node: '>= 6.0.0'} @@ -913,8 +1094,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -930,8 +1111,8 @@ packages: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} - webpack@5.102.1: - resolution: {integrity: sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==} + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -971,6 +1152,84 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -997,7 +1256,7 @@ snapshots: '@kevisual/local-proxy@0.0.8': {} - '@kevisual/query@0.0.31': {} + '@kevisual/query@0.0.32': {} '@kevisual/ws@8.0.0': {} @@ -1210,6 +1469,10 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/bun@1.3.5': + dependencies: + bun-types: 1.3.5 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -1224,13 +1487,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/lodash-es@4.17.12': - dependencies: - '@types/lodash': 4.17.20 - - '@types/lodash@4.17.20': {} - - '@types/node@24.10.4': + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 @@ -1238,15 +1495,15 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 24.10.4 + '@types/node': 25.0.3 '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.4 + '@types/node': 25.0.3 '@types/xml2js@0.4.14': dependencies: - '@types/node': 24.10.4 + '@types/node': 25.0.3 '@webassemblyjs/ast@1.14.1': dependencies: @@ -1366,25 +1623,29 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - baseline-browser-mapping@2.8.28: {} + baseline-browser-mapping@2.9.11: {} braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.28.0: + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.8.28 - caniuse-lite: 1.0.30001754 - electron-to-chromium: 1.5.252 + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + electron-to-chromium: 1.5.267 node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.28.0) + update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-from@1.1.2: {} + bun-types@1.3.5: + dependencies: + '@types/node': 25.0.3 + bytestreamjs@2.0.1: {} - caniuse-lite@1.0.30001754: {} + caniuse-lite@1.0.30001761: {} chalk@4.1.2: dependencies: @@ -1419,16 +1680,45 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.252: {} + electron-to-chromium@1.5.267: {} encodeurl@2.0.0: {} - enhanced-resolve@5.18.3: + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 - es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} @@ -1472,6 +1762,10 @@ snapshots: function-bind@1.1.2: {} + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-to-regexp@0.4.1: {} graceful-fs@4.2.11: {} @@ -1482,12 +1776,12 @@ snapshots: dependencies: function-bind: 1.1.2 - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 inherits@2.0.4: {} @@ -1506,7 +1800,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.10.4 + '@types/node': 25.0.3 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -1519,8 +1813,6 @@ snapshots: loader-runner@4.3.1: {} - lodash-es@4.17.21: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1542,7 +1834,7 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -1593,6 +1885,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -1653,15 +1947,15 @@ snapshots: semver@7.7.3: {} - send@1.2.0: + send@1.2.1: dependencies: debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -1684,8 +1978,6 @@ snapshots: source-map@0.7.6: {} - statuses@2.0.1: {} - statuses@2.0.2: {} supports-color@7.2.0: @@ -1700,14 +1992,14 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.14(webpack@5.102.1): + terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.102.1 + webpack: 5.104.1 terser@5.44.1: dependencies: @@ -1722,24 +2014,24 @@ snapshots: toidentifier@1.0.1: {} - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.102.1): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.3 + enhanced-resolve: 5.18.4 micromatch: 4.0.8 semver: 7.7.3 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.102.1 + webpack: 5.104.1 - ts-node@10.9.2(@types/node@24.10.4)(typescript@5.9.3): + ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.10.4 + '@types/node': 25.0.3 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -1754,6 +2046,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + tsyringe@4.10.0: dependencies: tslib: 1.14.1 @@ -1762,9 +2061,9 @@ snapshots: undici-types@7.16.0: {} - update-browserslist-db@1.1.4(browserslist@4.28.0): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.28.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -1777,7 +2076,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.102.1: + webpack@5.104.1: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -1787,10 +2086,10 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.0 + browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 + enhanced-resolve: 5.18.4 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -1801,7 +2100,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.102.1) + terser-webpack-plugin: 5.3.16(webpack@5.104.1) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..7da6687 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - esbuild +packages: + - 'demo/**/*' \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index daa6620..4a58137 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,20 +1,21 @@ import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts'; -import { Server, ServerOpts, HandleCtx } from './server/server.ts'; -import { WsServer } from './server/ws-server.ts'; +import { ServerNode, ServerNodeOpts } from './server/server.ts'; +import { HandleCtx } from './server/server-base.ts'; +import { ServerType } from './server/server-type.ts'; import { CustomError } from './result/error.ts'; import { handleServer } from './server/handle-server.ts'; import { IncomingMessage, ServerResponse } from 'http'; +import { isBun } from './utils/is-engine.ts'; +import { BunServer } from './server/server-bun.ts'; type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any }; type AppOptions = { router?: QueryRouter; - server?: Server; + server?: ServerType; /** handle msg 关联 */ routerHandle?: RouterHandle; routerContext?: RouteContext; - serverOptions?: ServerOpts; - io?: boolean; - ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext; path?: string }; + serverOptions?: ServerNodeOpts; }; export type AppRouteContext = HandleCtx & RouteContext & { app: App }; @@ -25,18 +26,22 @@ export type AppRouteContext = HandleCtx & RouteContext & { app: App { router: QueryRouter; - server: Server; - io: WsServer; + server: ServerType; constructor(opts?: AppOptions) { const router = opts?.router || new QueryRouter(); - const server = opts?.server || new Server(opts?.serverOptions || {}); + let server = opts?.server; + if (!server) { + const serverOptions = opts?.serverOptions || {}; + if (!isBun) { + server = new ServerNode(serverOptions) + } else { + server = new BunServer(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) { - 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; @@ -49,9 +54,6 @@ export class App { 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); @@ -130,7 +132,10 @@ export class App { if (!this.server) { throw new Error('Server is not initialized'); } - this.server.on(fn); + this.server.on({ + id: 'app-request-listener', + fun: fn + }); } } diff --git a/src/index.ts b/src/index.ts index 5840c6d..43f5c87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts'; export type { Run } from './route.ts'; -export { Server, handleServer } from './server/index.ts'; +export { ServerNode, handleServer } from './server/index.ts'; /** * 自定义错误 */ @@ -13,7 +13,7 @@ export { CustomError } from './result/error.ts'; export { createSchema } from './validator/index.ts'; -export type { Rule, Schema, } from './validator/index.ts'; +export type { Rule, Schema, } from './validator/index.ts'; export { App } from './app.ts'; diff --git a/src/router-simple.ts b/src/router-simple.ts index 8a7c2b5..b28b7cd 100644 --- a/src/router-simple.ts +++ b/src/router-simple.ts @@ -62,6 +62,7 @@ export class SimpleRouter { } isSse(req: Req) { const { headers } = req; + if (!headers) return false; if (headers['accept'] && headers['accept'].includes('text/event-stream')) { return true; } diff --git a/src/server/handle-server.ts b/src/server/handle-server.ts index 103b031..25d462c 100644 --- a/src/server/handle-server.ts +++ b/src/server/handle-server.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import { parseBody } from './parse-body.ts'; import url from 'node:url'; -import { createHandleCtx } from './server.ts'; +import { createHandleCtx } from './server-base.ts'; /** * get params and body diff --git a/src/server/index.ts b/src/server/index.ts index bfb2689..39b5dad 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,2 +1,3 @@ -export { Server } from './server.ts'; +export { ServerNode } from './server.ts'; +export { BunServer } from './server-bun.ts'; export { handleServer } from './handle-server.ts'; diff --git a/src/server/parse-body.ts b/src/server/parse-body.ts index 3beb9f7..f01f038 100644 --- a/src/server/parse-body.ts +++ b/src/server/parse-body.ts @@ -1,7 +1,49 @@ import type { IncomingMessage } from 'node:http'; import url from 'node:url'; - +import { isBun } from '../utils/is-engine.ts'; export const parseBody = async >(req: IncomingMessage) => { + const resolveBody = (body: string) => { + // 获取 Content-Type 头信息 + const contentType = req.headers['content-type'] || ''; + const resolve = (data: T) => { + return data; + } + // 处理 application/json + if (contentType.includes('application/json')) { + return resolve(JSON.parse(body) as T); + } + // 处理 application/x-www-form-urlencoded + if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = new URLSearchParams(body); + const result: Record = {}; + + formData.forEach((value, key) => { + // 尝试将值解析为 JSON,如果失败则保留原始字符串 + try { + result[key] = JSON.parse(value); + } catch { + result[key] = value; + } + }); + + return resolve(result as T); + } + + // 默认尝试 JSON 解析 + try { + return resolve(JSON.parse(body) as T); + } catch { + return resolve({} as T); + } + } + if (isBun) { + // @ts-ignore + const body = req.body; + if (body) { + return resolveBody(body) + } + return {} as T; + } return new Promise((resolve, reject) => { const arr: any[] = []; req.on('data', (chunk) => { @@ -10,39 +52,8 @@ export const parseBody = async >(req: IncomingMessage) = req.on('end', () => { try { const body = Buffer.concat(arr).toString(); + resolve(resolveBody(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 = {}; - - 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); } diff --git a/src/server/server-base.ts b/src/server/server-base.ts new file mode 100644 index 0000000..ff3269d --- /dev/null +++ b/src/server/server-base.ts @@ -0,0 +1,265 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { handleServer } from './handle-server.ts'; +import * as cookie from './cookie.ts'; +import { ServerType, Listener, OnListener, ServerOpts } from './server-type.ts'; +import { parseIfJson } from '../utils/parse.ts'; + +type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void; + +export type HandleCtx = { + req: IncomingMessage & { cookies: Record }; + 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; +}; + +export const resultError = (error: string, code = 500) => { + const r = { + code: code, + message: error, + }; + return JSON.stringify(r); +}; + +export class ServerBase implements ServerType { + path = '/api/router'; + _server: any; + handle: ServerOpts['handle']; + _callback: any; + cors: Cors; + listeners: Listener[] = []; + 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.customListen(...args); + } + /** + * child class can custom listen method + * @param args + */ + customListen(...args: any[]) { + console.error('Please use createServer to create server instance'); + } + get handleServer() { + return this._callback; + } + set handleServer(fn: any) { + this._callback = fn; + } + get callback() { + return this._callback || this.createCallback(); + } + get server() { + return this._server; + } + setHandle(handle?: any) { + this.handle = handle; + } + /** + * get callback + * @returns + */ + createCallback() { + const path = this.path; + const handle = this.handle; + const cors = this.cors; + const that = this; + const _callback = async (req: IncomingMessage, res: ServerResponse) => { + // only handle /api/router + if (req.url === '/favicon.ico') { + return; + } + const listeners = that.listeners || []; + for (const item of listeners) { + const fun = item.fun; + if (typeof fun === 'function' && !item.io) { + await fun(req, res); + } + } + if (res.headersSent) { + // 程序已经在其他地方响应了 + return; + } + if (!req.url.startsWith(path)) { + // 判断不是当前路径的请求,交给其他监听处理 + return; + } + 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; + } + } + 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, { req, res }); + if (res.writableEnded) { + // 如果响应已经结束,则不进行任何操作 + return; + } + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + if (typeof end === 'string') { + res.end(end); + } else { + res.end(JSON.stringify(end)); + } + } 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 { + res.end(resultError('Router Server error')); + } + } + }; + this._callback = _callback; + return _callback; + } + on(listener: OnListener) { + this.listeners = []; + if (typeof listener === 'function') { + this.listeners.push({ fun: listener }); + return; + } + if (Array.isArray(listener)) { + for (const item of listener) { + if (typeof item === 'function') { + this.listeners.push({ fun: item }); + } else { + this.listeners.push(item); + } + } + } else { + this.listeners.push(listener); + } + } + async onWebSocket({ ws, message, pathname, token, id }) { + const listener = this.listeners.find((item) => item.path === pathname && item.io); + const data: any = parseIfJson(message); + + if (listener) { + const end = (data: any) => { + ws.send(JSON.stringify(data)); + } + listener.fun({ + data, + token, + id, + ws, + }, { end }); + return; + } + + if (typeof data === 'string') { + const cleanMessage = data.trim().replace(/^["']|["']$/g, ''); + if (cleanMessage === 'close') { + ws.close(); + return; + } + if (cleanMessage === 'ping') { + ws.send('pong'); + return; + } + } + + const { type, data: typeData, ...rest } = data; + if (!type) { + ws.send(JSON.stringify({ code: 500, message: 'type is required' })); + return; + } + + const res = { + type, + data: {} as any, + ...rest, + }; + const end = (data: any, all?: Record) => { + const result = { + ...res, + data, + ...all, + }; + ws.send(JSON.stringify(result)); + }; + + + // 调用 handle 处理消息 + if (type === 'router' && this.handle) { + try { + const result = await this.handle(typeData as any); + end(result); + } catch (e: any) { + if (e.code && typeof e.code === 'number') { + end({ + code: e.code, + message: e.message, + }); + } else { + end({ code: 500, message: 'Router Server error' }); + } + } + } else { + end({ code: 500, message: `${type} server is error` }); + } + } +} diff --git a/src/server/server-bun.ts b/src/server/server-bun.ts new file mode 100644 index 0000000..00465e4 --- /dev/null +++ b/src/server/server-bun.ts @@ -0,0 +1,174 @@ +/** + * @title Bun Server Implementation + * @description Bun 服务器实现,提供基于 Bun.serve 的 HTTP 和 WebSocket 功能 + * @tags bun, server, websocket, http + * @createdAt 2025-12-20 + */ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { ServerType, type ServerOpts, type Cors, Listener } from './server-type.ts'; +import { handleServer } from './handle-server.ts'; +import { ServerBase } from './server-base.ts'; +import { parseIfJson } from '../utils/parse.ts'; + +const resultError = (error: string, code = 500) => { + const r = { + code: code, + message: error, + }; + return JSON.stringify(r); +}; + +export class BunServer extends ServerBase implements ServerType { + declare _server: any; + declare _callback: any; + declare cors: Cors; + constructor(opts?: ServerOpts) { + super(opts); + } + customListen(...args: any[]): void { + this.listenWithBun(...args); + } + /** + * Bun 运行时的 listen 实现 + */ + private listenWithBun(...args: any[]) { + // @ts-ignore - Bun 全局 API + if (typeof Bun === 'undefined' || !Bun.serve) { + throw new Error('Bun runtime not detected'); + } + + let port: number = 3000; + let hostname: string = 'localhost'; + let callback: (() => void) | undefined; + + // 解析参数 + if (typeof args[0] === 'number') { + port = args[0]; + if (typeof args[1] === 'string') { + hostname = args[1]; + callback = args[2] || args[3]; + } else if (typeof args[1] === 'function') { + callback = args[1]; + } else { + callback = args[2]; + } + } + + const requestCallback = this.createCallback(); + const wsPath = this.path; + // @ts-ignore + this._server = Bun.serve({ + port, + hostname, + idleTimeout: 0, // 4 minutes idle timeout (max 255 seconds) + fetch: async (request: Request, server: any) => { + const host = request.headers.get('host') || 'localhost'; + const url = new URL(request.url, `http://${host}`); + // 处理 WebSocket 升级请求 + if (request.headers.get('upgrade') === 'websocket') { + const listenPath = this.listeners.map((item) => item.path).filter((item) => item); + if (listenPath.includes(url.pathname) || url.pathname === wsPath) { + const token = url.searchParams.get('token') || ''; + const id = url.searchParams.get('id') || ''; + const upgraded = server.upgrade(request, { + data: { url: url, pathname: url.pathname, token, id }, + }); + if (upgraded) { + return undefined; // WebSocket 连接成功 + } + } + return new Response('WebSocket upgrade failed', { status: 400 }); + } + + // 将 Bun 的 Request 转换为 Node.js 风格的 req/res + return new Promise((resolve) => { + const req: any = { + url: url.pathname + url.search, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + }; + + const res: any = { + statusCode: 200, + headersSent: false, + writableEnded: false, + _headers: {} as Record, + writeHead(statusCode: number, headers: Record) { + this.statusCode = statusCode; + for (const key in headers) { + this._headers[key] = headers[key]; + } + this.headersSent = true; + }, + setHeader(name: string, value: string | string[]) { + this._headers[name] = value; + }, + cookie(name: string, value: string, options?: any) { + let cookieString = `${name}=${value}`; + if (options) { + if (options.maxAge) { + cookieString += `; Max-Age=${options.maxAge}`; + } + if (options.domain) { + cookieString += `; Domain=${options.domain}`; + } + if (options.path) { + cookieString += `; Path=${options.path}`; + } + if (options.expires) { + cookieString += `; Expires=${options.expires.toUTCString()}`; + } + if (options.httpOnly) { + cookieString += `; HttpOnly`; + } + if (options.secure) { + cookieString += `; Secure`; + } + if (options.sameSite) { + cookieString += `; SameSite=${options.sameSite}`; + } + } + this.setHeader('Set-Cookie', cookieString); + }, + end(data?: string) { + this.writableEnded = true; + resolve( + new Response(data, { + status: this.statusCode, + headers: this._headers as any, + }) + ); + }, + }; + // 处理请求体 + if (request.method !== 'GET' && request.method !== 'HEAD') { + request.text().then((body) => { + (req as any).body = body; + requestCallback(req, res); + }); + } else { + requestCallback(req, res); + } + }); + }, + websocket: { + open: (ws: any) => { + ws.send('connected'); + }, + message: async (ws: any, message: string | Buffer) => { + const pathname = ws.data.pathname || ''; + const token = ws.data.token || ''; + const id = ws.data.id || ''; + await this.onWebSocket({ ws, message, pathname, token, id }); + }, + close: (ws: any) => { + // WebSocket 连接关闭 + }, + }, + }); + + if (callback) { + callback(); + } + } +} diff --git a/src/server/server-type.ts b/src/server/server-type.ts new file mode 100644 index 0000000..a3b9ae2 --- /dev/null +++ b/src/server/server-type.ts @@ -0,0 +1,70 @@ +import * as http from 'http'; + +export type Listener = { + id?: string; + io?: boolean; + path?: string; + fun: (...args: any[]) => Promise | void; +} +export type ListenerFun = (...args: any[]) => Promise | void; +export type OnListener = Listener | Listener[] | ListenerFun | ListenerFun[]; +export type Cors = { + /** + * @default '*'' + */ + origin?: string | undefined; +}; + +export type ServerOpts = { + /**path default `/api/router` */ + path?: string; + /**handle Fn */ + handle?: (msg?: { path: string; key?: string;[key: string]: any }, ctx?: { req: http.IncomingMessage; res: http.ServerResponse }) => any; + cors?: Cors; + io?: boolean; +} & T; + +export interface ServerType { + path?: string; + server?: any; + handle: ServerOpts['handle']; + setHandle(handle?: any): void; + listeners: Listener[]; + 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; + /** + * 兜底监听,当除开 `/api/router` 之外的请求,框架只监听一个api,所以有其他的请求都执行其他的监听 + * @description 主要是为了兼容其他的监听 + * @param listener + */ + on(listener: OnListener): void; + onWebSocket({ ws, message, pathname, token, id }: { ws: WS; message: string | Buffer; pathname: string, token?: string, id?: string }): void; +} + +type WS = { + send: (data: any) => void; + close: () => void; +} + +export type CommonReq = { + url: string; + method: string; + headers: Record; + [key: string]: any; +} + +export type CommonRes = { + statusCode: number; + writableEnded: boolean; + writeHead: (statusCode: number, headers?: Record) => void; + setHeader: (name: string, value: string | string[]) => void; + cookie: (name: string, value: string, options?: any) => void; + end: (data?: any) => void; + [key: string]: any; +} \ No newline at end of file diff --git a/src/server/server.ts b/src/server/server.ts index 33da3e0..74248e3 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,64 +1,22 @@ -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.ts'; -export type Listener = (...args: any[]) => void; +import { isBun } from '../utils/is-engine.ts'; +import { ServerType, Listener, ServerOpts } from './server-type.ts'; +import { ServerBase } from './server-base.ts'; +import { WsServer } from './ws-server.ts'; -type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void; - -export type HandleCtx = { - req: IncomingMessage & { cookies: Record }; - 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; }; -export type ServerOpts = { - /**path default `/api/router` */ - path?: string; - /**handle Fn */ - handle?: (msg?: { path: string; key?: string;[key: string]: any }, ctx?: { req: http.IncomingMessage; res: http.ServerResponse }) => any; - cors?: Cors; +export type ServerNodeOpts = ServerOpts<{ httpType?: 'http' | 'https' | 'http2'; httpsKey?: string; httpsCert?: string; -}; +}>; export const resultError = (error: string, code = 500) => { const r = { code: code, @@ -67,41 +25,39 @@ export const resultError = (error: string, code = 500) => { return JSON.stringify(r); }; -export class Server { - path = '/api/router'; - private _server: http.Server | https.Server | http2.Http2SecureServer; - public handle: ServerOpts['handle']; - private _callback: any; - private cors: Cors; - private hasOn = false; +export class ServerNode extends ServerBase implements ServerType { + declare _server: http.Server | https.Server | http2.Http2SecureServer; + declare _callback: any; + declare cors: Cors; private httpType = 'http'; + declare listeners: Listener[]; private options = { key: '', cert: '', }; - constructor(opts?: ServerOpts) { - this.path = opts?.path || '/api/router'; - this.handle = opts?.handle; - this.cors = opts?.cors; + io: WsServer | undefined; + constructor(opts?: ServerNodeOpts) { + super(opts); this.httpType = opts?.httpType || 'http'; this.options = { key: opts?.httpsKey || '', cert: opts?.httpsCert || '', }; + const io = opts?.io ?? false; + if (io) { + this.io = new WsServer(this); + } } - 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[]) { + customListen(...args: any[]): void { + if (isBun) { + throw new Error('Use BunServer from server-bun module for Bun runtime'); + } this._server = this.createServer(); const callback = this.createCallback(); this._server.on('request', callback); this._server.listen(...args); + + this.io?.listen(); } createServer() { let server: http.Server | https.Server | http2.Http2SecureServer; @@ -132,112 +88,4 @@ export class Server { server = http.createServer(); return server; } - 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) => { - // only handle /api/router - if (req.url === '/favicon.ico') { - return; - } - if (res.headersSent) { - // 程序已经在其他地方响应了 - return; - } - if (this.hasOn && !req.url.startsWith(path)) { - // 其他监听存在,不判断不是当前路径的请求, - // 也就是不处理!url.startsWith(path)这个请求了 - // 交给其他监听处理 - return; - } - 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; - } - } - 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, { req, res }); - if (res.writableEnded) { - // 如果响应已经结束,则不进行任何操作 - return; - } - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - if (typeof end === 'string') { - res.end(end); - } else { - res.end(JSON.stringify(end)); - } - } 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 { - 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 || this.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); - } - const callbackListener = this._callback || this.createCallback(); - this._server.on('request', callbackListener); - return () => { - if (Array.isArray(listener)) { - listener.forEach((l) => this._server.removeListener('request', l as Listener)); - } else { - this._server.removeListener('request', listener as Listener); - } - this.hasOn = false; - this._server.removeListener('request', callbackListener); - } - } - get callback() { - return this._callback || this.createCallback(); - } - get server() { - return this._server; - } -} +} \ No newline at end of file diff --git a/src/server/ws-server.ts b/src/server/ws-server.ts index 713874a..4542404 100644 --- a/src/server/ws-server.ts +++ b/src/server/ws-server.ts @@ -1,39 +1,38 @@ // @ts-type=ws import { WebSocketServer } from 'ws'; import type { WebSocket } from 'ws'; -import { Server } from './server.ts'; +import { ServerType } from './server-type.ts' import { parseIfJson } from '../utils/parse.ts'; +import { isBun } from '../utils/is-engine.ts'; -export const createWsServer = (server: Server) => { +export const createWsServer = (server: ServerType) => { // 将 WebSocket 服务器附加到 HTTP 服务器 const wss = new WebSocketServer({ server: server.server as any }); return wss; }; type WsServerBaseOpts = { - wss?: WebSocketServer; + wss?: WebSocketServer | null; path?: string; }; export type ListenerFn = (message: { data: Record; ws: WebSocket; end: (data: any) => any }) => Promise; export type Listener = { type: T; + path?: string; listener: ListenerFn; }; export class WsServerBase { - wss: WebSocketServer; - path: string; - listeners: { type: string; listener: ListenerFn }[] = []; + wss: WebSocketServer | null; + listeners: Listener[] = []; listening: boolean = false; + server: ServerType; + constructor(opts: WsServerBaseOpts) { this.wss = opts.wss; - if (!this.wss) { + if (!this.wss && !isBun) { throw new Error('wss is required'); } - this.path = opts.path || ''; - } - setPath(path: string) { - this.path = path; } listen() { if (this.listening) { @@ -42,116 +41,49 @@ export class WsServerBase { } this.listening = true; - this.wss.on('connection', (ws) => { - ws.on('message', async (message: string | Buffer) => { - const data = parseIfJson(message); - if (typeof data === 'string') { - const cleanMessage = data.trim().replace(/^["']|["']$/g, ''); - ws.emit('string', cleanMessage); - 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) => { - const result = { - ...res, - data, - ...all, - }; - ws.send(JSON.stringify(result)); - }; + if (!this.wss) { + // Bun 环境下,wss 可能为 null + return; + } - 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'); - } + this.wss.on('connection', (ws, req) => { + const url = new URL(req.url, 'http://localhost'); + const pathname = url.pathname; + const token = url.searchParams.get('token') || ''; + const id = url.searchParams.get('id') || ''; + ws.on('message', async (message: string | Buffer) => { + await this.server.onWebSocket({ ws, message, pathname, token, id }); }); 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; + constructor(server: ServerType) { + const wss = isBun ? null : new WebSocketServer({ noServer: true }); 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() { + if (isBun) { + // Bun 的 WebSocket 在 Bun.serve 中处理,这里不需要额外操作 + // WebSocket 升级会在 listenWithBun 中处理 + this.listening = true; + return; + } super.listen(); const server = this.server; const wss = this.wss; + // HTTP 服务器的 upgrade 事件 + // @ts-ignore server.server.on('upgrade', (req, socket, head) => { - if (req.url === this.path) { + const url = new URL(req.url, 'http://localhost'); + const listenPath = this.server.listeners.map((item) => item.path).filter((item) => item); + if (listenPath.includes(url.pathname) || url.pathname === this.server.path) { wss.handleUpgrade(req, socket, head, (ws) => { - // 这里手动触发 connection 事件 + // 这里手动触发 connection事件 // @ts-ignore wss.emit('connection', ws, req); }); diff --git a/src/test/listen-ip.ts b/src/test/listen-ip.ts index 56f4689..599a14b 100644 --- a/src/test/listen-ip.ts +++ b/src/test/listen-ip.ts @@ -1,7 +1,7 @@ // import { Server } from 'node:http'; -import { Server } from '../server/server.ts' +import { ServerNode } from '../server/server.ts' -const server = new Server({ +const server = new ServerNode({ path: '/', handle: async (data, ctx) => { console.log('ctx', ctx.req.url) diff --git a/src/utils/is-engine.ts b/src/utils/is-engine.ts index a524171..e544caa 100644 --- a/src/utils/is-engine.ts +++ b/src/utils/is-engine.ts @@ -2,9 +2,13 @@ export const isNode = typeof process !== 'undefined' && process.versions != 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'; +// @ts-ignore +export const isBun = typeof Bun !== 'undefined' && typeof Bun.version === 'string'; export const getEngine = () => { - if (isNode) { + if (isBun) { + return 'bun'; + } else if (isNode) { return 'node'; } else if (isBrowser) { return 'browser';