From ecb69ba326ff5e7b4c3e1e547ed8090cedea1518 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Tue, 17 Feb 2026 21:39:41 +0800 Subject: [PATCH] refactor: migrate from Rollup to Bun for build configuration feat: update adapter to use globalThis for origin resolution fix: remove unused ClientQuery export from query.ts chore: update tsconfig to include test files and set rootDir feat: add create-query functionality for dynamic API generation feat: implement QueryApi with enhanced type inference from JSON Schema test: add comprehensive API tests for QueryApi functionality test: create demo routes and schemas for testing purposes docs: add type inference demo for QueryApi usage --- bun.config.ts | 8 + demo/backend/app.ts | 9 +- demo/src/index.ts | 4 +- demo/type-inference-demo.ts | 67 +++++ package.json | 38 +-- pnpm-lock.yaml | 497 +++--------------------------------- rollup.config.js | 81 ------ src/adapter.ts | 2 +- src/create-query/index.ts | 130 ++++++++++ src/query-api.ts | 136 ++++++++++ src/query-browser.ts | 4 +- src/query.ts | 11 +- test/api.ts | 30 +++ test/common.ts | 26 ++ test/query.ts | 472 ++++++++++++++++++++++++++++++++++ test/router.ts | 131 ++++++++++ test/schema.ts | 43 ++++ tsconfig.json | 8 +- 18 files changed, 1106 insertions(+), 591 deletions(-) create mode 100644 bun.config.ts create mode 100644 demo/type-inference-demo.ts delete mode 100644 rollup.config.js create mode 100644 src/create-query/index.ts create mode 100644 src/query-api.ts create mode 100644 test/api.ts create mode 100644 test/common.ts create mode 100644 test/query.ts create mode 100644 test/router.ts create mode 100644 test/schema.ts diff --git a/bun.config.ts b/bun.config.ts new file mode 100644 index 0000000..d52e3af --- /dev/null +++ b/bun.config.ts @@ -0,0 +1,8 @@ +import { buildWithBun } from '@kevisual/code-builder'; + +await buildWithBun({ naming: 'query-browser', entry: 'src/query-browser.ts', dts: true, target: 'browser' }); +await buildWithBun({ naming: 'query', entry: 'src/query.ts', dts: true, target: 'browser' }); +await buildWithBun({ naming: 'query-ws', entry: 'src/ws.ts', dts: true, target: 'browser' }); +await buildWithBun({ naming: 'query-adapter', entry: 'src/adapter.ts', dts: true, target: 'browser' }); +await buildWithBun({ naming: 'query-api', entry: 'src/query-api.ts', dts: true, target: 'browser' }); + diff --git a/demo/backend/app.ts b/demo/backend/app.ts index 7dae7ea..5d6684d 100644 --- a/demo/backend/app.ts +++ b/demo/backend/app.ts @@ -1,8 +1,6 @@ import { App } from '@kevisual/router'; -const app = new App({ - io: true, -}); +const app = new App({}); app .route({ @@ -17,8 +15,3 @@ app app.listen(4000, () => { console.log('Server is running at http://localhost:4000'); }); - -app.io.addListener('subscribe', async ({ data, end, ws }) => { - console.log('A user connected', data); - ws.send('Hello World'); -}); diff --git a/demo/src/index.ts b/demo/src/index.ts index b767f2e..bb39d50 100644 --- a/demo/src/index.ts +++ b/demo/src/index.ts @@ -1,6 +1,6 @@ // console.log('Hello World'); -import { adapter, Query } from '@abearxiong/query'; -import { QueryWs } from '@abearxiong/query/ws'; +import { adapter, Query } from '@kevisual/query'; +import { QueryWs } from '@kevisual/query/ws'; window.onload = async () => { // const res = await adapter({ diff --git a/demo/type-inference-demo.ts b/demo/type-inference-demo.ts new file mode 100644 index 0000000..890e7b7 --- /dev/null +++ b/demo/type-inference-demo.ts @@ -0,0 +1,67 @@ +import { QueryApi } from "../src/query-api"; + +const api = new QueryApi(); + +// 示例 1: args 的 value 是 string 类型 +api.post( + { + path: "/users", + metadata: { + args: { + name: "John", + email: "john@example.com" + } + } + }, + { + pos: "someValue", // ✅ TypeScript 会推断 pos 应该是 string 类型 + other: "data" + } +); + +// 示例 2: args 的 value 是 number 类型 +api.post( + { + path: "/products", + metadata: { + args: { + id: 123, + price: 99.99 + } + } + }, + { + pos: 456, // ✅ TypeScript 会推断 pos 应该是 number 类型 + other: "data" + } +); + +// 示例 3: args 的 value 是混合类型 +api.post( + { + path: "/orders", + metadata: { + args: { + orderId: 123, + status: "pending", + total: 99.99 + } + } + }, + { + pos: "status", // ✅ TypeScript 会推断 pos 可以是 string 或 number + // pos: 999, // 这也是合法的 + other: "data" + } +); + +// 示例 4: 没有 metadata 或 args +api.post( + { + path: "/test" + }, + { + pos: undefined, // ✅ pos 类型为 never,只能是 undefined + other: "data" + } +); diff --git a/package.json b/package.json index 328662d..d946394 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,9 @@ { "name": "@kevisual/query", - "version": "0.0.40", - "main": "dist/query-browser.js", - "private": false, + "version": "0.0.41", "type": "module", "scripts": { - "build": "npm run clean && rollup -c", - "dev:lib": "rollup -c -w", + "build": "npm run clean && bun run bun.config.ts", "clean": "rm -rf dist" }, "files": [ @@ -21,11 +18,12 @@ "license": "ISC", "description": "", "devDependencies": { - "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-typescript": "^12.3.0", - "rollup": "^4.57.1", - "rollup-plugin-dts": "^6.3.0", + "@kevisual/code-builder": "^0.0.6", + "@kevisual/router": "^0.0.72", + "@types/node": "^25.2.3", "typescript": "^5.9.3", + "es-toolkit": "^1.44.0", + "zod": "^4.3.6", "zustand": "^5.0.11" }, "publishConfig": { @@ -36,20 +34,10 @@ "url": "git+ssh://git@github.com/abearxiong/kevisual-query.git" }, "exports": { - ".": { - "import": "./dist/query-browser.js", - "require": "./dist/query-browser.js" - }, - "./query": { - "import": "./dist/query.js", - "require": "./dist/query.js" - }, - "./ws": { - "import": "./dist/query-ws.js", - "require": "./dist/query-ws.js" - } + ".": "./dist/query-browser.js", + "./query": "./dist/query.js", + "./ws": "./dist/query-ws.js", + "./api": "./dist/query-api.js" }, - "dependencies": { - "tslib": "^2.8.1" - } -} + "dependencies": {} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec2ab00..67b95fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,288 +8,55 @@ importers: .: dependencies: - tslib: - specifier: ^2.8.1 - version: 2.8.1 + es-toolkit: + specifier: ^1.44.0 + version: 1.44.0 devDependencies: - '@rollup/plugin-node-resolve': - specifier: ^16.0.3 - version: 16.0.3(rollup@4.57.1) - '@rollup/plugin-typescript': - specifier: ^12.3.0 - version: 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3) - rollup: - specifier: ^4.57.1 - version: 4.57.1 - rollup-plugin-dts: - specifier: ^6.3.0 - version: 6.3.0(rollup@4.57.1)(typescript@5.9.3) + '@kevisual/code-builder': + specifier: ^0.0.6 + version: 0.0.6 + '@kevisual/router': + specifier: ^0.0.72 + version: 0.0.72 + '@types/node': + specifier: ^25.2.3 + version: 25.2.3 typescript: specifier: ^5.9.3 version: 5.9.3 + zod: + specifier: ^4.3.6 + version: 4.3.6 zustand: specifier: ^5.0.11 version: 5.0.11 packages: - '@babel/code-frame@7.28.6': - resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-typescript@12.3.0': - resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.57.1': - resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.57.1': - resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.57.1': - resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.57.1': - resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.57.1': - resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.57.1': - resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.57.1': - resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.57.1': - resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.57.1': - resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.57.1': - resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.57.1': - resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.57.1': - resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.57.1': - resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.57.1': - resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.57.1': - resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.57.1': - resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.57.1': - resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.57.1': - resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.57.1': - resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.57.1': - resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.57.1': - resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.57.1': - resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.57.1': - resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.57.1': - resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.57.1': - resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} - cpu: [x64] - os: [win32] - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} + '@kevisual/code-builder@0.0.6': + resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==} hasBin: true - rollup-plugin-dts@6.3.0: - resolution: {integrity: sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==} - engines: {node: '>=16'} - peerDependencies: - rollup: ^3.29.4 || ^4 - typescript: ^4.5 || ^5.0 + '@kevisual/router@0.0.72': + resolution: {integrity: sha512-+HL4FINZsjnoRRa8Qs7xoPg+5/TcHR7jZQ7AHWHogo0BJzCAtnQwmidMQzeGL4z0WKNbbgVhXdz1wAYoxHJZTg==} - rollup@4.57.1: - resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + '@types/node@25.2.3': + resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + es-toolkit@1.44.0: + resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@5.0.11: resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} @@ -310,206 +77,22 @@ packages: snapshots: - '@babel/code-frame@7.28.6': + '@kevisual/code-builder@0.0.6': {} + + '@kevisual/router@0.0.72': dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - optional: true + es-toolkit: 1.44.0 - '@babel/helper-validator-identifier@7.28.5': - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@rollup/plugin-node-resolve@16.0.3(rollup@4.57.1)': + '@types/node@25.2.3': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.11 - optionalDependencies: - rollup: 4.57.1 + undici-types: 7.16.0 - '@rollup/plugin-typescript@12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - resolve: 1.22.11 - typescript: 5.9.3 - optionalDependencies: - rollup: 4.57.1 - tslib: 2.8.1 - - '@rollup/pluginutils@5.3.0(rollup@4.57.1)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.57.1 - - '@rollup/rollup-android-arm-eabi@4.57.1': - optional: true - - '@rollup/rollup-android-arm64@4.57.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.57.1': - optional: true - - '@rollup/rollup-darwin-x64@4.57.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.57.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.57.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.57.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.57.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.57.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.57.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.57.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.57.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.57.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.57.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.57.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.57.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.57.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.57.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.57.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.57.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.57.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.57.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.57.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.57.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.57.1': - optional: true - - '@types/estree@1.0.8': {} - - '@types/resolve@1.20.2': {} - - deepmerge@4.3.1: {} - - estree-walker@2.0.2: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-module@1.0.0: {} - - js-tokens@4.0.0: - optional: true - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - path-parse@1.0.7: {} - - picocolors@1.1.1: - optional: true - - picomatch@4.0.3: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - rollup-plugin-dts@6.3.0(rollup@4.57.1)(typescript@5.9.3): - dependencies: - magic-string: 0.30.21 - rollup: 4.57.1 - typescript: 5.9.3 - optionalDependencies: - '@babel/code-frame': 7.28.6 - - rollup@4.57.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.57.1 - '@rollup/rollup-android-arm64': 4.57.1 - '@rollup/rollup-darwin-arm64': 4.57.1 - '@rollup/rollup-darwin-x64': 4.57.1 - '@rollup/rollup-freebsd-arm64': 4.57.1 - '@rollup/rollup-freebsd-x64': 4.57.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 - '@rollup/rollup-linux-arm-musleabihf': 4.57.1 - '@rollup/rollup-linux-arm64-gnu': 4.57.1 - '@rollup/rollup-linux-arm64-musl': 4.57.1 - '@rollup/rollup-linux-loong64-gnu': 4.57.1 - '@rollup/rollup-linux-loong64-musl': 4.57.1 - '@rollup/rollup-linux-ppc64-gnu': 4.57.1 - '@rollup/rollup-linux-ppc64-musl': 4.57.1 - '@rollup/rollup-linux-riscv64-gnu': 4.57.1 - '@rollup/rollup-linux-riscv64-musl': 4.57.1 - '@rollup/rollup-linux-s390x-gnu': 4.57.1 - '@rollup/rollup-linux-x64-gnu': 4.57.1 - '@rollup/rollup-linux-x64-musl': 4.57.1 - '@rollup/rollup-openbsd-x64': 4.57.1 - '@rollup/rollup-openharmony-arm64': 4.57.1 - '@rollup/rollup-win32-arm64-msvc': 4.57.1 - '@rollup/rollup-win32-ia32-msvc': 4.57.1 - '@rollup/rollup-win32-x64-gnu': 4.57.1 - '@rollup/rollup-win32-x64-msvc': 4.57.1 - fsevents: 2.3.3 - - supports-preserve-symlinks-flag@1.0.0: {} - - tslib@2.8.1: {} + es-toolkit@1.44.0: {} typescript@5.9.3: {} + undici-types@7.16.0: {} + + zod@4.3.6: {} + zustand@5.0.11: {} diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index b3e9782..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,81 +0,0 @@ -// rollup.config.js - -import typescript from '@rollup/plugin-typescript'; -import resolve from '@rollup/plugin-node-resolve'; -import { dts } from 'rollup-plugin-dts'; -/** - * @type {import('rollup').RollupOptions} - */ -export default [ - { - input: 'src/index.ts', // TypeScript 入口文件 - output: { - file: 'dist/query-browser.js', // 输出文件 - format: 'es', // 输出格式设置为 ES 模块 - }, - plugins: [ - resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块 - typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件 - ], - }, - { - input: 'src/index.ts', // TypeScript 入口文件 - output: { - file: 'dist/query-browser.d.ts', // 输出文件 - format: 'es', // 输出格式设置为 ES 模块 - }, - plugins: [dts()], - }, - { - input: 'src/query.ts', - output: { - file: 'dist/query.js', - format: 'es', - }, - plugins: [resolve(), typescript()], - }, - { - input: 'src/query.ts', - output: { - file: 'dist/query.d.ts', - format: 'es', - }, - - plugins: [dts()], - }, - { - input: 'src/ws.ts', // TypeScript 入口文件 - output: { - file: 'dist/query-ws.js', // 输出文件 - format: 'es', // 输出格式设置为 ES 模块 - }, - plugins: [ - resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块 - typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件 - ], - }, - { - input: 'src/ws.ts', // TypeScript 入口文件 - output: { - file: 'dist/query-ws.d.ts', // 输出文件 - format: 'es', // 输出格式设置为 ES 模块 - }, - plugins: [dts()], - }, - { - input: 'src/adapter.ts', - output: { - file: 'dist/query-adapter.js', - format: 'es', - }, - plugins: [resolve(), typescript()], - }, - { - input: 'src/adapter.ts', // TypeScript 入口文件 - output: { - file: 'dist/query-adapter.d.ts', // 输出文件 - format: 'es', // 输出格式设置为 ES 模块 - }, - plugins: [dts()], - }, -]; diff --git a/src/adapter.ts b/src/adapter.ts index 28f150e..d76033e 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -60,7 +60,7 @@ export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit if (opts?.url?.startsWith('http')) { url = new URL(opts.url); } else { - origin = window?.location?.origin || 'http://localhost:51515'; + origin = globalThis?.location?.origin || 'http://localhost:51515'; url = new URL(opts?.url || '', origin); } const isGet = method === 'GET'; diff --git a/src/create-query/index.ts b/src/create-query/index.ts new file mode 100644 index 0000000..1734cb0 --- /dev/null +++ b/src/create-query/index.ts @@ -0,0 +1,130 @@ + +type RouteInfo = { + path: string; + key: string; + id: string; + description?: string; + metadata?: { + summary?: string; + args?: Record; + }; +} +export const createQueryByRoutes = (list: RouteInfo[]) => { + const obj: any = {}; + for (const route of list) { + if (!obj[route.path]) { + obj[route.path] = {}; + } + obj[route.path][route.key] = route; + } + const code = ` +import { createQueryApi } from '@kevisual/query/api'; +const api = ${generateApiCode(obj)} as const; +const queryApi = createQueryApi({ api }); +export { queryApi }; +` + return code; +} + +// 生成带注释的对象字符串 +function generateApiCode(obj: any): string { + let code = '{\n'; + const paths = Object.keys(obj); + + for (let i = 0; i < paths.length; i++) { + const path = paths[i]; + const methods = obj[path]; + + code += ` "${path}": {\n`; + + const keys = Object.keys(methods); + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + const route = methods[key]; + if (route?.id) { + if (route.id.startsWith('rand-')) { + delete route.id; // 删除随机生成的 ID + } + } + const description = route?.metadata?.summary || route?.description || ''; + const args = route?.metadata?.args || {}; + + // 添加 JSDoc 注释 + if (description || Object.keys(args).length > 0) { + code += ` /**\n`; + + // 添加主描述 + if (description) { + // 转义描述中的特殊字符 + const escapedDescription = description + .replace(/\\/g, '\\\\') // 转义反斜杠 + .replace(/\*/g, '\\*') // 转义星号 + .replace(/\n/g, '\n * '); // 处理多行描述 + code += ` * ${escapedDescription}\n`; + } + + // 添加参数描述 + if (Object.keys(args).length > 0) { + if (description) { + code += ` *\n`; // 添加空行分隔 + } + code += ` * @param data - Request parameters\n`; + + for (const [argName, schema] of Object.entries(args)) { + const argSchema = schema as any; + const argType = argSchema.type || 'unknown'; + const argDesc = argSchema.description || ''; + + // 构建类型信息 + let typeInfo = argType; + if (argType === 'string' && argSchema.enum) { + typeInfo = argSchema.enum.map((v: any) => `"${v}"`).join(' | '); + } else if (argType === 'number' || argType === 'integer') { + const constraints = []; + if (argSchema.minimum !== undefined) constraints.push(`min: ${argSchema.minimum}`); + if (argSchema.maximum !== undefined) constraints.push(`max: ${argSchema.maximum}`); + if (argSchema.exclusiveMinimum !== undefined) constraints.push(`> ${argSchema.exclusiveMinimum}`); + if (argSchema.exclusiveMaximum !== undefined) constraints.push(`< ${argSchema.exclusiveMaximum}`); + if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`; + } else if (argType === 'string') { + const constraints = []; + if (argSchema.minLength !== undefined) constraints.push(`minLength: ${argSchema.minLength}`); + if (argSchema.maxLength !== undefined) constraints.push(`maxLength: ${argSchema.maxLength}`); + if (argSchema.format) constraints.push(`format: ${argSchema.format}`); + if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`; + } + + // 转义参数描述 + const escapedArgDesc = argDesc + .replace(/\\/g, '\\\\') + .replace(/\*/g, '\\*') + .replace(/\n/g, ' '); + + code += ` * @param data.${argName} - {${typeInfo}}${escapedArgDesc ? ' ' + escapedArgDesc : ''}\n`; + } + } + + code += ` */\n`; + } + + code += ` "${key}": ${JSON.stringify(route, null, 2).split('\n').map((line, idx) => + idx === 0 ? line : ' ' + line + ).join('\n')}`; + + if (j < keys.length - 1) { + code += ','; + } + code += '\n'; + } + + code += ` }`; + if (i < paths.length - 1) { + code += ','; + } + code += '\n'; + } + + code += '}'; + return code; +} + diff --git a/src/query-api.ts b/src/query-api.ts new file mode 100644 index 0000000..50359a7 --- /dev/null +++ b/src/query-api.ts @@ -0,0 +1,136 @@ +import { DataOpts, Query } from "./query.ts"; +import { z } from "zod"; +import { createQueryByRoutes } from "./create-query/index.ts"; +import { pick } from 'es-toolkit' +type Pos = { + path?: string; + key?: string; + id?: string; + metadata?: { + args?: Record; + }; +} + +// JSON Schema 类型推断 - 使用更精确的类型匹配 +type InferFromJSONSchema = + T extends { type: "string"; enum: readonly (infer E)[] } ? E : + T extends { type: "string"; enum: (infer E)[] } ? E : + T extends { type: "string" } ? string : + T extends { type: "number" } ? number : + T extends { type: "integer" } ? number : + T extends { type: "boolean" } ? boolean : + T extends { type: "object"; properties: infer P } + ? { [K in keyof P]: InferFromJSONSchema } + : T extends { type: "array"; items: infer I } + ? Array> + : unknown; + +// 统一类型推断:支持 Zod schema 和原始 JSON Schema +type InferType = + T extends z.ZodType ? U : // Zod schema + T extends { type: infer TType } ? InferFromJSONSchema : // 任何包含 type 字段的 JSON Schema(忽略 $schema) + T; + +// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型 +type ExtractArgsFromMetadata = T extends { metadata?: { args?: infer A } } + ? A extends Record + ? { [K in keyof A]: InferType } + : never + : never; + +// 类型映射:将 API 配置转换为方法签名 +type ApiMethods

= { + [Path in keyof P]: { + [Key in keyof P[Path]]: ( + data?: Partial>, + opts?: DataOpts + ) => ReturnType + } +} +type QueryApiOpts

= { + query?: Query, + api?: P +} +export class QueryApi

{ + query: Query; + + constructor(opts?: QueryApiOpts

) { + this.query = opts?.query ?? new Query(); + if (opts?.api) { + this.createApi(opts.api); + } + } + + // 使用泛型来推断类型 + post( + pos: T, + data?: Partial>, + opts?: DataOpts + ) { + const _pos = pick(pos, ['path', 'key', 'id']); + return this.query.post({ + ..._pos, + payload: data + }, opts) + } + + createApi(api: P): asserts this is this & ApiMethods

{ + const that = this as any; + const apiEntries = Object.entries(api); + const keepPaths = ['createApi', 'query', 'post']; + + for (const [path, methods] of apiEntries) { + if (keepPaths.includes(path)) continue; + + // 为每个 path 创建命名空间对象 + if (!that[path]) { + that[path] = {}; + } + + for (const [key, pos] of Object.entries(methods)) { + that[path][key] = (data?: Partial>, opts?: DataOpts) => { + const _pos = pick(pos, ['path', 'key', 'id']); + return that.query.post({ + ..._pos, + payload: data + }, opts); + }; + } + } + } +} + +// 创建工厂函数,提供更好的类型推断 +export function createQueryApi

( + opts?: QueryApiOpts

+): QueryApi

& ApiMethods

{ + return new QueryApi(opts) as QueryApi

& ApiMethods

; +} + +export { createQueryByRoutes }; +// const demo = { +// "test_path": { +// "test_key": { +// "path": "demo", +// "key": "test", +// metadata: { +// args: { +// name: z.string(), +// age: z.number(), +// } +// } +// } +// } +// } as const; + +// // 方式1: 使用工厂函数创建(推荐) +// const queryApi = createQueryApi({ query: new Query(), api: demo }); + +// // 现在调用时会有完整的类型推断 +// // data 参数会被推断为 { name?: string, age?: number } +// queryApi.test_path.test_key({ name: "test", age: 18 }); +// // 也可以不传参数 +// queryApi.test_path.test_key(); + +// // 或者只传递 opts +// queryApi.test_path.test_key(undefined, { timeout: 5000 }); \ No newline at end of file diff --git a/src/query-browser.ts b/src/query-browser.ts index fcdfb69..a670508 100644 --- a/src/query-browser.ts +++ b/src/query-browser.ts @@ -1,9 +1,9 @@ import { adapter } from './adapter.ts'; import { QueryWs, QueryWsOpts } from './ws.ts'; -import { Query, ClientQuery } from './query.ts'; +import { Query } from './query.ts'; import { BaseQuery, QueryOptions, wrapperError } from './query.ts'; -export { QueryOpts, QueryWs, ClientQuery, Query, QueryWsOpts, adapter, BaseQuery, wrapperError }; +export { QueryOpts, QueryWs, Query, QueryWsOpts, adapter, BaseQuery, wrapperError }; export { QueryOptions } export type { DataOpts, Result, Data } from './query.ts'; diff --git a/src/query.ts b/src/query.ts index 03983f0..0d4d0f4 100644 --- a/src/query.ts +++ b/src/query.ts @@ -162,6 +162,7 @@ export class Query { */ async post(body: Data & P, options?: DataOpts): Promise> { const url = options?.url || this.url; + console.log('query post', url, body, options); const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {}; const _headers = { ...this.headers, ...headers }; const _adapter = adapter || this.adapter; @@ -301,13 +302,3 @@ export class BaseQuery { + return util.inspect(data, { depth: null, colors: true }); +} +const routes = await app.run({ path: 'router', key: 'list' }) +// console.log('rourtes', showMore(routes.data.list)); +const list = routes.data.list + +const obj: any = {} + +for (const route of list) { + if (!obj[route.path]) { + obj[route.path] = {}; + } + obj[route.path][route.key] = route; +} + +// console.log('obj', showMore(obj)); + +const code = createQueryByRoutes(list); + +fs.writeFileSync('test/query.ts', code, 'utf-8'); + diff --git a/test/query.ts b/test/query.ts new file mode 100644 index 0000000..8fd8615 --- /dev/null +++ b/test/query.ts @@ -0,0 +1,472 @@ + +import { createQueryApi } from '@kevisual/query/api'; +const api = { + "test": { + /** + * test route + * + * @param data - Request parameters + * @param data.a - {string} arg a + */ + "test": { + "path": "test", + "key": "test", + "description": "test route", + "type": "route", + "middleware": [], + "metadata": { + "args": { + "a": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "arg a", + "type": "string" + } + } + } + } + }, + "demo": { + /** + * First demo route demonstrating string and number parameters + * + * @param data - Request parameters + * @param data.username - {string (minLength: 3, maxLength: 20)} The username to be validated, must be between 3 and 20 characters + * @param data.age - {number (min: 18, max: 100)} The age of the user, must be between 18 and 100 + * @param data.email - {string (format: email)} The email address of the user for notification purposes + * @param data.count - {integer (max: 9007199254740991, > 0)} The number of items to process, must be a positive integer + * @param data.name - {string} The display name of the user + */ + "d1": { + "path": "demo", + "key": "d1", + "description": "First demo route demonstrating string and number parameters", + "type": "route", + "middleware": [], + "metadata": { + "args": { + "username": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 3, + "maxLength": 20, + "description": "The username to be validated, must be between 3 and 20 characters" + }, + "age": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 18, + "maximum": 100, + "description": "The age of the user, must be between 18 and 100" + }, + "email": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "The email address of the user for notification purposes" + }, + "count": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "The number of items to process, must be a positive integer" + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "The display name of the user" + } + } + } + }, + /** + * Second demo route for boolean and enum parameters + * + * @param data - Request parameters + * @param data.isActive - {boolean} Whether the user account is currently active and accessible + * @param data.isAdmin - {boolean} Whether the user has administrative privileges + * @param data.notifications - {boolean} Whether to enable email and push notifications + * @param data.mode - {"read" | "write" | "execute"} The operation mode for the current session + * @param data.verified - {boolean} Whether the user email has been verified + */ + "d2": { + "path": "demo", + "key": "d2", + "description": "Second demo route for boolean and enum parameters", + "type": "route", + "middleware": [], + "metadata": { + "args": { + "isActive": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "boolean", + "description": "Whether the user account is currently active and accessible" + }, + "isAdmin": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "boolean", + "description": "Whether the user has administrative privileges" + }, + "notifications": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "boolean", + "description": "Whether to enable email and push notifications" + }, + "mode": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "enum": [ + "read", + "write", + "execute" + ], + "description": "The operation mode for the current session" + }, + "verified": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "boolean", + "description": "Whether the user email has been verified" + } + } + } + }, + /** + * Third demo route handling array and optional parameters + * + * @param data - Request parameters + * @param data.tags - {array} List of tags associated with the content, between 1 and 10 tags + * @param data.categories - {array} List of category names for filtering and classification + * @param data.ids - {array} Array of numeric identifiers for the resources + * @param data.priority - {number (min: 1, max: 5)} Priority level from 1 to 5, defaults to 3 if not specified + * @param data.keywords - {array} Keywords for search optimization, up to 20 keywords + */ + "d3": { + "path": "demo", + "key": "d3", + "description": "Third demo route handling array and optional parameters", + "type": "route", + "middleware": [], + "metadata": { + "args": { + "tags": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minItems": 1, + "maxItems": 10, + "type": "array", + "items": { + "type": "string" + }, + "description": "List of tags associated with the content, between 1 and 10 tags" + }, + "categories": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { + "type": "string" + }, + "description": "List of category names for filtering and classification" + }, + "ids": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "description": "Array of numeric identifiers for the resources" + }, + "priority": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Priority level from 1 to 5, defaults to 3 if not specified", + "type": "number", + "minimum": 1, + "maximum": 5 + }, + "keywords": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxItems": 20, + "type": "array", + "items": { + "type": "string" + }, + "description": "Keywords for search optimization, up to 20 keywords" + } + } + } + }, + /** + * Fourth demo route with nested object parameters + * + * @param data - Request parameters + * @param data.user - {object} Complete user profile information + * @param data.settings - {object} User preference settings + * @param data.address - {object} Mailing address, optional field + */ + "d4": { + "path": "demo", + "key": "d4", + "description": "Fourth demo route with nested object parameters", + "type": "route", + "middleware": [], + "metadata": { + "args": { + "user": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "Unique identifier for the user" + }, + "name": { + "type": "string", + "description": "Full name of the user" + }, + "contact": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Primary email address" + }, + "phone": { + "description": "Phone number with country code", + "type": "string" + } + }, + "required": [ + "email" + ], + "additionalProperties": false, + "description": "Contact information for the user" + } + }, + "required": [ + "id", + "name", + "contact" + ], + "additionalProperties": false, + "description": "Complete user profile information" + }, + "settings": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "auto" + ], + "description": "UI theme preference" + }, + "language": { + "default": "en", + "description": "Preferred language code", + "type": "string" + }, + "timezone": { + "type": "string", + "description": "Timezone identifier like America/New_York" + } + }, + "required": [ + "theme", + "language", + "timezone" + ], + "additionalProperties": false, + "description": "User preference settings" + }, + "address": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Mailing address, optional field", + "type": "object", + "properties": { + "street": { + "type": "string", + "description": "Street address line" + }, + "city": { + "type": "string", + "description": "City name" + }, + "country": { + "type": "string", + "description": "Country code or name" + } + }, + "required": [ + "street", + "city", + "country" + ], + "additionalProperties": false + } + } + } + }, + /** + * Fifth demo route with mixed complex parameters and validation + * + * @param data - Request parameters + * @param data.query - {string (minLength: 1)} Search query string, minimum 1 character required + * @param data.filters - {object} Advanced search filters configuration + * @param data.pagination - {object} Pagination settings for query results + * @param data.includeMetadata - {boolean} Whether to include metadata in response + * @param data.timeout - {number (min: 1000, max: 30000)} Request timeout in milliseconds, between 1s and 30s + * @param data.retry - {integer (min: 0, max: 5)} Number of retry attempts on failure + */ + "d5": { + "path": "demo", + "key": "d5", + "description": "Fifth demo route with mixed complex parameters and validation", + "type": "route", + "middleware": [], + "metadata": { + "args": { + "query": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 1, + "description": "Search query string, minimum 1 character required" + }, + "filters": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "all", + "image", + "video", + "audio", + "document" + ], + "description": "Content type filter" + }, + "dateRange": { + "description": "Date range filter, optional", + "type": "object", + "properties": { + "start": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Start date in ISO 8601 format" + }, + "end": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "End date in ISO 8601 format" + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + }, + "size": { + "description": "Size filter for media content", + "type": "string", + "enum": [ + "small", + "medium", + "large", + "extra-large" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Advanced search filters configuration" + }, + "pagination": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "page": { + "default": 1, + "description": "Page number starting from 1", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "limit": { + "default": 20, + "description": "Number of items per page, max 100", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 100 + }, + "sort": { + "default": "desc", + "description": "Sort order for results", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "required": [ + "page", + "limit", + "sort" + ], + "additionalProperties": false, + "description": "Pagination settings for query results" + }, + "includeMetadata": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "default": false, + "description": "Whether to include metadata in response", + "type": "boolean" + }, + "timeout": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Request timeout in milliseconds, between 1s and 30s", + "type": "number", + "minimum": 1000, + "maximum": 30000 + }, + "retry": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "default": 3, + "description": "Number of retry attempts on failure", + "type": "integer", + "minimum": 0, + "maximum": 5 + } + } + } + } + }, + "router": { + /** + * 列出当前应用下的所有的路由信息 + */ + "list": { + "path": "router", + "key": "list", + "description": "列出当前应用下的所有的路由信息", + "type": "route", + "middleware": [] + } + } +} as const; +const queryApi = createQueryApi({ api }); +export { queryApi }; diff --git a/test/router.ts b/test/router.ts new file mode 100644 index 0000000..07968c5 --- /dev/null +++ b/test/router.ts @@ -0,0 +1,131 @@ +import { App } from '@kevisual/router'; +import { z } from 'zod'; +export const app = new App({}); + +app + .route({ + path: 'test', + key: 'test', + description: 'test route', + metadata: { + args: { + a: z.string().optional().describe('arg a'), + } + } + }) + .define(async (ctx) => { + ctx.body = 'test'; + }) + .addTo(app); + +app.route({ + path: 'demo', + key: 'd1', + description: 'First demo route demonstrating string and number parameters', + metadata: { + args: { + username: z.string().min(3).max(20).describe('The username to be validated, must be between 3 and 20 characters'), + age: z.number().min(18).max(100).describe('The age of the user, must be between 18 and 100'), + email: z.email().describe('The email address of the user for notification purposes'), + count: z.number().int().positive().describe('The number of items to process, must be a positive integer'), + name: z.string().describe('The display name of the user'), + } + } +}).define(async (ctx) => { + ctx.body = 'demo1'; +}).addTo(app); + +app.route({ + path: 'demo', + key: 'd2', + description: 'Second demo route for boolean and enum parameters', + metadata: { + args: { + isActive: z.boolean().describe('Whether the user account is currently active and accessible'), + isAdmin: z.boolean().describe('Whether the user has administrative privileges'), + notifications: z.boolean().describe('Whether to enable email and push notifications'), + mode: z.enum(['read', 'write', 'execute']).describe('The operation mode for the current session'), + verified: z.boolean().describe('Whether the user email has been verified'), + } + } +}).define(async (ctx) => { + ctx.body = 'demo2'; +}).addTo(app); + +app.route({ + path: 'demo', + key: 'd3', + description: 'Third demo route handling array and optional parameters', + metadata: { + args: { + tags: z.array(z.string()).min(1).max(10).describe('List of tags associated with the content, between 1 and 10 tags'), + categories: z.array(z.string()).describe('List of category names for filtering and classification'), + ids: z.array(z.number().int().positive()).describe('Array of numeric identifiers for the resources'), + priority: z.number().min(1).max(5).optional().describe('Priority level from 1 to 5, defaults to 3 if not specified'), + keywords: z.array(z.string()).max(20).describe('Keywords for search optimization, up to 20 keywords'), + } + } +}).define(async (ctx) => { + ctx.body = 'demo3'; +}).addTo(app); + +app.route({ + path: 'demo', + key: 'd4', + description: 'Fourth demo route with nested object parameters', + metadata: { + args: { + user: z.object({ + id: z.number().int().positive().describe('Unique identifier for the user'), + name: z.string().describe('Full name of the user'), + contact: z.object({ + email: z.email().describe('Primary email address'), + phone: z.string().optional().describe('Phone number with country code'), + }).describe('Contact information for the user'), + }).describe('Complete user profile information'), + settings: z.object({ + theme: z.enum(['light', 'dark', 'auto']).describe('UI theme preference'), + language: z.string().default('en').describe('Preferred language code'), + timezone: z.string().describe('Timezone identifier like America/New_York'), + }).describe('User preference settings'), + address: z.object({ + street: z.string().describe('Street address line'), + city: z.string().describe('City name'), + country: z.string().describe('Country code or name'), + }).optional().describe('Mailing address, optional field'), + } + } +}).define(async (ctx) => { + ctx.body = 'demo4'; +}).addTo(app); + +app.route({ + path: 'demo', + key: 'd5', + description: 'Fifth demo route with mixed complex parameters and validation', + metadata: { + args: { + query: z.string().min(1).describe('Search query string, minimum 1 character required'), + filters: z.object({ + type: z.enum(['all', 'image', 'video', 'audio', 'document']).describe('Content type filter'), + dateRange: z.object({ + start: z.iso.datetime().describe('Start date in ISO 8601 format'), + end: z.iso.datetime().describe('End date in ISO 8601 format'), + }).optional().describe('Date range filter, optional'), + size: z.enum(['small', 'medium', 'large', 'extra-large']).optional().describe('Size filter for media content'), + }).describe('Advanced search filters configuration'), + pagination: z.object({ + page: z.number().int().positive().default(1).describe('Page number starting from 1'), + limit: z.number().int().positive().max(100).default(20).describe('Number of items per page, max 100'), + sort: z.enum(['asc', 'desc']).default('desc').describe('Sort order for results'), + }).describe('Pagination settings for query results'), + includeMetadata: z.boolean().default(false).describe('Whether to include metadata in response'), + timeout: z.number().min(1000).max(30000).optional().describe('Request timeout in milliseconds, between 1s and 30s'), + retry: z.number().int().min(0).max(5).default(3).describe('Number of retry attempts on failure'), + } + } +}).define(async (ctx) => { + ctx.body = 'demo5'; +}).addTo(app); + +app.createRouteList() \ No newline at end of file diff --git a/test/schema.ts b/test/schema.ts new file mode 100644 index 0000000..9896da3 --- /dev/null +++ b/test/schema.ts @@ -0,0 +1,43 @@ +import z, { toJSONSchema } from "zod"; + +const schema = z.object({ + name: z.string().describe("The name of the person"), + age: z.number().int().min(0).describe("The age of the person"), + email: z.string().optional().describe("The email address of the person"), +}); + +console.log("JSON Schema for the person object:"); +console.log( + JSON.stringify(toJSONSchema(schema), null, 2) +); +const jsonSchema = toJSONSchema(schema); + +const schema2 = z.fromJSONSchema(jsonSchema); + +// schema2 的类型是 ZodSchema,所以无法在编译时推断出具体类型 +// 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息 + +schema2.parse({ + name: "John Doe", + age: 30, // 添加必需的 age 字段 + email: "", +}) + +type Schema2Type = z.infer; +// Schema2Type 被推断为 any + +// 对比:原始 schema 的类型推断是正常的 +type OriginalSchemaType = z.infer; +// OriginalSchemaType = { name: string; age: number; email?: string | undefined } + +const v: Schema2Type = { + name: "John Doe", + email: "" +} + +// 如果使用原始 schema,类型推断会正常工作: +const v2: OriginalSchemaType = { + name: "John Doe", + age: 30, + // email 是可选的 +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 359421b..cbbe5a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "sourceMap": false, "allowJs": true, "newLine": "LF", + "rootDir": ".", "baseUrl": "./", "declaration": false, "typeRoots": [ @@ -21,20 +22,17 @@ "paths": { "@/*": [ "src/*" - ], - "*": [ - "types/*" ] } }, "include": [ "typings.d.ts", - "src/**/*.ts" + "src/**/*.ts", + "test/**/*.ts" ], "exclude": [ "node_modules", "demo/simple/dist", "src/**/*.test.ts", - "rollup.config.js", ] } \ No newline at end of file