├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── README_CN.md ├── eslint.config.js ├── package.json ├── packages ├── core │ ├── bin │ │ ├── index.dev.mjs │ │ └── index.mjs │ ├── package.json │ ├── src │ │ ├── cli │ │ │ ├── index.ts │ │ │ ├── parser.ts │ │ │ └── utils.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── generator │ │ │ └── index.ts │ │ ├── index.ts │ │ └── internal │ │ │ └── index.ts │ └── tsdown.config.ts ├── parser │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── parses │ │ │ ├── common.ts │ │ │ ├── index.ts │ │ │ ├── method.ts │ │ │ ├── parameter.ts │ │ │ └── schema.ts │ │ ├── transform │ │ │ ├── body.ts │ │ │ ├── definitions.ts │ │ │ ├── headers.ts │ │ │ ├── index.ts │ │ │ ├── parameters.ts │ │ │ └── urls.ts │ │ ├── traverse │ │ │ ├── index.ts │ │ │ └── paths.ts │ │ └── utils │ │ │ ├── format.ts │ │ │ ├── index.ts │ │ │ ├── is.ts │ │ │ ├── literal.ts │ │ │ └── utils.ts │ └── tsdown.config.ts ├── pipeline │ ├── package.json │ ├── src │ │ ├── compiler │ │ │ ├── index.ts │ │ │ ├── request.ts │ │ │ └── typings.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── dest │ │ │ └── index.ts │ │ ├── generate │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── original │ │ │ └── index.ts │ │ ├── pipeline │ │ │ └── index.ts │ │ └── types │ │ │ └── prettier.d.ts │ └── tsdown.config.ts ├── presets │ ├── package.json │ ├── src │ │ ├── axios │ │ │ ├── index.ts │ │ │ ├── js │ │ │ │ ├── config │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ │ └── index.ts │ │ │ └── ts │ │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ └── index.ts │ │ ├── fetch │ │ │ ├── index.ts │ │ │ ├── js │ │ │ │ ├── config │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ │ └── index.ts │ │ │ └── ts │ │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ └── index.ts │ │ ├── got │ │ │ ├── index.ts │ │ │ ├── js │ │ │ │ ├── config │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ │ └── index.ts │ │ │ └── ts │ │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── ky │ │ │ ├── index.ts │ │ │ ├── js │ │ │ │ ├── config │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ │ └── index.ts │ │ │ └── ts │ │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── parser │ │ │ │ └── index.ts │ │ └── ofetch │ │ │ ├── index.ts │ │ │ ├── js │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── parser │ │ │ │ └── index.ts │ │ │ └── ts │ │ │ ├── config │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── parser │ │ │ └── index.ts │ └── tsdown.config.ts ├── shared │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ └── statement.ts │ └── tsdown.config.ts └── transform │ ├── package.json │ ├── src │ ├── index.ts │ ├── swagger2-to-swagger3 │ │ └── index.ts │ └── wpapi-to-swagger2 │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsdown.config.ts ├── playground ├── genapi.config.ts └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public ├── case.gif └── swag-axios-js.png ├── test └── index.test.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | run_install: false 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | cache: pnpm 24 | 25 | - run: pnpm i -g @antfu/ni 26 | - run: nci 27 | - run: nr lint 28 | - run: nr typecheck 29 | 30 | test: 31 | runs-on: ${{ matrix.os }} 32 | 33 | strategy: 34 | matrix: 35 | node: [lts/*] 36 | os: [ubuntu-latest, windows-latest, macos-latest] 37 | fail-fast: false 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: pnpm/action-setup@v4 42 | with: 43 | run_install: false 44 | - name: Set node ${{ matrix.node }} 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ matrix.node }} 48 | cache: pnpm 49 | 50 | - run: pnpm i -g @antfu/ni 51 | - run: nci 52 | - run: nr test 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | - run: pnpm dlx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | 29 | # # Uncomment the following lines to publish to npm on CI 30 | # 31 | # - run: pnpm install 32 | # - run: pnpm publish -r --access public 33 | # env: 34 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | # NPM_CONFIG_PROVENANCE: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | cache 13 | .eslintcache 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off" }, 15 | { "rule": "*-indent", "severity": "off" }, 16 | { "rule": "*-spacing", "severity": "off" }, 17 | { "rule": "*-spaces", "severity": "off" }, 18 | { "rule": "*-order", "severity": "off" }, 19 | { "rule": "*-dangle", "severity": "off" }, 20 | { "rule": "*-newline", "severity": "off" }, 21 | { "rule": "*quotes", "severity": "off" }, 22 | { "rule": "*semi", "severity": "off" } 23 | ], 24 | 25 | // Enable eslint for all supported languages 26 | "eslint.validate": [ 27 | "javascript", 28 | "javascriptreact", 29 | "typescript", 30 | "typescriptreact", 31 | "vue", 32 | "html", 33 | "markdown", 34 | "json", 35 | "jsonc", 36 | "yaml" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-PRESENT Hairyf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # genapi 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | > [中文](./README_CN.md) | English 10 | 11 | API generator that converts OpenAPI (v2~v3) and other input sources into TypeScript/JavaScript APIs. 12 | 13 | ## Features 14 | 15 | - 🚀 **Multiple HTTP Clients** - Support for various HTTP clients: 16 | - `axios` - Popular promise-based HTTP client 17 | - `fetch` - Native browser fetch API 18 | - `ky` - Tiny and elegant HTTP client 19 | - `got` - Human-friendly HTTP request library 20 | - `ofetch` - A better fetch API with TypeScript support 21 | 22 | - 🔄 **Language Support** - Generate both TypeScript and JavaScript APIs: 23 | - `swag-axios-ts` / `swag-axios-js` 24 | - `swag-fetch-ts` / `swag-fetch-js` 25 | - `swag-ky-ts` / `swag-ky-js` 26 | - `swag-got-ts` / `swag-got-js` 27 | - `swag-ofetch-ts` / `swag-ofetch-js` 28 | 29 | - 🛠️ **Customizable** - Flexible pipeline system for customizing the generation process 30 | 31 | ## Installation 32 | 33 | ```bash 34 | # pnpm 35 | npm i @genapi/core @genapi/presets -D 36 | ``` 37 | 38 | > You can also install it globally but it's not recommended. 39 | 40 | ## Usage 41 | 42 | Create a configuration file in your project root: 43 | 44 | - `genapi.config.ts` 45 | - `genapi.config.js` 46 | - `genapi.config.json` 47 | 48 | ```ts 49 | import { defineConfig } from '@genapi/core' 50 | import { axios } from '@genapi/presets' 51 | 52 | export default defineConfig({ 53 | pipeline: axios.ts, 54 | // your input source (swagger api url or json) 55 | input: 'http://example.com/api-docs', 56 | output: { 57 | main: 'src/api/index.ts', 58 | type: 'src/api/index.type.ts', 59 | }, 60 | 61 | // your API baseUrl 62 | baseURL: 'import.meta.env.VITE_APP_BASE_API', 63 | // customize the output response type. default 'T' 64 | responseType: 'T extends { data?: infer V } ? V : void', 65 | }) 66 | ``` 67 | 68 | Then run: 69 | 70 | ```bash 71 | npm run genapi 72 | ``` 73 | 74 | ## Input Sources 75 | 76 | Input supports URL or JSON format: 77 | 78 | ```ts 79 | export default defineConfig({ 80 | // directly pass in url 81 | input: 'http://example.com/api-docs', 82 | // or JSON object 83 | input: { /* url|json */ } 84 | }) 85 | ``` 86 | 87 | ## Multiple Services 88 | 89 | For projects with multiple services, use the `server` configuration: 90 | 91 | ```ts 92 | export default defineConfig({ 93 | // Your API baseUrl, this configuration will be passed to the axios request 94 | baseUrl: 'https://example.com/api', 95 | // all servers inherit the upper layer configuration 96 | server: [ 97 | { input: 'http://service1/api-docs', output: { main: 'src/api/service1.ts' } }, 98 | { input: 'http://service2/api-docs', output: { main: 'src/api/service2.ts' } }, 99 | { input: 'http://service3/api-docs', output: { main: 'src/api/service3.ts' } }, 100 | ] 101 | }) 102 | ``` 103 | 104 | ## swag-axios-js 105 | 106 | Use any `js` pipeline to generate JavaScript files with types: 107 | 108 | ```ts 109 | import { defineConfig } from '@genapi/core' 110 | import { axios } from '@genapi/presets' 111 | 112 | export default defineConfig({ 113 | pipeline: axios.js, 114 | input: { 115 | uri: 'https://petstore.swagger.io/v2/swagger.json', 116 | }, 117 | }) 118 | ``` 119 | 120 | Run `genapi` and get: 121 | 122 | ![swag-axios-js](public/swag-axios-js.png) 123 | 124 | ## Custom Pipeline 125 | 126 | Pipeline is the core of genapi. You can create custom pipelines: 127 | 128 | ```ts 129 | // create an API pipeline generator using the pipeline provided by genapi 130 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 131 | // each pipeline exposes corresponding methods, which can be reused and reorganized 132 | import { axios } from '@genapi/presets' 133 | 134 | export default defineConfig({ 135 | pipeline: pipeline( 136 | // read config, convert to internal config, and provide default values 137 | config => axios.ts.config(config), 138 | // get data source 139 | configRead => original(configRead), 140 | // parse the data source as data graphs 141 | configRead => axios.ts.parser(configRead), 142 | // compile data and convert it into abstract syntax tree (AST) 143 | configRead => compiler(configRead), 144 | // generate code string 145 | configRead => generate(configRead), 146 | // use outputs to output files 147 | configRead => dest(configRead), 148 | ), 149 | }) 150 | ``` 151 | 152 | ## License 153 | 154 | [MIT](./LICENSE) License © [Hairyf](https://github.com/hairyf) 155 | 156 | 157 | 158 | [npm-version-src]: https://img.shields.io/npm/v/@genapi/core?style=flat&colorA=080f12&colorB=1fa669 159 | [npm-version-href]: https://npmjs.com/package/@genapi/core 160 | [npm-downloads-src]: https://img.shields.io/npm/dm/@genapi/core?style=flat&colorA=080f12&colorB=1fa669 161 | [npm-downloads-href]: https://npmjs.com/package/@genapi/core 162 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@genapi/core?style=flat&colorA=080f12&colorB=1fa669&label=minzip 163 | [bundle-href]: https://bundlephobia.com/result?p=@genapi/core 164 | [license-src]: https://img.shields.io/github/license/hairyf/genapi.svg?style=flat&colorA=080f12&colorB=1fa669 165 | [license-href]: https://github.com/hairyf/genapi/blob/main/LICENSE 166 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 167 | [jsdocs-href]: https://www.jsdocs.io/package/@genapi/core 168 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # genapi 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | API生成器,用于将OpenAPI(v2~v3)和其他输入源转换为TypeScript/JavaScript API。 10 | 11 | ## 特性 12 | 13 | - 🚀 **多种HTTP客户端** - 支持各种HTTP客户端: 14 | - `axios` - 流行的基于Promise的HTTP客户端 15 | - `fetch` - 原生浏览器fetch API 16 | - `ky` - 小巧优雅的HTTP客户端 17 | - `got` - 人性化的HTTP请求库 18 | - `ofetch` - 更好的fetch API,带有TypeScript支持 19 | 20 | - 🔄 **语言支持** - 生成TypeScript和JavaScript API: 21 | - `swag-axios-ts` / `swag-axios-js` 22 | - `swag-fetch-ts` / `swag-fetch-js` 23 | - `swag-ky-ts` / `swag-ky-js` 24 | - `swag-got-ts` / `swag-got-js` 25 | - `swag-ofetch-ts` / `swag-ofetch-js` 26 | 27 | - 🛠️ **可定制** - 灵活的管道系统,用于自定义生成过程 28 | 29 | ## 安装 30 | 31 | ```bash 32 | # pnpm 33 | npm i @genapi/core @genapi/presets -D 34 | ``` 35 | 36 | > 你也可以全局安装,但不推荐这样做。 37 | 38 | ## 使用方法 39 | 40 | 在项目根目录创建配置文件: 41 | 42 | - `genapi.config.ts` 43 | - `genapi.config.js` 44 | - `genapi.config.json` 45 | 46 | ```ts 47 | import { defineConfig } from '@genapi/core' 48 | import { axios } from '@genapi/presets' 49 | 50 | export default defineConfig({ 51 | pipeline: axios.ts, 52 | // 你的输入源(swagger api url或json) 53 | input: 'http://example.com/api-docs', 54 | output: { 55 | main: 'src/api/index.ts', 56 | type: 'src/api/index.type.ts', 57 | }, 58 | 59 | // 你的API基础URL 60 | baseURL: 'import.meta.env.VITE_APP_BASE_API', 61 | // 自定义输出响应类型,默认为'T' 62 | responseType: 'T extends { data?: infer V } ? V : void', 63 | }) 64 | ``` 65 | 66 | 然后运行: 67 | 68 | ```bash 69 | npm run genapi 70 | ``` 71 | 72 | ## 输入源 73 | 74 | 输入支持URL或JSON格式: 75 | 76 | ```ts 77 | export default defineConfig({ 78 | // 直接传入url 79 | input: 'http://example.com/api-docs', 80 | // 或JSON对象 81 | input: { /* url|json */ } 82 | }) 83 | ``` 84 | 85 | ## 多服务配置 86 | 87 | 对于有多个服务的项目,使用`server`配置: 88 | 89 | ```ts 90 | export default defineConfig({ 91 | // 你的API基础URL,此配置将传递给axios请求 92 | baseUrl: 'https://example.com/api', 93 | // 所有服务器继承上层配置 94 | server: [ 95 | { input: 'http://service1/api-docs', output: { main: 'src/api/service1.ts' } }, 96 | { input: 'http://service2/api-docs', output: { main: 'src/api/service2.ts' } }, 97 | { input: 'http://service3/api-docs', output: { main: 'src/api/service3.ts' } }, 98 | ] 99 | }) 100 | ``` 101 | 102 | ## swag-axios-js 103 | 104 | 使用任何`js`管道生成带有类型的JavaScript文件: 105 | 106 | ```ts 107 | import { defineConfig } from '@genapi/core' 108 | import { axios } from '@genapi/presets' 109 | 110 | export default defineConfig({ 111 | pipeline: axios.js, 112 | input: { 113 | uri: 'https://petstore.swagger.io/v2/swagger.json', 114 | }, 115 | }) 116 | ``` 117 | 118 | 运行`genapi`后得到: 119 | 120 | ![swag-axios-js](public/swag-axios-js.png) 121 | 122 | ## 自定义管道 123 | 124 | 管道是genapi的核心。你可以创建自定义管道: 125 | 126 | ```ts 127 | // 使用genapi提供的管道创建API管道生成器 128 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 129 | // 每个管道都暴露相应的方法,可以重用和重组 130 | import { axios } from '@genapi/presets' 131 | 132 | export default defineConfig({ 133 | pipeline: pipeline( 134 | // 读取配置,转换为内部配置,并提供默认值 135 | config => axios.ts.config(config), 136 | // 获取数据源 137 | configRead => original(configRead), 138 | // 将数据源解析为数据图 139 | configRead => axios.ts.parser(configRead), 140 | // 编译数据并转换为抽象语法树(AST) 141 | configRead => compiler(configRead), 142 | // 生成代码字符串 143 | configRead => generate(configRead), 144 | // 使用输出到输出文件 145 | configRead => dest(configRead), 146 | ), 147 | }) 148 | ``` 149 | 150 | ## License 151 | 152 | [MIT](./LICENSE) License © [Hairyf](https://github.com/hairyf) 153 | 154 | 155 | 156 | [npm-version-src]: https://img.shields.io/npm/v/@genapi/core?style=flat&colorA=080f12&colorB=1fa669 157 | [npm-version-href]: https://npmjs.com/package/@genapi/core 158 | [npm-downloads-src]: https://img.shields.io/npm/dm/@genapi/core?style=flat&colorA=080f12&colorB=1fa669 159 | [npm-downloads-href]: https://npmjs.com/package/@genapi/core 160 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@genapi/core?style=flat&colorA=080f12&colorB=1fa669&label=minzip 161 | [bundle-href]: https://bundlephobia.com/result?p=@genapi/core 162 | [license-src]: https://img.shields.io/github/license/hairyf/genapi.svg?style=flat&colorA=080f12&colorB=1fa669 163 | [license-href]: https://github.com/hairyf/genapi/blob/main/LICENSE 164 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 165 | [jsdocs-href]: https://www.jsdocs.io/package/@genapi/core 166 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu( 5 | { 6 | type: 'app', 7 | }, 8 | { 9 | rules: { 10 | 'ts/no-namespace': 'off', 11 | }, 12 | }, 13 | ) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "3.5.0", 4 | "private": true, 5 | "packageManager": "pnpm@10.4.0", 6 | "scripts": { 7 | "dev": "nr -r dev", 8 | "build": "nr -r build", 9 | "play": "pnpm -C playground run dev", 10 | "lint": "eslint --cache .", 11 | "prepublishOnly": "nr build", 12 | "docs": "pnpm -C docs run docs:dev", 13 | "docs:build": "pnpm -C docs run docs:build", 14 | "release": "bumpp -r && pnpm -r publish --access public", 15 | "test": "vitest", 16 | "typecheck": "tsc --noEmit", 17 | "prepare": "simple-git-hooks" 18 | }, 19 | "devDependencies": { 20 | "@antfu/eslint-config": "catalog:", 21 | "@antfu/ni": "catalog:", 22 | "@antfu/utils": "catalog:", 23 | "@types/node": "catalog:", 24 | "bumpp": "catalog:", 25 | "esbuild-plugin-file-path-extensions": "^2.1.4", 26 | "eslint": "catalog:", 27 | "lint-staged": "catalog:", 28 | "pnpm": "catalog:", 29 | "simple-git-hooks": "catalog:", 30 | "tsdown": "^0.8.1", 31 | "tsup": "^8.4.0", 32 | "tsx": "catalog:", 33 | "typescript": "catalog:", 34 | "unbuild": "catalog:", 35 | "vite": "catalog:", 36 | "vitest": "catalog:" 37 | }, 38 | "simple-git-hooks": { 39 | "pre-commit": "pnpm lint-staged" 40 | }, 41 | "lint-staged": { 42 | "*": "eslint --fix" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/bin/index.dev.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | import { register } from 'tsx/esm/api' 5 | 6 | // Register tsx enhancement 7 | const unregister = register() 8 | 9 | // eslint-disable-next-line antfu/no-top-level-await 10 | await import('../src/cli/index.ts') 11 | 12 | unregister() 13 | -------------------------------------------------------------------------------- /packages/core/bin/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | import '../dist/cli/index.mjs' 5 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genapi/core", 3 | "type": "module", 4 | "version": "3.5.0", 5 | "author": "Hairyf ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/hairyf/genapi#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hairyf/genapi.git" 11 | }, 12 | "bugs": "https://github.com/hairyf/genapi/issues", 13 | "keywords": [ 14 | "genapi", 15 | "parser" 16 | ], 17 | "main": "./src/index.ts", 18 | "publishConfig": { 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "import": "./dist/index.mjs" 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "main": "./dist/index.mjs", 27 | "module": "./dist/index.mjs", 28 | "types": "./dist/index.d.ts", 29 | "bin": { 30 | "genapi": "./bin/index.mjs" 31 | } 32 | }, 33 | "bin": { 34 | "genapi": "./bin/index.dev.mjs" 35 | }, 36 | "files": ["bin", "dist"], 37 | "scripts": { 38 | "build": "tsdown" 39 | }, 40 | "dependencies": { 41 | "@genapi/shared": "workspace:*", 42 | "@hairy/utils": "^1.7.4", 43 | "cac": "^6.7.14", 44 | "chalk": "^5.2.0", 45 | "fs-extra": "^11.1.0", 46 | "jiti": "^1.17.1", 47 | "ora": "^5", 48 | "unconfig": "^0.3.7" 49 | }, 50 | "devDependencies": { 51 | "@types/fs-extra": "^11.0.1", 52 | "@types/node": "^18.14.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { merge } from '@hairy/utils' 3 | import cac from 'cac' 4 | import { loadConfig } from 'unconfig' 5 | import { operatePipelineGenerator } from '../generator' 6 | import * as parser from './parser' 7 | 8 | const cli = cac('genapi') 9 | 10 | cli 11 | .option('--pipe ', 'The compilation pipeline used supports npm package (add the prefix genapi -) | local path') 12 | .option('--input ', 'The incoming string resolves to a uri or json path.') 13 | .option('--outfile ', 'genapi output file options') 14 | 15 | parser.readpack(cli) 16 | 17 | main() 18 | 19 | async function main() { 20 | const options = cli.parse().options 21 | if (options.help) 22 | return 23 | 24 | const { config } = await loadConfig({ 25 | sources: { 26 | files: 'genapi.config', 27 | rewrite: config => merge(parser.options(options), config) as any, 28 | }, 29 | }) 30 | 31 | const servers = parser.servers(config) 32 | await operatePipelineGenerator(servers) 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/cli/parser.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import type { CAC } from 'cac' 3 | import path, { join } from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | import { cloneDeep, isUndefined, merge } from '@hairy/utils' 6 | 7 | import fs from 'fs-extra' 8 | import { isNetworkUrl } from './utils' 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = path.dirname(__filename) 12 | 13 | export function readpack(cli: CAC) { 14 | const pkgPath = join(__dirname, '../package.json') 15 | 16 | if (fs.existsSync(pkgPath)) 17 | cli.version(fs.readJSONSync(pkgPath).version) 18 | 19 | cli.help() 20 | } 21 | 22 | export function options(options: any): Partial { 23 | const config: any = {} 24 | if (options.pipe) 25 | config.pipeline = options.pipe 26 | if (options.input) { 27 | if (isNetworkUrl(options.input)) 28 | config.input = { uri: options.input } 29 | else 30 | config.input = { json: options.input } 31 | 32 | if (options.outfile) 33 | config.output = { main: options.outfile } 34 | } 35 | 36 | return config 37 | } 38 | 39 | export function servers(config: ApiPipeline.DefineConfig): ApiPipeline.Config[] { 40 | if (isUndefined((config as ApiPipeline.ConfigServers).servers)) 41 | (config as ApiPipeline.ConfigServers).servers = [] 42 | 43 | const servers = (config as ApiPipeline.ConfigServers).servers as ApiPipeline.ConfigServers['servers'] 44 | 45 | delete (config as any).servers 46 | 47 | if ((config as ApiPipeline.Config).input) 48 | servers.push((config as ApiPipeline.Config)) 49 | 50 | for (let index = 0; index < servers.length; index++) 51 | servers[index] = merge(cloneDeep(config), servers[index]) 52 | 53 | return servers 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/cli/utils.ts: -------------------------------------------------------------------------------- 1 | export function isNetworkUrl(str: string) { 2 | // eslint-disable-next-line regexp/no-unused-capturing-group 3 | return /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{1,64})?\.)+[a-z]{2,6}\/?/.test(str) 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | 3 | /** 4 | * use genapi.config.ts|.. file options 5 | * @param config 6 | */ 7 | export function defineConfig(config: ApiPipeline.Config): ApiPipeline.Config 8 | export function defineConfig(config: ApiPipeline.ConfigServers): ApiPipeline.ConfigServers 9 | export function defineConfig(config: any) { 10 | return config 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/generator/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { exit } from 'node:process' 3 | import ora from 'ora' 4 | import { inPipeline } from '../internal' 5 | 6 | export async function operatePipelineGenerator(config: ApiPipeline.Config | ApiPipeline.Config[]) { 7 | const configs: ApiPipeline.Config[] = Array.isArray(config) ? config : [config] 8 | const spinner = ora('Generate API File...\n').start() 9 | 10 | const process = configs.map((config) => { 11 | const pipeline = inPipeline(config.pipeline || 'swag-axios-ts') 12 | if (!pipeline) 13 | throw new Error(`Pipeline not found ${config.pipeline}`) 14 | return pipeline(config) 15 | }) 16 | 17 | try { 18 | await Promise.all(process) 19 | spinner.succeed() 20 | spinner.clear() 21 | } 22 | catch (error: any) { 23 | spinner.clear() 24 | spinner.fail('Generate API File Error') 25 | console.error(error) 26 | exit() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './generator' 3 | export * from '@genapi/shared' 4 | -------------------------------------------------------------------------------- /packages/core/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import path from 'node:path' 3 | import { cwd } from 'node:process' 4 | import createJiti from 'jiti' 5 | 6 | const jiti = createJiti(cwd()) 7 | export function inPipeline(pipe: string | ApiPipeline.Pipeline): ApiPipeline.Pipeline | undefined { 8 | if (typeof pipe === 'function') 9 | return pipe as ApiPipeline.Pipeline 10 | 11 | const inputs = [`@genapi/presets/${pipe}`, `genapi-${pipe}`, pipe, absolutePath(pipe)] 12 | 13 | for (const input of inputs) { 14 | try { 15 | const inputModule = jiti(input) 16 | const pipeline = inputModule.default || inputModule 17 | if (pipeline) 18 | return pipeline 19 | } 20 | catch {} 21 | } 22 | } 23 | 24 | export function absolutePath(_path: string) { 25 | if (path.isAbsolute(_path)) 26 | return _path 27 | else 28 | return path.resolve(cwd(), _path) 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | import { dependencies } from './package.json' 3 | 4 | export default defineConfig({ 5 | fixedExtension: true, 6 | entry: ['src/**/*.ts'], 7 | format: ['esm'], 8 | clean: true, 9 | dts: true, 10 | external: Object.keys(dependencies || {}), 11 | }) 12 | -------------------------------------------------------------------------------- /packages/parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genapi/parser", 3 | "type": "module", 4 | "version": "3.5.0", 5 | "author": "Hairyf ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/hairyf/genapi#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hairyf/genapi.git" 11 | }, 12 | "bugs": "https://github.com/hairyf/genapi/issues", 13 | "keywords": [ 14 | "genapi", 15 | "shared" 16 | ], 17 | "sideEffects": false, 18 | "main": "./src/index.ts", 19 | "publishConfig": { 20 | "exports": { 21 | ".": "./dist/index.mjs" 22 | }, 23 | "main": "./dist/index.mjs", 24 | "module": "./dist/index.mjs", 25 | "types": "./dist/index.d.mts" 26 | }, 27 | "files": ["dist"], 28 | "scripts": { 29 | "build": "tsdown", 30 | "prepublishOnly": "nr build", 31 | "start": "tsx src/index.ts" 32 | }, 33 | "dependencies": { 34 | "@genapi/shared": "workspace:*", 35 | "@genapi/transform": "workspace:*", 36 | "@hairy/utils": "^1.7.4", 37 | "pascal-case": "^3.1.2", 38 | "transliteration": "^2.3.5" 39 | }, 40 | "devDependencies": { 41 | "openapi-specification-types": "^0.0.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/parser/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parses' 2 | export * from './transform' 3 | export * from './traverse' 4 | export * from './utils' 5 | -------------------------------------------------------------------------------- /packages/parser/src/parses/common.ts: -------------------------------------------------------------------------------- 1 | import type { OpenAPISpecificationV2 } from 'openapi-specification-types' 2 | import type { OpenAPISpecificationV3 } from 'openapi-specification-types/index-v3' 3 | import { swagger2ToSwagger3 } from '@genapi/transform' 4 | 5 | /** 6 | * parse OpenAPI info to commits 7 | * @param source 8 | */ 9 | export function parseHeaderCommits(source: OpenAPISpecificationV2) { 10 | const comments = [ 11 | `@title ${source.info.title}`, 12 | `@description ${source.info.description}`, 13 | source.swagger && `@swagger ${source.swagger}`, 14 | `@version ${source.info.version}`, 15 | ].filter(Boolean) 16 | return comments 17 | } 18 | 19 | export function parseOpenapiSpecification(source: OpenAPISpecificationV2 | OpenAPISpecificationV3) { 20 | return source.openapi?.startsWith('3') 21 | ? swagger2ToSwagger3(source as OpenAPISpecificationV3) 22 | : source as OpenAPISpecificationV2 23 | } 24 | -------------------------------------------------------------------------------- /packages/parser/src/parses/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './method' 3 | export * from './parameter' 4 | export * from './schema' 5 | -------------------------------------------------------------------------------- /packages/parser/src/parses/method.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/ban-ts-comment */ 2 | import type { StatementField, StatementInterface } from '@genapi/shared' 3 | import type { Parameter } from 'openapi-specification-types' 4 | import type { PathMethod } from '../traverse' 5 | import type { InSchemas, LiteralField } from '../utils' 6 | import { camelCase } from '@hairy/utils' 7 | import { isRequiredParameter, signAnyInter, toUndefField, varName } from '../utils' 8 | import { parseParameterFiled } from './parameter' 9 | import { parseSchemaType } from './schema' 10 | 11 | export type { InSchemas } 12 | 13 | /** 14 | * parse params to function options 15 | * @param {PathMethod} [pathMethod] { method, parameters, path } 16 | * @param {InSchemas} [schemas] 17 | */ 18 | export function parseMethodParameters({ method, parameters, path }: PathMethod, schemas?: InSchemas) { 19 | const requestConfigs = { 20 | body: [] as StatementField[], 21 | formData: [] as StatementField[], 22 | path: [] as StatementField[], 23 | query: [] as StatementField[], 24 | header: [] as StatementField[], 25 | } 26 | 27 | const config = { 28 | options: [] as LiteralField[], 29 | parameters: [] as StatementField[], 30 | interfaces: [] as StatementInterface[], 31 | } 32 | 33 | for (const parameter of parameters) 34 | requestConfigs[parameter.in].push(parseParameterFiled(parameter)) 35 | 36 | for (const [inType, properties] of Object.entries(requestConfigs) as [Parameter['in'], StatementField[]][]) { 37 | if (properties.length === 0) 38 | continue 39 | 40 | const name = toUndefField(inType, schemas) 41 | 42 | if (inType !== 'path') 43 | config.options.push(name) 44 | 45 | if (inType === 'header') 46 | signAnyInter(properties) 47 | 48 | if (inType === 'formData') { 49 | increaseFromDataParameter(name) 50 | continue 51 | } 52 | if (inType === 'body') { 53 | increaseBodyParameter(name, properties) 54 | continue 55 | } 56 | 57 | if (['header', 'path', 'query'].includes(inType)) { 58 | const typeName = varName([method, path, inType]) 59 | config.interfaces.push({ name: typeName, properties, export: true }) 60 | config.parameters.push({ name, type: typeName, required: inType === 'path' || isRequiredParameter(properties) }) 61 | } 62 | } 63 | 64 | function increaseBodyParameter(name: string, properties: StatementField[]) { 65 | config.parameters.push({ 66 | required: properties[0].required, 67 | type: properties[0].type, 68 | name, 69 | }) 70 | } 71 | function increaseFromDataParameter(name: string) { 72 | config.parameters.push({ 73 | type: 'FormData', 74 | required: true, 75 | name, 76 | }) 77 | } 78 | 79 | return config 80 | } 81 | 82 | export function parseMethodMetadata({ method, path, responses, options: meta }: PathMethod) { 83 | const comments = [ 84 | meta.summary && `@summary ${meta.summary}`, 85 | meta.description && `@description ${meta.description}`, 86 | `@method ${method}`, 87 | meta.tags && `@tags ${meta.tags.join(' | ') || '-'}`, 88 | meta.consumes && `@consumes ${meta.consumes.join('; ') || '-'}`, 89 | ] 90 | 91 | const name = camelCase(`${method}/${path}`) 92 | const url = `${path.replace(/(\{)/g, '${paths.')}` 93 | const responseSchema 94 | // @ts-expect-error 95 | = responses.default?.content?.['application/json']?.schema 96 | // @ts-expect-error 97 | || responses['200']?.content?.['application/json']?.schema 98 | || responses['200'] 99 | const responseType = responseSchema ? parseSchemaType(responseSchema) : 'void' 100 | 101 | return { description: comments.filter(Boolean), name, url, responseType, body: [] as string[] } 102 | } 103 | -------------------------------------------------------------------------------- /packages/parser/src/parses/parameter.ts: -------------------------------------------------------------------------------- 1 | import type { StatementField } from '@genapi/shared' 2 | import type { Parameter } from 'openapi-specification-types' 3 | import { spliceEnumDescription, varFiled } from '../utils' 4 | import { parseSchemaType } from './schema' 5 | 6 | /** 7 | * parse parameter to filed 8 | * @param parameter 9 | */ 10 | export function parseParameterFiled(parameter: Parameter) { 11 | const field: StatementField = { 12 | description: parameter.description ?? '', 13 | required: parameter.required, 14 | name: varFiled(parameter.name), 15 | type: '', 16 | } 17 | 18 | if (field.description) 19 | field.description = `@description ${field.description}` 20 | 21 | if (parameter.in === 'query' && parameter.type === 'array') { 22 | const enums = spliceEnumDescription(parameter.name, parameter.items?.enum) 23 | field.description = [field.description || '', enums].filter(Boolean) 24 | } 25 | 26 | if (['formData', 'body', 'header', 'path', 'query'].includes(parameter.in)) 27 | field.type = parseSchemaType(parameter) 28 | 29 | if (!field.description) 30 | delete field.description 31 | return field 32 | } 33 | -------------------------------------------------------------------------------- /packages/parser/src/parses/schema.ts: -------------------------------------------------------------------------------- 1 | import type { Schema } from 'openapi-specification-types' 2 | import { isArray, uniq } from '@hairy/utils' 3 | 4 | import { spliceEnumType, useRefMap, varName } from '../utils' 5 | 6 | /** 7 | * parse schema to type 8 | * @param propertie 9 | */ 10 | export function parseSchemaType(propertie: Schema): string { 11 | if (!propertie) 12 | return 'any' 13 | if (propertie.originalRef) 14 | return varName(propertie.originalRef) 15 | 16 | if (propertie.$ref) 17 | return varName(useRefMap(propertie.$ref)) 18 | 19 | if (propertie.schema) 20 | return parseSchemaType(propertie.schema) 21 | 22 | if (propertie.additionalProperties) 23 | return `Record` 24 | 25 | if (!propertie.type) 26 | return 'any' 27 | 28 | if (propertie.type === 'array') { 29 | if (propertie.items?.enum) 30 | return ['string', spliceEnumType(propertie.items.enum)].filter(Boolean).join(' | ') 31 | 32 | let itemsType = parseSchemaType(propertie.items!) 33 | itemsType = itemsType.includes('|') ? `(${itemsType})` : itemsType 34 | return `${itemsType}[]` 35 | } 36 | 37 | if (propertie.type === 'boolean') 38 | return propertie.type 39 | 40 | if (isArray(propertie.type)) 41 | return uniq(propertie.type.map(type => parseSchemaType({ type }))).join(' | ') 42 | 43 | if (['integer', 'long', 'float', 'byte', 'TypesLong', 'number'].includes(propertie.type)) 44 | return 'number' 45 | 46 | if (['byte', 'binary', 'date', 'dateTime', 'password', 'TypesString', 'string'].includes(propertie.type)) 47 | return 'string' 48 | 49 | return 'any' 50 | } 51 | -------------------------------------------------------------------------------- /packages/parser/src/transform/body.ts: -------------------------------------------------------------------------------- 1 | import type { StatementField } from '@genapi/shared' 2 | import type { LiteralField } from '../utils' 3 | 4 | export interface BodyJsonTransformOptions { 5 | options: LiteralField[] 6 | parameters: StatementField[] 7 | } 8 | 9 | export function transformBodyStringify(name: string, { options, parameters }: BodyJsonTransformOptions) { 10 | if (options.includes(name)) { 11 | const parameter = parameters.find(v => v.name === name) 12 | if (!parameter || parameter?.type === 'FormData' || parameter?.type === 'any') 13 | return 14 | const stringify = `JSON.stringify(${name}${parameter.required ? '' : ' || {}'})` 15 | options.splice(options.findIndex(v => v === name), 1, [name, stringify]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/parser/src/transform/definitions.ts: -------------------------------------------------------------------------------- 1 | import type { StatementInterface } from '@genapi/shared' 2 | import type { Definitions, Schema } from 'openapi-specification-types' 3 | import { parseSchemaType } from '../parses' 4 | import { varFiled, varName } from '../utils' 5 | 6 | export interface DefinitionTransformOptions { 7 | interfaces: StatementInterface[] 8 | } 9 | 10 | export function transformDefinitions(definitions: Definitions, { interfaces }: DefinitionTransformOptions) { 11 | for (const [name, definition] of Object.entries(definitions)) { 12 | const { properties = {} } = definition 13 | 14 | interfaces.push({ 15 | export: true, 16 | name: varName(name), 17 | properties: Object.keys(properties).map(name => defToFields(name, properties[name])), 18 | }) 19 | 20 | function defToFields(name: string, propertie: Schema) { 21 | propertie.required = definition?.required?.some(v => v === name) 22 | if (propertie.description) 23 | propertie.description = `@description ${propertie.description}` 24 | 25 | return { 26 | name: varFiled(name), 27 | type: parseSchemaType(propertie), 28 | description: propertie.description, 29 | required: propertie.required, 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/parser/src/transform/headers.ts: -------------------------------------------------------------------------------- 1 | import type { StatementField } from '@genapi/shared' 2 | import type { LiteralField } from '../utils' 3 | 4 | export interface HeadersTransformOptions { 5 | options: LiteralField[] 6 | parameters: StatementField[] 7 | } 8 | 9 | export function transformHeaderOptions(name: string, { parameters, options }: HeadersTransformOptions) { 10 | const applicationJSONFields = [ 11 | '\'Content-Type\': \'application/json\'', 12 | ] 13 | const applicationDataFields = [ 14 | '\'Content-Type\': \'multipart/form-data\'', 15 | ] 16 | const parameter = parameters.find(v => v.name === name) 17 | 18 | if (!parameter) 19 | return 20 | 21 | const headersOptionsIndex = options.findIndex(p => p === 'headers') 22 | 23 | if (headersOptionsIndex !== -1) { 24 | applicationDataFields.push('...headers') 25 | applicationJSONFields.push('...headers') 26 | options.splice(headersOptionsIndex, 1) 27 | } 28 | 29 | if (parameter?.type === 'FormData') 30 | options.unshift(['headers', `{ ${applicationDataFields} }`]) 31 | else 32 | options.unshift(['headers', `{ ${applicationJSONFields} }`]) 33 | } 34 | -------------------------------------------------------------------------------- /packages/parser/src/transform/index.ts: -------------------------------------------------------------------------------- 1 | export * from './body' 2 | export * from './definitions' 3 | export * from './headers' 4 | export * from './parameters' 5 | export * from './urls' 6 | -------------------------------------------------------------------------------- /packages/parser/src/transform/parameters.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementField, StatementInterface } from '@genapi/shared' 2 | 3 | export interface ParameterTransformOptions { 4 | configRead: ApiPipeline.ConfigRead 5 | interfaces: StatementInterface[] 6 | description: string[] 7 | responseType: string 8 | syntax: 'typescript' | 'ecmascript' 9 | generic?: string 10 | } 11 | export function transformParameters(parameters: StatementField[], options: ParameterTransformOptions) { 12 | const { configRead, syntax, interfaces, description, responseType } = options 13 | const importType = configRead.outputs.find(v => v.type === 'typings')?.import 14 | const isGenerate = configRead.outputs.map(v => v.type).includes('typings') 15 | const namespace = syntax === 'ecmascript' ? `import('${importType}')` : 'Types' 16 | const infer = configRead.graphs.response.infer || '' 17 | const generic = parseGenericType(configRead.graphs.response.generic || options.generic, syntax) 18 | const spaceResponseType = parseResponseType({ 19 | responseType: spliceTypeSpace(responseType), 20 | generic, 21 | infer, 22 | namespace, 23 | }) 24 | 25 | for (const parameter of parameters || []) { 26 | if (!parameter.type) 27 | continue 28 | parameter.type = spliceTypeSpace(parameter.type) 29 | 30 | if (syntax !== 'ecmascript') 31 | continue 32 | if (isGenerate) 33 | description.push(`@param {${parameter.type}${parameter.required ? '' : '='}} ${parameter.name}`) 34 | parameter.type = undefined 35 | parameter.required = true 36 | } 37 | 38 | if (isGenerate && syntax === 'ecmascript') 39 | description.push(`@return {${spaceResponseType}}`) 40 | 41 | function splitTypeSpaces(name: string) { 42 | const _name = name 43 | .replace(/[[\]()]/g, '') 44 | .split('|') 45 | .map(v => v.trim()) 46 | .map(spliceTypeSpace) 47 | .join(' | ') 48 | if (name.includes('(')) 49 | return `(${_name})` 50 | return _name 51 | } 52 | function spliceTypeSpace(name: string): string { 53 | if (name.includes('|') && !name.includes('[]')) 54 | return splitTypeSpaces(name) 55 | if (name.includes('|') && name.includes('[]')) 56 | return `${splitTypeSpaces(name)}[]` 57 | const isExists = interfaces.map(v => v.name).includes(name.replace('[]', '')) 58 | return (isGenerate && isExists) ? `${namespace}.${name}` : name 59 | } 60 | 61 | return { spaceResponseType } 62 | } 63 | 64 | function parseGenericType(generic = '', syntax: 'typescript' | 'ecmascript') { 65 | if (!generic) 66 | generic = syntax === 'ecmascript' ? 'Promise<{__type__}>' : '{__type__}' 67 | return generic 68 | } 69 | 70 | function parseResponseType({ responseType = '', namespace = '', generic = '', infer = '' }) { 71 | if (infer) 72 | responseType = `${namespace}.Infer<${responseType}>` 73 | 74 | responseType = generic.replace('{__type__}', responseType) 75 | 76 | return responseType 77 | } 78 | -------------------------------------------------------------------------------- /packages/parser/src/transform/urls.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import type { OpenAPISpecificationV2 } from 'openapi-specification-types' 3 | import type { LiteralField } from '../utils' 4 | import { literalFieldsToString } from '../utils' 5 | 6 | export interface QueryUrlTransformOptions { 7 | body?: string[] 8 | url?: string 9 | optionKey?: string 10 | options: LiteralField[] 11 | } 12 | 13 | export interface BaseUrlSyntaxTransformOptions { 14 | baseURL?: string | false 15 | } 16 | 17 | export interface BaseUrlTransformOptions { 18 | configRead: ApiPipeline.ConfigRead 19 | } 20 | export function transformQueryParams(name: string, { body, options, optionKey, url }: QueryUrlTransformOptions) { 21 | url = url || '' 22 | if (optionKey) { 23 | const searchParams = [optionKey, `new URLSearchParams(Object.entries(${name} || {}))`] as LiteralField 24 | if (options.includes(name)) 25 | options.splice(options.findIndex(v => v === name), 1, searchParams) 26 | } 27 | else if (options.includes(name)) { 28 | options.splice(options.findIndex(v => v === name), 1) 29 | body?.push(`const ${name}str = new URLSearchParams(Object.entries(${name} || {}))`) 30 | url += `?\${${name}str}` 31 | } 32 | return url || '' 33 | } 34 | 35 | export function transformUrlSyntax(url: string, { baseURL }: BaseUrlSyntaxTransformOptions = {}) { 36 | if (baseURL) 37 | url = `\${baseURL}${url}` 38 | if (!url.includes('$')) 39 | return `'${url}'` 40 | else 41 | return `\`${url}\`` 42 | } 43 | 44 | export function transformBaseURL(source: OpenAPISpecificationV2, { configRead }: BaseUrlTransformOptions) { 45 | if (configRead.config.baseURL === false) 46 | return 47 | 48 | if (!configRead.config.baseURL && source.schemes?.length && source.host) { 49 | const prefix = source.schemes.includes('https') ? 'https://' : 'http://' 50 | configRead.config.baseURL = `"${prefix}${source.host}${source.basePath}/"` 51 | } 52 | 53 | if (configRead.config.baseURL) { 54 | configRead.graphs.variables.push({ 55 | export: true, 56 | flag: 'const', 57 | name: 'baseURL', 58 | value: configRead.config.baseURL, 59 | }) 60 | } 61 | } 62 | 63 | export function transformFetchBody(url: string, options: LiteralField[], spaceResponseType: string) { 64 | const bodies = { 65 | json: [ 66 | `const response = await fetch(${url}, { 67 | ${literalFieldsToString(options)} 68 | })`, 69 | `return response.json() as Promise<${spaceResponseType}>`, 70 | ], 71 | text: [ 72 | `const response = await fetch(${url}, { 73 | ${literalFieldsToString(options)} 74 | })`, 75 | 'return response.text() as Promise', 76 | ], 77 | none: [ 78 | `const response = await fetch(${url}, { 79 | ${literalFieldsToString(options)} 80 | })`, 81 | 'return response', 82 | ], 83 | void: [ 84 | `await fetch(${url}, { 85 | ${literalFieldsToString(options)} 86 | })`, 87 | ], 88 | } 89 | 90 | if (spaceResponseType === 'void') 91 | return bodies.void 92 | 93 | if (spaceResponseType === 'string' || spaceResponseType === 'number') 94 | return bodies.text 95 | 96 | if (spaceResponseType === 'any') 97 | return bodies.none 98 | 99 | return bodies.json 100 | } 101 | -------------------------------------------------------------------------------- /packages/parser/src/traverse/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paths' 2 | -------------------------------------------------------------------------------- /packages/parser/src/traverse/paths.ts: -------------------------------------------------------------------------------- 1 | import type { Method, Parameter, Paths, RequestBody, Responses } from 'openapi-specification-types' 2 | 3 | export interface PathMethod { 4 | path: string 5 | parameters: Parameter[] 6 | method: string 7 | options: Method 8 | responses: Responses 9 | } 10 | 11 | export function traversePaths(paths: Paths, callback: (options: PathMethod) => void) { 12 | for (const [path, _others] of Object.entries(paths)) { 13 | let { parameters = [], ...methods } = _others 14 | for (const method in methods) { 15 | const options = methods[method as keyof typeof methods] 16 | const parametersMap = new Map() 17 | 18 | for (const parameter of parameters) 19 | parametersMap.set(parameter.name, parameter) 20 | for (const parameter of (options.parameters || [])) 21 | parametersMap.set(parameter.name, parameter) 22 | 23 | parameters = [...parametersMap.values()] 24 | 25 | extendsRequestBody(parameters, options.requestBody) 26 | callback({ 27 | responses: options.responses, 28 | path, 29 | method, 30 | options, 31 | parameters, 32 | }) 33 | } 34 | } 35 | } 36 | 37 | function extendsRequestBody(parameters: Parameter[], requestBody?: RequestBody) { 38 | if (!requestBody) 39 | return 40 | if (requestBody.content['multipart/form-data']) { 41 | const properties = requestBody.content['multipart/form-data'].schema.properties! 42 | for (const name in Object.keys(properties)) { 43 | parameters.push({ 44 | required: requestBody.required, 45 | in: 'formData', 46 | name, 47 | description: requestBody.description, 48 | ...properties[name], 49 | }) 50 | } 51 | return 52 | } 53 | 54 | if (requestBody.content['application/json']) { 55 | parameters.push({ 56 | ...requestBody.content['application/json'], 57 | description: requestBody.description, 58 | required: requestBody.required, 59 | in: 'body', 60 | name: 'body', 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/parser/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import { pascalCase } from 'pascal-case' 2 | import { transliterate } from 'transliteration' 3 | 4 | /** 5 | * Get available variable names 6 | * @param {*} string_ 7 | */ 8 | export function varName(string_: string | string[]) { 9 | if (!string_) { 10 | // eslint-disable-next-line no-console 11 | console.trace('\n\nvarName inner is not defined\n') 12 | return string_ 13 | } 14 | if (Array.isArray(string_)) 15 | string_ = string_.filter(Boolean).join('/') 16 | 17 | // 过一遍中文转拼音,没有中文转化之后无变化 18 | string_ = transliterate(string_ as string).replace(/\s+/g, '') 19 | // 转换为大驼峰 20 | string_ = pascalCase(string_) 21 | // 过滤非英文字符 22 | string_ = string_.replace(/[^\dA-Z]+/gi, '') 23 | // 转换为大驼峰 24 | string_ = pascalCase(string_) 25 | return string_ as string 26 | } 27 | 28 | /** 29 | * ref map 30 | * @param ref 31 | */ 32 | export function useRefMap(ref: string) { 33 | return ref.split('/').pop()! 34 | } 35 | 36 | /** 37 | * splice enum description 38 | * @param name 39 | * @param enums 40 | */ 41 | export function spliceEnumDescription(name: string, enums: string[] = []) { 42 | if (!enums?.length) 43 | return '' 44 | const em1 = `${name} '${enums?.join(',') || 'a,b,c'}'` 45 | const em2 = enums?.map(i => `${name}=${i}`).join('&') || `${name}=a&${name}=b` 46 | return `@param ${em1} | '${em2}'` 47 | } 48 | 49 | /** 50 | * splice enum type 51 | * @param enums 52 | */ 53 | export function spliceEnumType(enums: string[] = []) { 54 | if (!enums.length) 55 | return '' 56 | let stringTypes = enums.map(v => `'${v}'`).join(' | ') 57 | stringTypes = stringTypes.includes('|') ? `(${stringTypes})` : stringTypes 58 | return `${stringTypes}[]` 59 | } 60 | 61 | export function varFiled(name: string) { 62 | if (/[^A-Z]/i.test(name)) 63 | name = `'${name}'` 64 | return name 65 | } 66 | -------------------------------------------------------------------------------- /packages/parser/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './format' 2 | export * from './is' 3 | export * from './literal' 4 | export * from './utils' 5 | -------------------------------------------------------------------------------- /packages/parser/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { StatementField } from '@genapi/shared' 2 | 3 | /** 4 | * Determine if the current parameters are mandatory 5 | * @param fields 6 | */ 7 | export function isRequiredParameter(fields: StatementField[]) { 8 | return fields.some(({ type, required, name }) => required && !name.startsWith('[') && !type?.endsWith('any')) 9 | } 10 | -------------------------------------------------------------------------------- /packages/parser/src/utils/literal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 'a' > { a } 3 | * @example ['a', 'b'] > { a: b } 4 | * @example ['...', 'c'] > { ...c } 5 | */ 6 | export type LiteralField = string | [string | '...', string] 7 | 8 | /** 9 | * Convert literal fields to string 10 | * @param fields 11 | */ 12 | export function literalFieldsToString(fields: LiteralField[]) { 13 | function parse(field: LiteralField) { 14 | if (typeof field === 'string') 15 | return field 16 | if (field[0] === '...') 17 | return `...${field[1]}` 18 | return `${field[0]}:${field[1]}` 19 | } 20 | return fields.map(parse).join(', ') 21 | } 22 | -------------------------------------------------------------------------------- /packages/parser/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementField } from '@genapi/shared' 2 | import type { Method, Parameter } from 'openapi-specification-types' 3 | 4 | export interface InSchemas { 5 | path?: string 6 | body?: string 7 | query?: string 8 | header?: string 9 | formData?: string 10 | } 11 | 12 | export function fillParameters(options: Method, parameters: Parameter[]) { 13 | parameters = parameters.filter((item) => { 14 | return !options.parameters?.some(v => v.name === item.name) 15 | }) 16 | parameters = [...parameters, ...(options.parameters || [])] 17 | return parameters 18 | } 19 | 20 | export function toUndefField(inType: Parameter['in'], schemas: InSchemas = {}) { 21 | const toSchemas = { 22 | path: schemas.path || 'paths', 23 | body: schemas.body || 'body', 24 | query: schemas.query || 'query', 25 | header: schemas.header || 'headers', 26 | formData: schemas.formData || schemas.body || 'body', 27 | } 28 | return toSchemas[inType] 29 | } 30 | 31 | /** 32 | * 对类型进行 any 签名 33 | */ 34 | export function signAnyInter(fields: StatementField[]) { 35 | fields.push({ name: '[key: string]', required: true, type: 'any' }) 36 | } 37 | 38 | export function replaceMainext(output?: ApiPipeline.PreOutput['output'], ext: 'js' | 'ts' = 'js') { 39 | const from = ext === 'js' ? 'ts' : 'js' 40 | const to = ext === 'js' ? ext : 'ts' 41 | if (typeof output === 'string') 42 | return output.replace(from, to) 43 | return output?.main?.replace(from, to) 44 | } 45 | -------------------------------------------------------------------------------- /packages/parser/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | fixedExtension: true, 6 | format: ['esm'], 7 | clean: true, 8 | dts: true, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/pipeline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genapi/pipeline", 3 | "type": "module", 4 | "version": "3.5.0", 5 | "author": "Hairyf ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/hairyf/genapi#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hairyf/genapi.git" 11 | }, 12 | "bugs": "https://github.com/hairyf/genapi/issues", 13 | "keywords": [ 14 | "genapi", 15 | "shared" 16 | ], 17 | "sideEffects": false, 18 | "main": "./src/index.ts", 19 | "publishConfig": { 20 | "exports": { 21 | ".": "./dist/index.mjs" 22 | }, 23 | "main": "./dist/index.mjs", 24 | "module": "./dist/index.mjs", 25 | "types": "./dist/index.d.mts" 26 | }, 27 | "files": ["dist"], 28 | "scripts": { 29 | "build": "tsdown", 30 | "prepublishOnly": "nr build", 31 | "start": "tsx src/index.ts" 32 | }, 33 | "dependencies": { 34 | "@genapi/shared": "workspace:*", 35 | "fs-extra": "^11.1.0", 36 | "got": "11.8.6", 37 | "p-pipe": "^3", 38 | "prettier": "^2.8.4", 39 | "ts-factory-extra": "^0.0.5", 40 | "typescript": "^5.0.0" 41 | }, 42 | "devDependencies": { 43 | "@types/fs-extra": "^11.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/pipeline/src/compiler/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { compilerTsRequestDeclaration } from './request' 3 | import { compilerTsTypingsDeclaration } from './typings' 4 | 5 | export function compiler(configRead: ApiPipeline.ConfigRead) { 6 | for (const output of configRead.outputs) { 7 | if (output.type === 'request') 8 | output.ast = compilerTsRequestDeclaration(configRead) 9 | if (output.type === 'typings') 10 | output.ast = compilerTsTypingsDeclaration(configRead) 11 | } 12 | 13 | return configRead 14 | } 15 | -------------------------------------------------------------------------------- /packages/pipeline/src/compiler/request.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { codeToAstNode, createComment, createFunction, createImport, createVariable } from 'ts-factory-extra' 3 | 4 | import { factory, NodeFlags } from 'typescript' 5 | import { compilerTsTypingsDeclaration } from './typings' 6 | 7 | const varFlags = { 8 | let: NodeFlags.Let, 9 | const: NodeFlags.Const, 10 | var: NodeFlags.None, 11 | } 12 | // @ts-check 13 | export function compilerTsRequestDeclaration(configRead: ApiPipeline.ConfigRead) { 14 | configRead.graphs.imports = configRead.graphs.imports || [] 15 | configRead.graphs.comments = configRead.graphs.comments || [] 16 | configRead.graphs.variables = configRead.graphs.variables || [] 17 | configRead.graphs.functions = configRead.graphs.functions || [] 18 | 19 | const isGenerateType = configRead.outputs.some(v => v.type === 'typings') 20 | const isTypescript = configRead.outputs.some(v => v.type === 'request' && v.path.endsWith('.ts')) 21 | 22 | const comments = [ 23 | createComment('multi', configRead.graphs.comments), 24 | ] 25 | const imports = configRead.graphs.imports?.map((item) => { 26 | return createImport(item.name, item.names, item.value, item.namespace) 27 | }) 28 | const variables = configRead.graphs.variables.map((item) => { 29 | // eslint-disable-next-line ts/ban-ts-comment 30 | // @ts-expect-error 31 | return createVariable(item.export, varFlags[item.flag], item.name, item.value) 32 | }) 33 | const functions = configRead.graphs.functions.flatMap((item) => { 34 | return createFunction({ 35 | export: true, 36 | comment: item.description, 37 | name: item.name, 38 | parameters: item.parameters, 39 | body: item.body?.map(codeToAstNode), 40 | async: item.async, 41 | returnType: item.returnType, 42 | generics: item.generics, 43 | generator: item.generator, 44 | }) 45 | }) 46 | 47 | const nodes = [ 48 | ...comments, 49 | factory.createIdentifier(''), 50 | ...imports, 51 | factory.createIdentifier(''), 52 | ...variables, 53 | factory.createIdentifier(''), 54 | ...functions, 55 | ] 56 | 57 | if (!isGenerateType && isTypescript) { 58 | nodes.push(factory.createIdentifier('')) 59 | nodes.push(factory.createIdentifier('')) 60 | nodes.push(...compilerTsTypingsDeclaration(configRead, false)) 61 | } 62 | 63 | return nodes 64 | } 65 | -------------------------------------------------------------------------------- /packages/pipeline/src/compiler/typings.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { createComment, createInterface, createTypeAlias } from 'ts-factory-extra' 3 | import { factory } from 'typescript' 4 | 5 | export function compilerTsTypingsDeclaration(configRead: ApiPipeline.ConfigRead, comment = true) { 6 | configRead.graphs.comments = configRead.graphs.comments || [] 7 | configRead.graphs.typings = configRead.graphs.typings || [] 8 | configRead.graphs.interfaces = configRead.graphs.interfaces || [] 9 | 10 | const typings = configRead.graphs.typings.map((item) => { 11 | return createTypeAlias(item.export, item.name, item.value) 12 | }) 13 | const interfaces = configRead.graphs.interfaces.map((item) => { 14 | return createInterface({ 15 | export: item.export, 16 | name: item.name, 17 | properties: item.properties || [], 18 | }) 19 | }) 20 | 21 | const nodes = [ 22 | factory.createIdentifier(''), 23 | ...typings, 24 | factory.createIdentifier(''), 25 | ...interfaces, 26 | ] 27 | 28 | if (comment) 29 | nodes.unshift(createComment('multi', configRead.graphs.comments)) 30 | 31 | return nodes as any[] 32 | } 33 | -------------------------------------------------------------------------------- /packages/pipeline/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementImported, StatementTypeAlias } from '@genapi/shared' 2 | import path from 'node:path' 3 | import process from 'node:process' 4 | 5 | export function config(userConfig: ApiPipeline.Config) { 6 | userConfig.import = userConfig.import || {} 7 | 8 | userConfig.responseType = userConfig.responseType || {} 9 | 10 | if (typeof userConfig.output === 'string') 11 | userConfig.output = { main: userConfig.output } 12 | 13 | userConfig.output = userConfig.output || {} 14 | userConfig.output.main = userConfig.output.main || 'src/api/index.ts' 15 | 16 | if (typeof userConfig.baseURL === 'string') { 17 | userConfig.baseURL = userConfig.baseURL.endsWith('/"') 18 | ? userConfig.baseURL = `${userConfig.baseURL.slice(0, userConfig.baseURL.length - 2)}"` 19 | : userConfig.baseURL 20 | } 21 | 22 | if (userConfig.output?.type !== false) 23 | userConfig.output.type = userConfig.output.type || userConfig.output.main.replace(/\.ts|\.js/g, '.type.ts') 24 | if (typeof userConfig.responseType === 'string') 25 | userConfig.responseType = { infer: userConfig.responseType } 26 | 27 | const userRoot = process.cwd() 28 | const isTypescript = userConfig.output.main.endsWith('.ts') 29 | const isGenerateType = userConfig.output?.type !== false 30 | const importTypePath = userConfig.import.type || getImportTypePath(userConfig.output.main, userConfig.output.type || '') 31 | 32 | const imports: (StatementImported | false)[] = [ 33 | isTypescript && isGenerateType && { 34 | name: 'Types', 35 | value: importTypePath, 36 | type: true, 37 | namespace: true, 38 | }, 39 | ] 40 | 41 | const outputs: ApiPipeline.Output[] = [ 42 | { 43 | type: 'request', 44 | root: path.join(userRoot, path.dirname(userConfig.output.main)), 45 | path: path.join(userRoot, userConfig.output.main), 46 | }, 47 | ] 48 | 49 | const typings: (StatementTypeAlias | boolean)[] = [ 50 | !!userConfig.responseType.infer && { export: true, name: 'Infer', value: userConfig.responseType.infer! }, 51 | ] 52 | 53 | if (userConfig.output.type !== false) { 54 | outputs.push({ 55 | type: 'typings', 56 | root: path.join(userRoot, path.dirname(userConfig.output.type)), 57 | import: importTypePath, 58 | path: path.join(userRoot, userConfig.output.type), 59 | }) 60 | } 61 | 62 | const inputs: ApiPipeline.Inputs = {} 63 | 64 | if (typeof userConfig.input === 'string') 65 | inputs.uri = userConfig.input 66 | if (typeof userConfig.input === 'object') 67 | Object.assign(inputs, userConfig.input) 68 | 69 | const configRead: ApiPipeline.ConfigRead> = { 70 | config: userConfig as Required, 71 | inputs, 72 | outputs, 73 | graphs: { 74 | imports: imports.filter(Boolean) as StatementImported[], 75 | variables: [], 76 | comments: [], 77 | functions: [], 78 | interfaces: [], 79 | typings: typings.filter(Boolean) as StatementTypeAlias[], 80 | response: userConfig.responseType, 81 | }, 82 | } 83 | 84 | return configRead 85 | } 86 | 87 | function prefix(path: string) { 88 | return path.startsWith('.') ? path : `./${path}` 89 | } 90 | 91 | function getImportTypePath(main: string, type: string) { 92 | let importTypePath = path.dirname(main) 93 | importTypePath = path.relative(importTypePath, type || '') 94 | importTypePath = prefix(importTypePath).replace('.ts', '') 95 | return importTypePath 96 | } 97 | -------------------------------------------------------------------------------- /packages/pipeline/src/dest/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import fs from 'fs-extra' 3 | 4 | export function dest(configRead: ApiPipeline.ConfigRead) { 5 | configRead.outputs.map(async (output) => { 6 | await fs.ensureDir(output.root) 7 | await fs.writeFile(output.path, output.code || '', { flag: 'w' }) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /packages/pipeline/src/generate/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { format } from 'prettier' 3 | import { astNodeToCode } from 'ts-factory-extra' 4 | 5 | export function generate(configRead: ApiPipeline.ConfigRead) { 6 | for (const output of configRead.outputs || []) { 7 | if (output.ast) 8 | output.code = astNodeToCode(output.ast) 9 | if (output.code) 10 | output.code = formatTypescript(output.code) 11 | } 12 | return configRead 13 | } 14 | 15 | function formatTypescript(code: string) { 16 | return format(code, { printWidth: 800, parser: 'typescript' }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/pipeline/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PipelineDest, PipelineFlow, PipelineRead } from './pipeline' 2 | import { pipeline } from './pipeline' 3 | 4 | export * from './compiler' 5 | export * from './config' 6 | export * from './dest' 7 | export * from './generate' 8 | export * from './original' 9 | 10 | export type { 11 | PipelineDest, 12 | PipelineFlow, 13 | PipelineRead, 14 | } 15 | 16 | export default pipeline 17 | -------------------------------------------------------------------------------- /packages/pipeline/src/original/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import got from 'got' 3 | 4 | export async function original(configRead: ApiPipeline.ConfigRead) { 5 | if (configRead.inputs.uri) 6 | configRead.source = await got({ url: configRead.inputs.uri, responseType: 'json' }).json() 7 | if (configRead.inputs.http) 8 | configRead.source = await got({ ...configRead.inputs.http, responseType: 'json' }).json() 9 | if (configRead.inputs.json) 10 | configRead.source = await readJsonSource(configRead.inputs.json) 11 | 12 | if (!configRead.source) 13 | throw new Error('No source found, please check your input config.') 14 | 15 | if (!configRead.source.schemes?.length) { 16 | const schemes: string[] = [] 17 | if (configRead.inputs.uri?.startsWith('https://')) 18 | schemes.push('https', 'http') 19 | if (configRead.inputs.uri?.startsWith('http://')) 20 | schemes.push('http') 21 | configRead.source.schemes = schemes 22 | } 23 | 24 | return configRead 25 | } 26 | 27 | function readJsonSource(json: string | Record) { 28 | if (!json) 29 | return 30 | if (typeof json === 'object') 31 | return json 32 | else 33 | return import(json).then(mod => mod.default) 34 | } 35 | -------------------------------------------------------------------------------- /packages/pipeline/src/pipeline/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pPipe from 'p-pipe' 3 | import { compiler } from '../compiler' 4 | import { config } from '../config' 5 | import { dest } from '../dest' 6 | import { generate } from '../generate' 7 | import { original } from '../original' 8 | 9 | /** 10 | * Pipeline read(input)function 11 | */ 12 | export type PipelineRead = (config: Config) => ConfigRead | Promise 13 | /** 14 | * Transfer data in pipeline 15 | */ 16 | export type PipelineFlow = (configRead: ConfigRead) => ConfigRead | Promise 17 | /** 18 | * Pipeline dest(output)function 19 | */ 20 | export type PipelineDest = (configRead: ApiPipeline.ConfigRead) => void 21 | 22 | /** 23 | * create genapi pipeline process 24 | * @param config read config pa 25 | * @param original get the source according to config 26 | * @param parser resolve source as available data 27 | * @param compiler compile parse info conversion AST tree 28 | * @param generate generate code 29 | * @param dest dest file 30 | */ 31 | export function pipeline( 32 | config: PipelineRead, 33 | original: PipelineFlow, 34 | parser: PipelineFlow, 35 | compiler: PipelineFlow, 36 | generate: PipelineFlow, 37 | dest: PipelineDest, 38 | ) { 39 | const pipe = pPipe( 40 | config, 41 | original, 42 | parser, 43 | compiler, 44 | generate, 45 | dest, 46 | ) 47 | 48 | return pipe as ApiPipeline.Pipeline 49 | } 50 | pipeline.config = config 51 | pipeline.original = original 52 | pipeline.parser = original 53 | pipeline.compiler = compiler 54 | pipeline.generate = generate 55 | pipeline.dest = dest 56 | -------------------------------------------------------------------------------- /packages/pipeline/src/types/prettier.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'prettier' 2 | -------------------------------------------------------------------------------- /packages/pipeline/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | fixedExtension: true, 6 | format: ['esm'], 7 | clean: true, 8 | dts: true, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/presets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genapi/presets", 3 | "type": "module", 4 | "version": "3.5.0", 5 | "author": "Hairyf ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/hairyf/genapi#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hairyf/genapi.git" 11 | }, 12 | "bugs": "https://github.com/hairyf/genapi/issues", 13 | "keywords": [ 14 | "genapi", 15 | "shared" 16 | ], 17 | "sideEffects": false, 18 | "exports": { 19 | ".": "./src/index.ts", 20 | "./swag-axios-js": "./src/axios/js/index.ts", 21 | "./swag-axios-ts": "./src/axios/ts/index.ts", 22 | "./swag-fetch-js": "./src/fetch/js/index.ts", 23 | "./swag-fetch-ts": "./src/fetch/ts/index.ts", 24 | "./swag-got-js": "./src/got/js/index.ts", 25 | "./swag-got-ts": "./src/got/ts/index.ts", 26 | "./swag-ky-js": "./src/ky/js/index.ts", 27 | "./swag-ky-ts": "./src/ky/ts/index.ts", 28 | "./swag-ofetch-js": "./src/ofetch/js/index.ts", 29 | "./swag-ofetch-ts": "./src/ofetch/ts/index.ts" 30 | }, 31 | "publishConfig": { 32 | "exports": { 33 | ".": "./dist/index.mjs", 34 | "./swag-axios-js": { 35 | "types": "./dist/axios/js/index.d.mts", 36 | "default": "./dist/axios/js/index.mjs" 37 | }, 38 | "./swag-axios-ts": { 39 | "types": "./dist/axios/ts/index.d.mts", 40 | "default": "./dist/axios/ts/index.mjs" 41 | }, 42 | "./swag-fetch-js": { 43 | "types": "./dist/fetch/js/index.d.mts", 44 | "default": "./dist/fetch/js/index.mjs" 45 | }, 46 | "./swag-fetch-ts": { 47 | "types": "./dist/fetch/ts/index.d.mts", 48 | "default": "./dist/fetch/ts/index.mjs" 49 | }, 50 | "./swag-got-js": { 51 | "types": "./dist/got/js/index.d.mts", 52 | "default": "./dist/got/js/index.mjs" 53 | }, 54 | "./swag-got-ts": { 55 | "types": "./dist/got/ts/index.d.mts", 56 | "default": "./dist/got/ts/index.mjs" 57 | }, 58 | "./swag-ky-js": { 59 | "types": "./dist/ky/js/index.d.mts", 60 | "default": "./dist/ky/js/index.mjs" 61 | }, 62 | "./swag-ky-ts": { 63 | "types": "./dist/ky/ts/index.d.mts", 64 | "default": "./dist/ky/ts/index.mjs" 65 | }, 66 | "./swag-ofetch-js": { 67 | "types": "./dist/ofetch/js/index.d.mts", 68 | "default": "./dist/ofetch/js/index.mjs" 69 | }, 70 | "./swag-ofetch-ts": { 71 | "types": "./dist/ofetch/ts/index.d.mts", 72 | "default": "./dist/ofetch/ts/index.mjs" 73 | } 74 | } 75 | }, 76 | "files": [ 77 | "dist" 78 | ], 79 | "scripts": { 80 | "build": "tsdown", 81 | "prepublishOnly": "nr build", 82 | "start": "tsx src/index.ts" 83 | }, 84 | "dependencies": { 85 | "@genapi/parser": "workspace:*", 86 | "@genapi/pipeline": "workspace:*", 87 | "@genapi/shared": "workspace:*", 88 | "openapi-specification-types": "^0.0.3" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/presets/src/axios/index.ts: -------------------------------------------------------------------------------- 1 | export { default as js } from './js' 2 | export { default as ts } from './ts' 3 | -------------------------------------------------------------------------------- /packages/presets/src/axios/js/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { replaceMainext } from '@genapi/parser' 3 | import { config as _config } from '@genapi/pipeline' 4 | 5 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 6 | userConfig.import = userConfig.import || {} 7 | userConfig.output = userConfig.output || {} 8 | userConfig.import.http = userConfig.import.http || 'axios' 9 | userConfig.output = replaceMainext(userConfig.output) || 'src/api/index.js' 10 | 11 | const configRead = _config(userConfig) 12 | 13 | configRead.graphs.imports.push({ 14 | name: 'http', 15 | value: userConfig.import.http, 16 | }) 17 | 18 | return configRead 19 | } 20 | -------------------------------------------------------------------------------- /packages/presets/src/axios/js/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | import { config } from './config' 4 | import { parser } from './parser' 5 | 6 | function openapiPipeline(userConfig: ApiPipeline.Config) { 7 | const process = pipeline( 8 | userConfig => config(userConfig), 9 | configRead => original(configRead), 10 | configRead => parser(configRead), 11 | configRead => compiler(configRead), 12 | configRead => generate(configRead), 13 | configRead => dest(configRead), 14 | ) 15 | return process(userConfig) 16 | } 17 | export { compiler, config, dest, generate, original, parser } 18 | 19 | openapiPipeline.config = config 20 | openapiPipeline.original = original 21 | openapiPipeline.parser = parser 22 | openapiPipeline.compiler = compiler 23 | openapiPipeline.generate = generate 24 | openapiPipeline.dest = dest 25 | export default openapiPipeline 26 | -------------------------------------------------------------------------------- /packages/presets/src/axios/js/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformDefinitions, 11 | transformParameters, 12 | transformUrlSyntax, 13 | traversePaths, 14 | } from '@genapi/parser' 15 | 16 | export interface PathsTransformOptions { 17 | configRead: ApiPipeline.ConfigRead 18 | interfaces: StatementInterface[] 19 | functions: StatementFunction[] 20 | } 21 | 22 | export function parser(configRead: ApiPipeline.ConfigRead) { 23 | const source = parseOpenapiSpecification(configRead.source) 24 | 25 | const comments = parseHeaderCommits(source) 26 | 27 | const interfaces: StatementInterface[] = [] 28 | const functions: StatementFunction[] = [] 29 | 30 | transformBaseURL(source, { 31 | configRead, 32 | }) 33 | 34 | transformDefinitions(source.definitions, { 35 | interfaces, 36 | }) 37 | 38 | transformPaths(source.paths, { 39 | configRead, 40 | functions, 41 | interfaces, 42 | }) 43 | 44 | configRead.graphs.comments = comments 45 | configRead.graphs.functions = functions 46 | configRead.graphs.interfaces = interfaces 47 | 48 | return configRead 49 | } 50 | 51 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 52 | traversePaths(paths, (config) => { 53 | /** 54 | * function params/function options/function use interfaces 55 | */ 56 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config, { 57 | body: 'data', 58 | query: 'params', 59 | }) 60 | let { name, description, url, responseType } = parseMethodMetadata(config) 61 | 62 | options.push(['...', 'config']) 63 | interfaces.push(...attachInters) 64 | parameters.push({ 65 | type: 'import(\'axios\').AxiosRequestConfig', 66 | name: 'config', 67 | required: false, 68 | }) 69 | options.unshift('url') 70 | options.unshift(['method', `"${config.method}"`]) 71 | if (configRead.config.baseURL) 72 | options.unshift('baseURL') 73 | 74 | transformParameters(parameters, { 75 | syntax: 'ecmascript', 76 | configRead, 77 | description, 78 | interfaces, 79 | responseType, 80 | generic: 'import(\'axios\').AxiosResponse<{__type__}>', 81 | }) 82 | 83 | url = transformUrlSyntax(url) 84 | 85 | functions.push({ 86 | export: true, 87 | name, 88 | description, 89 | parameters, 90 | body: [ 91 | `const url = ${url}`, 92 | `return http.request({ ${literalFieldsToString(options)} })`, 93 | ], 94 | }) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /packages/presets/src/axios/ts/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { config as _config } from '@genapi/pipeline' 3 | 4 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 5 | userConfig.import = userConfig.import || {} 6 | userConfig.import.http = userConfig.import.http || 'axios' 7 | 8 | const configRead = _config(userConfig) 9 | 10 | configRead.graphs.imports.push({ 11 | name: 'http', 12 | names: userConfig.import.http === 'axios' ? ['AxiosRequestConfig'] : undefined, 13 | value: userConfig.import.http, 14 | }) 15 | 16 | if (userConfig.import.http !== 'axios') { 17 | configRead.graphs.imports.push({ 18 | names: ['AxiosRequestConfig'], 19 | value: 'axios', 20 | }) 21 | } 22 | 23 | return configRead 24 | } 25 | -------------------------------------------------------------------------------- /packages/presets/src/axios/ts/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/axios/ts/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformDefinitions, 11 | transformParameters, 12 | transformUrlSyntax, 13 | traversePaths, 14 | } from '@genapi/parser' 15 | 16 | export interface PathsTransformOptions { 17 | configRead: ApiPipeline.ConfigRead 18 | interfaces: StatementInterface[] 19 | functions: StatementFunction[] 20 | } 21 | 22 | export function parser(configRead: ApiPipeline.ConfigRead) { 23 | const source = parseOpenapiSpecification(configRead.source) 24 | 25 | const comments = parseHeaderCommits(source) 26 | 27 | const interfaces: StatementInterface[] = [] 28 | const functions: StatementFunction[] = [] 29 | 30 | transformBaseURL(source, { 31 | configRead, 32 | }) 33 | 34 | transformDefinitions(source.definitions, { 35 | interfaces, 36 | }) 37 | 38 | transformPaths(source.paths, { 39 | configRead, 40 | functions, 41 | interfaces, 42 | }) 43 | 44 | configRead.graphs.comments = comments 45 | configRead.graphs.functions = functions 46 | configRead.graphs.interfaces = interfaces 47 | 48 | return configRead 49 | } 50 | 51 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 52 | traversePaths(paths, (config) => { 53 | /** 54 | * function params/function options/function use interfaces 55 | */ 56 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config, { 57 | body: 'data', 58 | query: 'params', 59 | }) 60 | 61 | let { name, description, url, responseType } = parseMethodMetadata(config) 62 | 63 | options.push(['...', 'config']) 64 | interfaces.push(...attachInters) 65 | parameters.push({ 66 | name: 'config', 67 | type: 'AxiosRequestConfig', 68 | required: false, 69 | }) 70 | options.unshift('url') 71 | options.unshift(['method', `"${config.method}"`]) 72 | if (configRead.config.baseURL) 73 | options.unshift('baseURL') 74 | 75 | const { spaceResponseType } = transformParameters(parameters, { 76 | syntax: 'typescript', 77 | configRead, 78 | description, 79 | interfaces, 80 | responseType, 81 | }) 82 | url = transformUrlSyntax(url) 83 | 84 | functions.push({ 85 | export: true, 86 | name, 87 | description, 88 | parameters, 89 | body: [ 90 | `const url = ${url}`, 91 | `return http.request<${spaceResponseType}>({ ${literalFieldsToString(options)} })`, 92 | ], 93 | }) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as js } from './js' 2 | export { default as ts } from './ts' 3 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/js/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { replaceMainext } from '@genapi/parser' 3 | import { config as _config } from '@genapi/pipeline' 4 | 5 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 6 | userConfig.import = userConfig.import || {} 7 | userConfig.output = userConfig.output || {} 8 | userConfig.output = replaceMainext(userConfig.output) || 'src/api/index.js' 9 | const configRead = _config(userConfig) 10 | 11 | return configRead 12 | } 13 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/js/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | import { config } from './config' 4 | import { parser } from './parser' 5 | 6 | function openapiPipeline(userConfig: ApiPipeline.Config) { 7 | const process = pipeline( 8 | userConfig => config(userConfig), 9 | configRead => original(configRead), 10 | configRead => parser(configRead), 11 | configRead => compiler(configRead), 12 | configRead => generate(configRead), 13 | configRead => dest(configRead), 14 | ) 15 | return process(userConfig) 16 | } 17 | export { compiler, config, dest, generate, original, parser } 18 | 19 | openapiPipeline.config = config 20 | openapiPipeline.original = original 21 | openapiPipeline.parser = parser 22 | openapiPipeline.compiler = compiler 23 | openapiPipeline.generate = generate 24 | openapiPipeline.dest = dest 25 | export default openapiPipeline 26 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/js/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformBodyStringify, 11 | transformDefinitions, 12 | transformHeaderOptions, 13 | transformParameters, 14 | transformQueryParams, 15 | transformUrlSyntax, 16 | traversePaths, 17 | } from '@genapi/parser' 18 | 19 | export interface PathsTransformOptions { 20 | configRead: ApiPipeline.ConfigRead 21 | interfaces: StatementInterface[] 22 | functions: StatementFunction[] 23 | } 24 | 25 | export function parser(configRead: ApiPipeline.ConfigRead) { 26 | const source = parseOpenapiSpecification(configRead.source) 27 | 28 | const comments = parseHeaderCommits(source) 29 | 30 | const interfaces: StatementInterface[] = [] 31 | const functions: StatementFunction[] = [] 32 | 33 | transformBaseURL(source, { 34 | configRead, 35 | }) 36 | 37 | transformDefinitions(source.definitions, { 38 | interfaces, 39 | }) 40 | 41 | transformPaths(source.paths, { 42 | configRead, 43 | functions, 44 | interfaces, 45 | }) 46 | 47 | configRead.graphs.comments = comments 48 | configRead.graphs.functions = functions 49 | configRead.graphs.interfaces = interfaces 50 | 51 | return configRead 52 | } 53 | 54 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 55 | traversePaths(paths, (config) => { 56 | /** 57 | * function params/function options/function use interfaces 58 | */ 59 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config, { 60 | formData: 'body', 61 | }) 62 | 63 | let { name, description, url, responseType, body } = parseMethodMetadata(config) 64 | 65 | interfaces.push(...attachInters) 66 | parameters.push({ 67 | name: 'config', 68 | type: 'RequestInit', 69 | required: false, 70 | }) 71 | 72 | if (config.method.toLowerCase() !== 'get') 73 | options.unshift(['method', `"${config.method}"`]) 74 | 75 | transformHeaderOptions('body', { options, parameters }) 76 | 77 | options.push(['...', 'config']) 78 | 79 | transformParameters(parameters, { 80 | syntax: 'ecmascript', 81 | configRead, 82 | description, 83 | interfaces, 84 | responseType, 85 | }) 86 | 87 | transformBodyStringify('body', { options, parameters }) 88 | 89 | url = transformQueryParams('query', { body, options, url }) 90 | url = transformUrlSyntax(url, { baseURL: configRead.config.baseURL }) 91 | 92 | functions.push({ 93 | export: true, 94 | async: true, 95 | name, 96 | description, 97 | parameters, 98 | body: [ 99 | ...body, 100 | `const response = await fetch(${url}, { 101 | ${literalFieldsToString(options)} 102 | })`, 103 | 'return response.json()', 104 | ], 105 | }) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/ts/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { config as _config } from '@genapi/pipeline' 3 | 4 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 5 | userConfig.import = userConfig.import || {} 6 | userConfig.import.http = userConfig.import.http || 'axios' 7 | 8 | const configRead = _config(userConfig) 9 | 10 | return configRead 11 | } 12 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/ts/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/fetch/ts/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | parseHeaderCommits, 5 | parseMethodMetadata, 6 | parseMethodParameters, 7 | parseOpenapiSpecification, 8 | transformBaseURL, 9 | transformBodyStringify, 10 | transformDefinitions, 11 | transformFetchBody, 12 | transformHeaderOptions, 13 | transformParameters, 14 | transformQueryParams, 15 | transformUrlSyntax, 16 | traversePaths, 17 | } from '@genapi/parser' 18 | 19 | export interface PathsTransformOptions { 20 | configRead: ApiPipeline.ConfigRead 21 | interfaces: StatementInterface[] 22 | functions: StatementFunction[] 23 | } 24 | 25 | export function parser(configRead: ApiPipeline.ConfigRead) { 26 | const source = parseOpenapiSpecification(configRead.source) 27 | const comments = parseHeaderCommits(source) 28 | 29 | const interfaces: StatementInterface[] = [] 30 | const functions: StatementFunction[] = [] 31 | 32 | transformBaseURL(source, { 33 | configRead, 34 | }) 35 | transformDefinitions(source.definitions, { 36 | interfaces, 37 | }) 38 | 39 | transformPaths(source.paths, { 40 | configRead, 41 | functions, 42 | interfaces, 43 | }) 44 | 45 | configRead.graphs.comments = comments 46 | configRead.graphs.functions = functions 47 | configRead.graphs.interfaces = interfaces 48 | 49 | return configRead 50 | } 51 | 52 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 53 | traversePaths(paths, (config) => { 54 | /** 55 | * function params/function options/function use interfaces 56 | */ 57 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config) 58 | let { name, description, url, responseType, body } = parseMethodMetadata(config) 59 | 60 | interfaces.push(...attachInters) 61 | parameters.push({ 62 | name: 'config', 63 | type: 'RequestInit', 64 | required: false, 65 | }) 66 | 67 | if (config.method.toLowerCase() !== 'get') 68 | options.unshift(['method', `"${config.method}"`]) 69 | 70 | transformHeaderOptions('body', { options, parameters }) 71 | 72 | options.push(['...', 'config']) 73 | 74 | const { spaceResponseType } = transformParameters(parameters, { 75 | syntax: 'typescript', 76 | configRead, 77 | description, 78 | interfaces, 79 | responseType, 80 | }) 81 | 82 | transformBodyStringify('body', { options, parameters }) 83 | url = transformQueryParams('query', { body, options, url }) 84 | url = transformUrlSyntax(url, { baseURL: configRead.config.baseURL }) 85 | const fetch = transformFetchBody(url, options, spaceResponseType) 86 | functions.push({ 87 | export: true, 88 | async: true, 89 | name, 90 | description, 91 | parameters, 92 | body: [ 93 | ...body, 94 | ...fetch, 95 | ], 96 | }) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /packages/presets/src/got/index.ts: -------------------------------------------------------------------------------- 1 | export { default as js } from './js' 2 | export { default as ts } from './ts' 3 | -------------------------------------------------------------------------------- /packages/presets/src/got/js/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { replaceMainext } from '@genapi/parser' 3 | import { config as _config } from '@genapi/pipeline' 4 | 5 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 6 | userConfig.import = userConfig.import || {} 7 | userConfig.output = userConfig.output || {} 8 | userConfig.import.http = userConfig.import.http || 'got' 9 | userConfig.output = replaceMainext(userConfig.output) || 'src/api/index.js' 10 | 11 | const configRead = _config(userConfig) 12 | 13 | configRead.graphs.imports.push({ 14 | name: 'http', 15 | value: userConfig.import.http, 16 | }) 17 | 18 | return configRead 19 | } 20 | -------------------------------------------------------------------------------- /packages/presets/src/got/js/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/got/js/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformBodyStringify, 11 | transformDefinitions, 12 | transformParameters, 13 | transformQueryParams, 14 | transformUrlSyntax, 15 | traversePaths, 16 | } from '@genapi/parser' 17 | 18 | export interface PathsTransformOptions { 19 | configRead: ApiPipeline.ConfigRead 20 | interfaces: StatementInterface[] 21 | functions: StatementFunction[] 22 | } 23 | 24 | export function parser(configRead: ApiPipeline.ConfigRead) { 25 | const source = parseOpenapiSpecification(configRead.source) 26 | 27 | const comments = parseHeaderCommits(source) 28 | 29 | const interfaces: StatementInterface[] = [] 30 | const functions: StatementFunction[] = [] 31 | 32 | transformBaseURL(source, { 33 | configRead, 34 | }) 35 | 36 | transformDefinitions(source.definitions, { 37 | interfaces, 38 | }) 39 | 40 | transformPaths(source.paths, { 41 | configRead, 42 | functions, 43 | interfaces, 44 | }) 45 | 46 | configRead.graphs.comments = comments 47 | configRead.graphs.functions = functions 48 | configRead.graphs.interfaces = interfaces 49 | 50 | return configRead 51 | } 52 | 53 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 54 | traversePaths(paths, (config) => { 55 | /** 56 | * function params/function options/function use interfaces 57 | */ 58 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config) 59 | let { name, description, url, responseType } = parseMethodMetadata(config) 60 | 61 | interfaces.push(...attachInters) 62 | parameters.push({ 63 | name: 'config', 64 | type: 'import(\'got\').OptionsOfTextResponseBody', 65 | required: false, 66 | }) 67 | options.push(['...', 'config']) 68 | if (configRead.config.baseURL) 69 | options.unshift(['prefixUrl', 'baseURL']) 70 | 71 | for (const parameter of parameters) { 72 | if (parameter.type === 'FormData') 73 | parameter.type = 'any' 74 | } 75 | 76 | transformParameters(parameters, { 77 | syntax: 'ecmascript', 78 | configRead, 79 | description, 80 | interfaces, 81 | responseType, 82 | }) 83 | transformBodyStringify('body', { options, parameters }) 84 | transformQueryParams('query', { optionKey: 'searchParams', options }) 85 | url = transformUrlSyntax(url) 86 | 87 | functions.push({ 88 | export: true, 89 | async: true, 90 | name, 91 | description, 92 | parameters, 93 | body: [ 94 | `const response = await http(${url}, { 95 | ${literalFieldsToString(options)} 96 | })`, 97 | 'return response.json()', 98 | ], 99 | }) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /packages/presets/src/got/ts/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { config as _config } from '@genapi/pipeline' 3 | 4 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 5 | userConfig.import = userConfig.import || {} 6 | userConfig.import.http = userConfig.import.http || 'got' 7 | 8 | const configRead = _config(userConfig) 9 | 10 | configRead.graphs.imports.push({ 11 | name: 'http', 12 | names: userConfig.import.http === 'got' ? ['OptionsOfTextResponseBody'] : undefined, 13 | value: userConfig.import.http, 14 | }) 15 | 16 | if (userConfig.import.http !== 'got') { 17 | configRead.graphs.imports.push({ 18 | names: ['OptionsOfTextResponseBody'], 19 | value: 'got', 20 | }) 21 | } 22 | 23 | return configRead 24 | } 25 | -------------------------------------------------------------------------------- /packages/presets/src/got/ts/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/got/ts/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformBodyStringify, 11 | transformDefinitions, 12 | transformParameters, 13 | transformQueryParams, 14 | transformUrlSyntax, 15 | traversePaths, 16 | } from '@genapi/parser' 17 | 18 | export interface PathsTransformOptions { 19 | configRead: ApiPipeline.ConfigRead 20 | interfaces: StatementInterface[] 21 | functions: StatementFunction[] 22 | } 23 | 24 | export function parser(configRead: ApiPipeline.ConfigRead) { 25 | const source = parseOpenapiSpecification(configRead.source) 26 | 27 | const comments = parseHeaderCommits(source) 28 | 29 | const interfaces: StatementInterface[] = [] 30 | const functions: StatementFunction[] = [] 31 | 32 | transformBaseURL(source, { 33 | configRead, 34 | }) 35 | 36 | transformDefinitions(source.definitions, { 37 | interfaces, 38 | }) 39 | 40 | transformPaths(source.paths, { 41 | configRead, 42 | functions, 43 | interfaces, 44 | }) 45 | 46 | configRead.graphs.comments = comments 47 | configRead.graphs.functions = functions 48 | configRead.graphs.interfaces = interfaces 49 | 50 | return configRead 51 | } 52 | 53 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 54 | traversePaths(paths, (config) => { 55 | /** 56 | * function params/function options/function use interfaces 57 | */ 58 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config) 59 | let { name, description, url, responseType } = parseMethodMetadata(config) 60 | 61 | interfaces.push(...attachInters) 62 | parameters.push({ 63 | name: 'config', 64 | type: 'OptionsOfTextResponseBody', 65 | required: false, 66 | }) 67 | options.push(['...', 'config']) 68 | if (configRead.config.baseURL) 69 | options.unshift(['prefixUrl', 'baseURL']) 70 | 71 | for (const parameter of parameters) { 72 | if (parameter.type === 'FormData') 73 | parameter.type = 'any' 74 | } 75 | 76 | const { spaceResponseType } = transformParameters(parameters, { 77 | syntax: 'typescript', 78 | configRead, 79 | description, 80 | interfaces, 81 | responseType, 82 | }) 83 | 84 | transformBodyStringify('body', { options, parameters }) 85 | transformQueryParams('query', { optionKey: 'searchParams', options }) 86 | url = transformUrlSyntax(url) 87 | 88 | functions.push({ 89 | export: true, 90 | async: true, 91 | name, 92 | description, 93 | parameters, 94 | body: [ 95 | `const response = http.${config.method}(${url}, { 96 | ${literalFieldsToString(options)} 97 | })`, 98 | `return response.json<${spaceResponseType}>()`, 99 | ], 100 | }) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /packages/presets/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as axios from './axios' 2 | import * as fetch from './fetch' 3 | import * as got from './got' 4 | import * as ky from './ky' 5 | import * as ofetch from './ofetch' 6 | 7 | export { 8 | axios, 9 | fetch, 10 | got, 11 | ky, 12 | ofetch, 13 | } 14 | 15 | export default { 16 | axios, 17 | fetch, 18 | got, 19 | ky, 20 | ofetch, 21 | } 22 | -------------------------------------------------------------------------------- /packages/presets/src/ky/index.ts: -------------------------------------------------------------------------------- 1 | export { default as js } from './js' 2 | export { default as ts } from './ts' 3 | -------------------------------------------------------------------------------- /packages/presets/src/ky/js/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { replaceMainext } from '@genapi/parser' 3 | import { config as _config } from '@genapi/pipeline' 4 | 5 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 6 | userConfig.import = userConfig.import || {} 7 | userConfig.output = userConfig.output || {} 8 | userConfig.import.http = userConfig.import.http || 'ky' 9 | userConfig.output = replaceMainext(userConfig.output) || 'src/api/index.js' 10 | 11 | const configRead = _config(userConfig) 12 | 13 | configRead.graphs.imports.push({ 14 | name: 'http', 15 | value: userConfig.import.http, 16 | }) 17 | 18 | return configRead 19 | } 20 | -------------------------------------------------------------------------------- /packages/presets/src/ky/js/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/ky/js/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformBodyStringify, 11 | transformDefinitions, 12 | transformParameters, 13 | transformQueryParams, 14 | transformUrlSyntax, 15 | traversePaths, 16 | } from '@genapi/parser' 17 | 18 | export interface PathsTransformOptions { 19 | configRead: ApiPipeline.ConfigRead 20 | interfaces: StatementInterface[] 21 | functions: StatementFunction[] 22 | } 23 | 24 | export function parser(configRead: ApiPipeline.ConfigRead) { 25 | const source = parseOpenapiSpecification(configRead.source) 26 | 27 | const comments = parseHeaderCommits(source) 28 | 29 | const interfaces: StatementInterface[] = [] 30 | const functions: StatementFunction[] = [] 31 | 32 | transformBaseURL(source, { 33 | configRead, 34 | }) 35 | 36 | transformDefinitions(source.definitions, { 37 | interfaces, 38 | }) 39 | 40 | transformPaths(source.paths, { 41 | configRead, 42 | functions, 43 | interfaces, 44 | }) 45 | 46 | configRead.graphs.comments = comments 47 | configRead.graphs.functions = functions 48 | configRead.graphs.interfaces = interfaces 49 | 50 | return configRead 51 | } 52 | 53 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 54 | traversePaths(paths, (config) => { 55 | /** 56 | * function params/function options/function use interfaces 57 | */ 58 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config) 59 | let { name, description, url, responseType } = parseMethodMetadata(config) 60 | 61 | interfaces.push(...attachInters) 62 | parameters.push({ 63 | name: 'config', 64 | type: 'import(\'ky\').Options', 65 | required: false, 66 | }) 67 | options.push(['...', 'config']) 68 | if (configRead.config.baseURL) 69 | options.unshift(['prefixUrl', 'baseURL']) 70 | 71 | transformParameters(parameters, { 72 | syntax: 'ecmascript', 73 | configRead, 74 | description, 75 | interfaces, 76 | responseType, 77 | generic: 'import(\'ky\').KyResponse<{__type__}>', 78 | }) 79 | transformBodyStringify('body', { options, parameters }) 80 | transformQueryParams('query', { optionKey: 'searchParams', options }) 81 | url = transformUrlSyntax(url) 82 | 83 | functions.push({ 84 | export: true, 85 | async: true, 86 | name, 87 | description, 88 | parameters, 89 | body: [ 90 | `const response = await http(${url}, { 91 | ${literalFieldsToString(options)} 92 | })`, 93 | 'return response.json()', 94 | ], 95 | }) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /packages/presets/src/ky/ts/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { config as _config } from '@genapi/pipeline' 3 | 4 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 5 | userConfig.import = userConfig.import || {} 6 | userConfig.import.http = userConfig.import.http || 'ky' 7 | 8 | const configRead = _config(userConfig) 9 | 10 | configRead.graphs.imports.push({ 11 | name: 'http', 12 | names: userConfig.import.http === 'ky' ? ['Options'] : undefined, 13 | value: userConfig.import.http, 14 | }) 15 | 16 | if (userConfig.import.http !== 'ky') { 17 | configRead.graphs.imports.push({ 18 | names: ['Options'], 19 | value: 'ky', 20 | }) 21 | } 22 | 23 | return configRead 24 | } 25 | -------------------------------------------------------------------------------- /packages/presets/src/ky/ts/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/ky/ts/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformBodyStringify, 11 | transformDefinitions, 12 | transformParameters, 13 | transformQueryParams, 14 | transformUrlSyntax, 15 | traversePaths, 16 | } from '@genapi/parser' 17 | 18 | export interface PathsTransformOptions { 19 | configRead: ApiPipeline.ConfigRead 20 | interfaces: StatementInterface[] 21 | functions: StatementFunction[] 22 | } 23 | 24 | export function parser(configRead: ApiPipeline.ConfigRead) { 25 | const source = parseOpenapiSpecification(configRead.source) 26 | 27 | const comments = parseHeaderCommits(source) 28 | 29 | const interfaces: StatementInterface[] = [] 30 | const functions: StatementFunction[] = [] 31 | 32 | transformBaseURL(source, { 33 | configRead, 34 | }) 35 | 36 | transformDefinitions(source.definitions, { 37 | interfaces, 38 | }) 39 | 40 | transformPaths(source.paths, { 41 | configRead, 42 | functions, 43 | interfaces, 44 | }) 45 | 46 | configRead.graphs.comments = comments 47 | configRead.graphs.functions = functions 48 | configRead.graphs.interfaces = interfaces 49 | 50 | return configRead 51 | } 52 | 53 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 54 | traversePaths(paths, (config) => { 55 | /** 56 | * function params/function options/function use interfaces 57 | */ 58 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config) 59 | let { name, description, url, responseType } = parseMethodMetadata(config) 60 | 61 | interfaces.push(...attachInters) 62 | parameters.push({ 63 | name: 'config', 64 | type: 'Options', 65 | required: false, 66 | }) 67 | options.push(['...', 'config']) 68 | if (configRead.config.baseURL) 69 | options.unshift(['prefixUrl', 'baseURL']) 70 | 71 | const { spaceResponseType } = transformParameters(parameters, { 72 | syntax: 'typescript', 73 | configRead, 74 | description, 75 | interfaces, 76 | responseType, 77 | }) 78 | 79 | transformBodyStringify('body', { options, parameters }) 80 | transformQueryParams('query', { optionKey: 'searchParams', options }) 81 | url = transformUrlSyntax(url) 82 | 83 | functions.push({ 84 | export: true, 85 | async: true, 86 | name, 87 | description, 88 | parameters, 89 | body: [ 90 | `const response = await http(${url}, { 91 | ${literalFieldsToString(options)} 92 | })`, 93 | `return response.json<${spaceResponseType}>()`, 94 | ], 95 | }) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as js } from './js' 2 | export { default as ts } from './ts' 3 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/js/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { replaceMainext } from '@genapi/parser' 3 | import { config as _config } from '@genapi/pipeline' 4 | 5 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 6 | userConfig.import = userConfig.import || {} 7 | userConfig.output = userConfig.output || {} 8 | userConfig.import.http = userConfig.import.http || 'ofetch' 9 | userConfig.output = replaceMainext(userConfig.output) || 'src/api/index.js' 10 | 11 | const configRead = _config(userConfig) 12 | 13 | configRead.graphs.imports.push({ 14 | name: 'ofetch', 15 | value: userConfig.import.http, 16 | }) 17 | 18 | return configRead 19 | } 20 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/js/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/js/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformDefinitions, 11 | transformParameters, 12 | transformQueryParams, 13 | transformUrlSyntax, 14 | traversePaths, 15 | } from '@genapi/parser' 16 | 17 | export interface PathsTransformOptions { 18 | configRead: ApiPipeline.ConfigRead 19 | interfaces: StatementInterface[] 20 | functions: StatementFunction[] 21 | } 22 | 23 | export function parser(configRead: ApiPipeline.ConfigRead) { 24 | const source = parseOpenapiSpecification(configRead.source) 25 | 26 | const comments = parseHeaderCommits(source) 27 | 28 | const interfaces: StatementInterface[] = [] 29 | const functions: StatementFunction[] = [] 30 | 31 | transformBaseURL(source, { 32 | configRead, 33 | }) 34 | 35 | transformDefinitions(source.definitions, { 36 | interfaces, 37 | }) 38 | 39 | transformPaths(source.paths, { 40 | configRead, 41 | functions, 42 | interfaces, 43 | }) 44 | configRead.graphs.comments = comments 45 | configRead.graphs.functions = functions 46 | configRead.graphs.interfaces = interfaces 47 | 48 | return configRead 49 | } 50 | 51 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 52 | traversePaths(paths, (config) => { 53 | /** 54 | * function params/function options/function use interfaces 55 | */ 56 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config, { 57 | formData: 'body', 58 | query: 'params', 59 | }) 60 | 61 | let { name, description, url, responseType, body } = parseMethodMetadata(config) 62 | 63 | interfaces.push(...attachInters) 64 | parameters.push({ 65 | name: 'options', 66 | type: 'import(\'ofetch\').FetchOptions', 67 | required: false, 68 | }) 69 | options.push(['...', 'options']) 70 | options.unshift(['method', `"${config.method}"`]) 71 | if (configRead.config.baseURL) 72 | options.unshift('baseURL') 73 | 74 | transformParameters(parameters, { 75 | syntax: 'ecmascript', 76 | configRead, 77 | description, 78 | interfaces, 79 | responseType, 80 | }) 81 | 82 | url = transformQueryParams('query', { body, options, url }) 83 | url = transformUrlSyntax(url, { baseURL: configRead.config.baseURL }) 84 | 85 | functions.push({ 86 | export: true, 87 | async: true, 88 | name, 89 | description, 90 | parameters, 91 | body: [ 92 | ...body, 93 | `return ofetch(${url}, {${literalFieldsToString(options)}})`, 94 | ], 95 | }) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/ts/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import { config as _config } from '@genapi/pipeline' 3 | 4 | export function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead { 5 | userConfig.import = userConfig.import || {} 6 | userConfig.import.http = userConfig.import.http || 'ofetch' 7 | 8 | const configRead = _config(userConfig) 9 | 10 | configRead.graphs.imports.push({ 11 | name: 'ofetch', 12 | names: userConfig.import.http === 'ofetch' ? ['FetchOptions'] : undefined, 13 | value: userConfig.import.http, 14 | }) 15 | 16 | if (userConfig.import.http !== 'ofetch') { 17 | configRead.graphs.imports.push({ 18 | names: ['FetchOptions'], 19 | value: 'ofetch', 20 | }) 21 | } 22 | 23 | return configRead 24 | } 25 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/ts/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline } from '@genapi/shared' 2 | import pipeline, { compiler, dest, generate, original } from '@genapi/pipeline' 3 | 4 | import { config } from './config' 5 | import { parser } from './parser' 6 | 7 | function openapiPipeline(userConfig: ApiPipeline.Config) { 8 | const process = pipeline( 9 | userConfig => config(userConfig), 10 | configRead => original(configRead), 11 | configRead => parser(configRead), 12 | configRead => compiler(configRead), 13 | configRead => generate(configRead), 14 | configRead => dest(configRead), 15 | ) 16 | return process(userConfig) 17 | } 18 | export { compiler, config, dest, generate, original, parser } 19 | 20 | openapiPipeline.config = config 21 | openapiPipeline.original = original 22 | openapiPipeline.parser = parser 23 | openapiPipeline.compiler = compiler 24 | openapiPipeline.generate = generate 25 | openapiPipeline.dest = dest 26 | export default openapiPipeline 27 | -------------------------------------------------------------------------------- /packages/presets/src/ofetch/ts/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiPipeline, StatementFunction, StatementInterface } from '@genapi/shared' 2 | import type { Paths } from 'openapi-specification-types' 3 | import { 4 | literalFieldsToString, 5 | parseHeaderCommits, 6 | parseMethodMetadata, 7 | parseMethodParameters, 8 | parseOpenapiSpecification, 9 | transformBaseURL, 10 | transformDefinitions, 11 | transformParameters, 12 | transformQueryParams, 13 | transformUrlSyntax, 14 | traversePaths, 15 | } from '@genapi/parser' 16 | 17 | export interface PathsTransformOptions { 18 | configRead: ApiPipeline.ConfigRead 19 | interfaces: StatementInterface[] 20 | functions: StatementFunction[] 21 | } 22 | 23 | export function parser(configRead: ApiPipeline.ConfigRead) { 24 | const source = parseOpenapiSpecification(configRead.source) 25 | 26 | const comments = parseHeaderCommits(source) 27 | 28 | const interfaces: StatementInterface[] = [] 29 | const functions: StatementFunction[] = [] 30 | 31 | transformBaseURL(source, { 32 | configRead, 33 | }) 34 | 35 | transformDefinitions(source.definitions, { 36 | interfaces, 37 | }) 38 | 39 | transformPaths(source.paths, { 40 | configRead, 41 | functions, 42 | interfaces, 43 | }) 44 | 45 | configRead.graphs.comments = comments 46 | configRead.graphs.functions = functions 47 | configRead.graphs.interfaces = interfaces 48 | 49 | return configRead 50 | } 51 | 52 | export function transformPaths(paths: Paths, { configRead, functions, interfaces }: PathsTransformOptions) { 53 | traversePaths(paths, (config) => { 54 | /** 55 | * function params/function options/function use interfaces 56 | */ 57 | const { parameters, interfaces: attachInters, options } = parseMethodParameters(config, { 58 | formData: 'body', 59 | query: 'params', 60 | }) 61 | 62 | let { name, description, url, responseType, body } = parseMethodMetadata(config) 63 | 64 | interfaces.push(...attachInters) 65 | parameters.push({ 66 | name: 'options', 67 | type: 'FetchOptions', 68 | required: false, 69 | }) 70 | options.push(['...', 'options']) 71 | options.unshift(['method', `"${config.method}"`]) 72 | if (configRead.config.baseURL) 73 | options.unshift('baseURL') 74 | 75 | const { spaceResponseType } = transformParameters(parameters, { 76 | syntax: 'typescript', 77 | configRead, 78 | description, 79 | interfaces, 80 | responseType, 81 | }) 82 | 83 | url = transformQueryParams('query', { body, options, url }) 84 | url = transformUrlSyntax(url) 85 | 86 | functions.push({ 87 | export: true, 88 | name, 89 | description, 90 | parameters, 91 | body: [ 92 | `return ofetch<${spaceResponseType}>(${url}, {${literalFieldsToString(options)}})`, 93 | ], 94 | }) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /packages/presets/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | import { dependencies } from './package.json' 3 | 4 | export default defineConfig({ 5 | fixedExtension: true, 6 | entry: ['src/**/*.ts'], 7 | format: ['esm'], 8 | clean: true, 9 | dts: true, 10 | external: Object.keys(dependencies || {}), 11 | }) 12 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genapi/shared", 3 | "type": "module", 4 | "version": "3.5.0", 5 | "author": "Hairyf ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/hairyf/genapi#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hairyf/genapi.git" 11 | }, 12 | "bugs": "https://github.com/hairyf/genapi/issues", 13 | "keywords": [ 14 | "genapi", 15 | "shared" 16 | ], 17 | "sideEffects": false, 18 | "main": "./src/index.ts", 19 | "publishConfig": { 20 | "exports": { 21 | ".": "./dist/index.mjs" 22 | }, 23 | "main": "./dist/index.mjs", 24 | "module": "./dist/index.mjs", 25 | "types": "./dist/index.d.mts" 26 | }, 27 | "files": [ 28 | "dist" 29 | ], 30 | "scripts": { 31 | "build": "tsdown", 32 | "prepublishOnly": "nr build", 33 | "start": "tsx src/index.ts" 34 | }, 35 | "dependencies": { 36 | "got": "11.8.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | -------------------------------------------------------------------------------- /packages/shared/src/types/config.ts: -------------------------------------------------------------------------------- 1 | import type { OptionsOfJSONResponseBody } from 'got' 2 | import type { 3 | StatementFunction, 4 | StatementImported, 5 | StatementInterface, 6 | StatementResponse, 7 | StatementTypeAlias, 8 | StatementVariable, 9 | } from './statement' 10 | 11 | export namespace ApiPipeline { 12 | export interface Output { 13 | type: 'request' | 'typings' 14 | root: string 15 | path: string 16 | import?: string 17 | ast?: any 18 | code?: string 19 | } 20 | export interface Inputs { 21 | json?: string | Record 22 | http?: OptionsOfJSONResponseBody 23 | uri?: string 24 | } 25 | export interface PreInputs { 26 | /** 27 | * genapi input pipe source 28 | * @description 29 | * the incoming string resolves to a uri. 30 | * 31 | * You can also pass in `{ uri }` with the same effect as above 32 | * 33 | * `{ json }` json can pass in path/object 34 | */ 35 | input: string | { uri: string } | { json: string | Record } | { http: OptionsOfJSONResponseBody } 36 | } 37 | export interface PreOutput { 38 | /** 39 | * genapi output file options 40 | */ 41 | output?: string | { main?: string, type?: string | false } 42 | } 43 | 44 | export interface Meta { 45 | /** The current interface base URL, which can be used for the definition of env variables */ 46 | baseURL?: string | false 47 | /** Import type of makefile */ 48 | import?: { 49 | http?: string 50 | type?: string 51 | } 52 | /** 53 | * type conversion of response body 54 | * @template `T extends { data?: infer V } ? V : void` 55 | */ 56 | responseType?: string | { 57 | /** 58 | * External generic type definition for js type commit 59 | * 60 | * @template `Promise>` 61 | * 62 | * @default {__type__} 63 | */ 64 | generic?: string 65 | /** 66 | * type conversion of response body 67 | * 68 | * @template `T extends { data?: infer V } ? V : void` 69 | */ 70 | infer?: string 71 | } 72 | /** Mandatory parameters optional */ 73 | paramsPartial?: boolean 74 | } 75 | 76 | export interface Config extends PreInputs, PreOutput, Meta { 77 | /** 78 | * The compilation pipeline used supports npm package (add the prefix @genapi/ or genapi-) | local path 79 | * @default 'swag-axios-ts' 80 | */ 81 | pipeline?: string | ApiPipeline.Pipeline 82 | } 83 | 84 | export interface Graphs { 85 | /** 86 | * all comments 87 | */ 88 | comments: string[] 89 | 90 | /** 91 | * all api options 92 | */ 93 | functions: StatementFunction[] 94 | /** 95 | * all request imports 96 | */ 97 | imports: StatementImported[] 98 | 99 | /** 100 | * all request variables 101 | */ 102 | variables: StatementVariable[] 103 | /** 104 | * all request typings 105 | */ 106 | typings: StatementTypeAlias[] 107 | /** 108 | * all request interfaces 109 | */ 110 | interfaces: StatementInterface[] 111 | 112 | response: StatementResponse 113 | } 114 | export interface ConfigRead { 115 | /** 116 | * source input 117 | */ 118 | inputs: Inputs 119 | /** 120 | * source config 121 | */ 122 | config: Config 123 | /** 124 | * graphs 125 | */ 126 | graphs: Graphs 127 | /** 128 | * output configs 129 | */ 130 | outputs: Output[] 131 | /** 132 | * source data 133 | */ 134 | source?: any 135 | 136 | } 137 | export interface ConfigServers extends Omit { 138 | servers: Config[] 139 | } 140 | export type DefineConfig = ConfigServers | Config 141 | 142 | export type Pipeline = (config: ApiPipeline.Config) => Promise 143 | } 144 | -------------------------------------------------------------------------------- /packages/shared/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './statement' 3 | -------------------------------------------------------------------------------- /packages/shared/src/types/statement.ts: -------------------------------------------------------------------------------- 1 | export interface StatementGeneric { 2 | name: string 3 | extends?: string 4 | default?: string 5 | } 6 | export interface StatementFunction { 7 | /** 8 | * function name 9 | */ 10 | name: string 11 | /** 12 | * function params 13 | */ 14 | parameters?: StatementField[] 15 | /** 16 | * function block 17 | */ 18 | body?: string[] 19 | /** 20 | * is export 21 | */ 22 | export?: boolean 23 | /** 24 | * is async 25 | */ 26 | async?: boolean 27 | 28 | generator?: boolean 29 | 30 | /** 31 | * function description 32 | */ 33 | description?: string | string[] 34 | 35 | returnType?: string 36 | 37 | generics?: StatementGeneric[] 38 | } 39 | 40 | export interface StatementInterface { 41 | /** 42 | * interface name 43 | */ 44 | name: string 45 | /** 46 | * all properties 47 | */ 48 | properties?: StatementField[] 49 | /** 50 | * is export 51 | */ 52 | export?: boolean 53 | } 54 | 55 | export interface StatementField { 56 | /** 57 | * field name 58 | */ 59 | name: string 60 | /** 61 | * field type 62 | */ 63 | type?: string 64 | /** 65 | * is required 66 | */ 67 | required?: boolean 68 | /** 69 | * field description 70 | */ 71 | description?: string | string[] 72 | } 73 | 74 | /** 75 | * @example import [name], {[names]} from [value] 76 | * @example import http, { AxiosConfig } from 'axios' 77 | */ 78 | export interface StatementImported { 79 | name?: string 80 | names?: string[] 81 | namespace?: boolean 82 | type?: boolean 83 | value: string 84 | } 85 | 86 | /** 87 | * @example [export] [flag] [name] = [value] 88 | */ 89 | export interface StatementVariable { 90 | export?: boolean 91 | flag: 'let' | 'const' | 'var' 92 | name: string 93 | value?: string 94 | } 95 | 96 | /** 97 | * @example [export] type [name] = [value] 98 | */ 99 | export interface StatementTypeAlias { 100 | export?: boolean 101 | name: string 102 | value: string 103 | } 104 | 105 | export interface StatementResponse { 106 | generic?: string 107 | infer?: string 108 | } 109 | -------------------------------------------------------------------------------- /packages/shared/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | fixedExtension: true, 5 | entry: ['src/index.ts'], 6 | format: ['esm'], 7 | clean: true, 8 | dts: true, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genapi/transform", 3 | "type": "module", 4 | "version": "3.5.0", 5 | "author": "Hairyf ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/hairyf/genapi#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hairyf/genapi.git" 11 | }, 12 | "bugs": "https://github.com/hairyf/genapi/issues", 13 | "keywords": [ 14 | "genapi", 15 | "transform" 16 | ], 17 | "sideEffects": false, 18 | "main": "./src/index.ts", 19 | "publishConfig": { 20 | "exports": { 21 | ".": "./dist/index.mjs" 22 | }, 23 | "main": "./dist/index.mjs", 24 | "module": "./dist/index.mjs", 25 | "types": "./dist/index.d.mjs" 26 | }, 27 | "files": ["dist"], 28 | "scripts": { 29 | "build": "tsdown", 30 | "prepublishOnly": "nr build", 31 | "start": "tsx src/index.ts" 32 | }, 33 | "devDependencies": { 34 | "openapi-specification-types": "^0.0.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/transform/src/index.ts: -------------------------------------------------------------------------------- 1 | import { swagger2ToSwagger3 } from './swagger2-to-swagger3' 2 | import { wpapiToSwagger2 } from './wpapi-to-swagger2' 3 | 4 | export * from './swagger2-to-swagger3' 5 | export * from './wpapi-to-swagger2' 6 | 7 | export default { 8 | swagger2ToSwagger3, 9 | wpapiToSwagger2, 10 | } 11 | -------------------------------------------------------------------------------- /packages/transform/src/swagger2-to-swagger3/index.ts: -------------------------------------------------------------------------------- 1 | import type { OpenAPISpecificationV2 } from 'openapi-specification-types' 2 | import type { OpenAPISpecificationV3 } from 'openapi-specification-types/index-v3' 3 | 4 | export function swagger2ToSwagger3(source: OpenAPISpecificationV3) { 5 | const change = source as unknown as OpenAPISpecificationV2 6 | if (source.openapi?.startsWith('3')) { 7 | const target = source as OpenAPISpecificationV3 8 | change.swagger = target.openapi 9 | change.host = target.servers[0]?.url 10 | change.basePath = target.servers[0]?.url 11 | change.schemes = target.servers.map(v => v.url) 12 | change.info = target.info 13 | change.paths = target.paths 14 | change.definitions = target.components.schemas 15 | change.tags = target.tags 16 | change.externalDocs = target.externalDocs 17 | } 18 | return change 19 | } 20 | -------------------------------------------------------------------------------- /packages/transform/src/wpapi-to-swagger2/index.ts: -------------------------------------------------------------------------------- 1 | import type { OpenAPISpecificationV2, Responses, Security } from 'openapi-specification-types' 2 | import type { WordPressAPISchema, WordPressRoute } from './types' 3 | import { convertEndpoint, getParametersFromArgs, getParametersFromEndpoint } from './utils' 4 | 5 | /** 6 | * Converts WordPress REST API schema to Swagger v2 format 7 | * @param source WordPress REST API schema 8 | * @returns Swagger v2 document 9 | */ 10 | export function wpapiToSwagger2(source: WordPressAPISchema): OpenAPISpecificationV2 { 11 | const swagger: OpenAPISpecificationV2 = { 12 | swagger: '2.0', 13 | basePath: '', 14 | host: '', 15 | info: { 16 | title: 'WordPress REST API', 17 | version: '1.0', 18 | description: 'Using the WordPress REST API you can create a plugin to provide an entirely new admin experience for WordPress, build a brand new interactive front-end experience, or bring your WordPress content into completely separate applications.', 19 | } as any, 20 | definitions: {}, 21 | consumes: [], 22 | externalDocs: {} as any, 23 | paths: {}, 24 | schemes: ['https', 'http'], 25 | securityDefinitions: { 26 | basic: { 27 | type: 'basic', 28 | description: 'Basic authentication', 29 | }, 30 | } as any, 31 | tags: [], 32 | } 33 | 34 | // Process paths and endpoints 35 | for (const [path, route] of Object.entries(source.routes)) { 36 | for (const endpoint of route.endpoints) { 37 | const methods = endpoint.methods 38 | 39 | // Check if path contains parameters 40 | const convertedPath = convertEndpoint(path) 41 | 42 | // Extract parameters from path 43 | const pathParameters = getParametersFromEndpoint(path) 44 | 45 | for (const method of methods) { 46 | // Get parameters 47 | const parameters = getParametersFromArgs( 48 | convertedPath, 49 | endpoint.args || {}, 50 | method, 51 | ) 52 | 53 | // Merge path parameters and request parameters 54 | const allParameters = [...parameters] 55 | const existingNames = new Set(parameters.map(param => param.name)) 56 | 57 | // Add only parameters with unique names 58 | for (const param of pathParameters) { 59 | if (!existingNames.has(param.name)) 60 | allParameters.push(param) 61 | } 62 | 63 | // Set tags, default to namespace 64 | const tags = [route.namespace] 65 | 66 | // Set responses 67 | const responses: Responses = { 68 | 200: { description: 'OK' }, 69 | 400: { description: 'Bad Request' }, 70 | 404: { description: 'Not Found' }, 71 | } as any 72 | 73 | // Set content types for consumption and production 74 | const consumes = [ 75 | 'application/json', 76 | 'application/x-www-form-urlencoded', 77 | 'multipart/form-data', 78 | ] 79 | 80 | const produces = ['application/json'] 81 | 82 | // Set security definitions 83 | const security: Security[] = [{ basic: [] }] as any 84 | 85 | // Create operation ID 86 | const operationId = `${method}${convertedPath.replace(/\//g, '_').replace(/\{|\}/g, '')}` 87 | 88 | // Initialize path object if it doesn't exist 89 | swagger.paths[convertedPath] = swagger.paths[convertedPath] ?? {} 90 | 91 | // Add method to path 92 | swagger.paths[convertedPath][method] = { 93 | tags, 94 | summary: endpoint.description || '', 95 | description: endpoint.description || '', 96 | operationId, 97 | consumes, 98 | produces, 99 | parameters: allParameters, 100 | responses, 101 | security, 102 | } 103 | } 104 | } 105 | } 106 | 107 | return swagger 108 | } 109 | 110 | export * from './utils' 111 | -------------------------------------------------------------------------------- /packages/transform/src/wpapi-to-swagger2/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for WordPress REST API route 3 | */ 4 | export interface WordPressRoute { 5 | namespace: string 6 | endpoints: WordPressEndpoint[] 7 | } 8 | export interface WordPressAPISchema { 9 | routes: Record 10 | } 11 | /** 12 | * Interface for WordPress REST API endpoint 13 | */ 14 | export interface WordPressEndpoint { 15 | methods: ('get' | 'post' | 'put' | 'delete')[] 16 | description?: string 17 | args?: Record 18 | } 19 | 20 | /** 21 | * Interface for WordPress REST API argument 22 | */ 23 | export interface WordPressArgument { 24 | type?: string | string[] 25 | description?: string 26 | required?: boolean 27 | enum?: string[] 28 | default?: any 29 | items?: { 30 | type?: string 31 | } 32 | maximum?: number 33 | minimum?: number 34 | format?: string 35 | schema?: any 36 | } 37 | -------------------------------------------------------------------------------- /packages/transform/src/wpapi-to-swagger2/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/ban-ts-comment */ 2 | /* eslint-disable no-cond-assign */ 3 | 4 | import type { Parameter, SchemaType } from 'openapi-specification-types' 5 | import type { WordPressArgument } from './types' 6 | 7 | /** 8 | * Converts WordPress-style endpoints to Swagger style 9 | * Example: /wp/v2/posts/(?P[\d]+) -> /wp/v2/posts/{id} 10 | * @param endpoint WordPress endpoint path 11 | * @returns Swagger-compatible path 12 | */ 13 | export function convertEndpoint(endpoint: string): string { 14 | if (endpoint.includes('(?P<')) 15 | return endpoint.replace(/\(\?P<([^>]+)>[^)]+\)/g, '{$1}') 16 | 17 | return endpoint 18 | } 19 | 20 | /** 21 | * Extracts path parameters from an endpoint 22 | * @param endpoint WordPress endpoint path 23 | * @returns Array of Swagger parameters 24 | */ 25 | export function getParametersFromEndpoint(endpoint: string): Parameter[] { 26 | const pathParams: Parameter[] = [] 27 | const regex = /\(\?P<([^>]+)>([^)]+)\)/g 28 | let match 29 | 30 | while ((match = regex.exec(endpoint)) !== null) { 31 | const name = match[1] 32 | const pattern = match[2] 33 | const type = pattern.includes('\\d') ? 'integer' : 'string' 34 | 35 | const param: Parameter = { 36 | name, 37 | in: 'path', 38 | description: '', 39 | required: true, 40 | type, 41 | } 42 | 43 | if (type === 'integer') 44 | param.format = 'int64' 45 | 46 | pathParams.push(param) 47 | } 48 | 49 | return pathParams 50 | } 51 | 52 | /** 53 | * Builds a parameter object from parameter definition 54 | * @param name Parameter name 55 | * @param method HTTP method 56 | * @param endpoint Endpoint path 57 | * @param detail Parameter details 58 | * @returns Swagger parameter object 59 | */ 60 | export function buildParam(name: string, method: string, endpoint: string, detail: WordPressArgument): Parameter { 61 | // Determine parameter type 62 | const typeValue = detail.type || 'string' 63 | let type = 'string' 64 | 65 | if (Array.isArray(typeValue) && typeValue.length > 0) 66 | type = typeValue[0] 67 | else if (typeof typeValue === 'string') 68 | type = typeValue 69 | 70 | if (!type) { 71 | // Infer type from name if not specified 72 | if (name.includes('_id') || name.toLowerCase() === 'id') 73 | type = 'integer' 74 | else 75 | type = 'string' 76 | } 77 | 78 | // Determine parameter location 79 | let paramIn: 'path' | 'query' | 'formData' = 'query' 80 | if (endpoint.includes(`{${name}}`)) 81 | paramIn = 'path' 82 | else if (method === 'post' || method === 'put') 83 | paramIn = 'formData' 84 | 85 | // Determine if parameter is required 86 | let required = !!detail.required 87 | if (paramIn === 'path') 88 | required = true // Path parameters are always required 89 | 90 | const param: Parameter = { 91 | type: type as SchemaType, 92 | description: detail.description || '', 93 | in: paramIn, 94 | name, 95 | required, 96 | } 97 | 98 | // Handle enumerations 99 | if (detail.enum) { 100 | param.type = 'array' 101 | // @ts-expect-error 102 | param.items = { type: typeof typeValue === 'string' ? typeValue : 'string', enum: detail.enum } 103 | 104 | if (detail.default !== undefined && param.items) 105 | // @ts-expect-error 106 | param.items.default = detail.default 107 | 108 | // @ts-expect-error 109 | param.collectionFormat = 'multi' 110 | } 111 | // Handle arrays 112 | else if (detail.items) { 113 | // @ts-expect-error 114 | param.items = { type: detail.items.type || 'string' } 115 | } 116 | 117 | // Handle numeric ranges 118 | // @ts-expect-error 119 | detail.maximum !== undefined && (param.maximum = detail.maximum) 120 | 121 | // @ts-expect-error 122 | detail.minimum !== undefined && (param.minimum = detail.minimum) 123 | 124 | // Handle format 125 | if (detail.format) 126 | param.format = detail.format 127 | else if (type === 'integer') 128 | param.format = 'int64' 129 | 130 | // Handle schema 131 | if (detail.schema) 132 | param.schema = detail.schema 133 | 134 | return param 135 | } 136 | 137 | /** 138 | * Extracts parameters from argument definitions 139 | * @param endpoint Endpoint path 140 | * @param args Argument definitions 141 | * @param method HTTP method 142 | * @returns Array of Swagger parameters 143 | */ 144 | export function getParametersFromArgs(endpoint: string, args: Record, method: string): Parameter[] { 145 | const parameters: Parameter[] = [] 146 | 147 | for (const [param, detail] of Object.entries(args)) 148 | parameters.push(buildParam(param, method, endpoint, detail)) 149 | 150 | return parameters 151 | } 152 | -------------------------------------------------------------------------------- /packages/transform/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | fixedExtension: true, 6 | format: ['esm'], 7 | clean: true, 8 | dts: true, 9 | }) 10 | -------------------------------------------------------------------------------- /playground/genapi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@genapi/core' 2 | // create an API pipeline generator using the pipeline provided by genapi 3 | // each pipeline exposes corresponding methods, which can be reused and reorganized 4 | // import pipeline from '@genapi/pipeline' 5 | import { fetch } from '@genapi/presets' 6 | 7 | const config = defineConfig({ 8 | input: 'https://petstore.swagger.io/v2/swagger.json', 9 | pipeline: fetch.ts, 10 | import: { 11 | http: './index.http', 12 | }, 13 | output: { 14 | main: 'dist/index.ts', 15 | type: 'dist/index.type.ts', 16 | }, 17 | }) 18 | 19 | export default config 20 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "genapi" 7 | }, 8 | "dependencies": { 9 | "@genapi/core": "workspace:*", 10 | "@genapi/pipeline": "workspace:*", 11 | "@genapi/presets": "workspace:*", 12 | "@genapi/transform": "workspace:*", 13 | "wpapi-to-swagger": "^0.1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - docs 4 | - packages/* 5 | - examples/* 6 | catalog: 7 | '@antfu/eslint-config': ^4.2.1 8 | '@antfu/ni': ^23.3.1 9 | '@antfu/utils': ^9.0.0 10 | '@iconify-json/svg-spinners': ^1.2.2 11 | '@shikijs/vitepress-twoslash': ^2.3.2 12 | '@types/node': ^22.13.4 13 | '@unocss/reset': ^65.5.0 14 | '@vueuse/core': ^12.7.0 15 | bumpp: ^10.0.3 16 | eslint: ^9.20.1 17 | floating-vue: ^5.2.2 18 | lint-staged: ^15.4.3 19 | pinia: ^3.0.1 20 | pnpm: ^10.4.0 21 | simple-git-hooks: ^2.11.1 22 | tsx: ^4.19.2 23 | typescript: ^5.7.3 24 | unbuild: ^3.3.1 25 | unocss: ^65.5.0 26 | unplugin-vue-components: ^28.1.0 27 | vite: ^6.1.0 28 | vite-tsconfig-paths: ^5.1.4 29 | vitepress: ^2.0.0-alpha.2 30 | vitepress-plugin-group-icons: ^1.3.5 31 | vitest: ^3.0.5 32 | vue: ^3.5.13 33 | -------------------------------------------------------------------------------- /public/case.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairyf/genapi/cbaca615e73de4bf0d505dc0f754425eb4ceeb0b/public/case.gif -------------------------------------------------------------------------------- /public/swag-axios-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hairyf/genapi/cbaca615e73de4bf0d505dc0f754425eb4ceeb0b/public/swag-axios-js.png -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | describe('should', () => { 4 | it('exported', () => { 5 | expect(1).toEqual(1) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "skipDefaultLibCheck": true, 13 | "skipLibCheck": true 14 | }, 15 | "exclude": [ 16 | "playground/dist" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------