├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── comment-release.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── biome.json ├── docs ├── .gitignore ├── README.md ├── astro.config.mjs ├── package.json ├── public │ ├── favicon.svg │ └── og.jpg ├── src │ ├── assets │ │ └── logo.webp │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── contributing.md │ │ │ ├── examples │ │ │ ├── nextjs.md │ │ │ ├── react-router-6.md │ │ │ ├── react-router-7.md │ │ │ ├── tanstack-router.md │ │ │ └── vite-react.md │ │ │ ├── guides │ │ │ ├── cli-options.mdx │ │ │ ├── introduction.mdx │ │ │ └── usage.mdx │ │ │ ├── index.mdx │ │ │ └── license.md │ └── env.d.ts └── tsconfig.json ├── examples ├── nextjs-app │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── components │ │ │ ├── PaginatedPets.tsx │ │ │ └── Pets.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── infinite-loader │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── providers.tsx │ ├── fetchClient.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── tailwind.config.ts │ └── tsconfig.json ├── petstore.yaml ├── react-app │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── axios.ts │ │ ├── components │ │ │ ├── SuspenseChild.tsx │ │ │ └── SuspenseParent.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── queryClient.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── react-router-6-app │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── axios.ts │ │ ├── index.css │ │ ├── main.tsx │ │ ├── queryClient.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── react-router-7-app │ ├── .gitignore │ ├── app │ │ ├── index.css │ │ ├── root.tsx │ │ ├── routes.ts │ │ └── routes │ │ │ └── _index │ │ │ ├── App.css │ │ │ └── route.tsx │ ├── assets │ │ └── react.svg │ ├── fetchClient.ts │ ├── package.json │ ├── providers.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vite.svg │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite-env.d.ts │ └── vite.config.ts └── tanstack-router-app │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ ├── fetchClient.ts │ ├── main.tsx │ ├── routeTree.gen.ts │ └── routes │ │ ├── __root.tsx │ │ ├── about.tsx │ │ └── index.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── lefthook.yml ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── cli.mts ├── common.mts ├── constants.mts ├── createExports.mts ├── createImports.mts ├── createPrefetchOrEnsure.mts ├── createSource.mts ├── createUseMutation.mts ├── createUseQuery.mts ├── format.mts ├── generate.mts ├── print.mts ├── service.mts └── util.mts ├── tests ├── __snapshots__ │ ├── createSource.test.ts.snap │ └── generate.test.ts.snap ├── common.test.ts ├── createExports.test.ts ├── createImports.test.ts ├── createSource.test.ts ├── generate.test.ts ├── inputs │ ├── no-models.yaml │ └── petstore.yaml ├── print.test.ts ├── service.test.ts ├── utils.test.ts └── utils.ts ├── tsconfig.json └── vitest.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [7nohe] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **OpenAPI spec file** 17 | If possible, please upload the OpenAPI spec file. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots or logs to help explain your problem. 24 | 25 | - OS: [e.g. macOS] 26 | - Version [e.g. v1.0.0] 27 | 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/comment-release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | issue_comment: 8 | types: 9 | - created 10 | 11 | jobs: 12 | release: 13 | if: ${{ github.event.issue.pull_request && github.event.comment.body == 'npm publish' }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: ⬇️ Checkout PR 21 | run: | 22 | git fetch origin pull/${{ github.event.issue.number }}/head:pr-find-commit 23 | git checkout pr-find-commit 24 | 25 | - name: Install pnpm 26 | uses: pnpm/action-setup@v3 27 | with: 28 | version: 9 29 | 30 | - name: Install Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20 34 | cache: "pnpm" 35 | registry-url: "https://registry.npmjs.org" 36 | 37 | - name: npm version 38 | run: npm version --no-git-tag-version 0.0.0-$(git rev-parse HEAD) 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | 42 | - name: Install dependencies 43 | run: pnpm install 44 | 45 | # publish to npm tag as next 46 | - run: pnpm publish --no-git-checks --tag pre 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | 50 | - name: "Update comment" 51 | uses: actions/github-script@v7 52 | with: 53 | github-token: ${{ secrets.GITHUB_TOKEN }} 54 | script: | 55 | const { issue: { number: issue_number }, repo: { owner, repo }, payload } = context; 56 | const fs = require('fs') 57 | const jsonString = fs.readFileSync(`${process.env.GITHUB_WORKSPACE}/package.json`) 58 | var packageJson = JSON.parse(jsonString) 59 | const { name: packageName, version } = packageJson; 60 | 61 | const body = [ 62 | `npm package published to pre tag.`, 63 | `\`\`\`bash\nnpm install ${packageName}@pre\n\`\`\`` 64 | `\`\`\`bash\nnpm install ${packageName}@${version}\n\`\`\`` 65 | ].join('\n\n'); 66 | 67 | await github.rest.issues.updateComment({ 68 | owner, 69 | repo, 70 | comment_id: payload.comment.id, 71 | body, 72 | }); 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v3 21 | with: 22 | version: 9 23 | 24 | - name: Install Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | cache: "pnpm" 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - run: npx changelogithub 32 | env: 33 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - run: pnpm publish --no-git-checks 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | concurrency: 3 | group: test-${{ github.github.base_ref }} 4 | cancel-in-progress: true 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - uses: pnpm/action-setup@v3 22 | with: 23 | version: 9 24 | - name: Install Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | cache: "pnpm" 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Build 34 | run: pnpm -w build 35 | 36 | - name: Run codegen for react-app 37 | run: pnpm --filter @7nohe/react-app generate:api 38 | 39 | - name: Run codegen for nextjs-app 40 | run: pnpm --filter nextjs-app generate:api 41 | 42 | - name: Run codegen for tanstack-router-app 43 | run: pnpm --filter tanstack-router-app generate:api 44 | 45 | - name: Archive generated query files 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: generated-query-file-${{ matrix.os }} 49 | path: examples/react-app/openapi/queries 50 | 51 | - name: Run tsc in react-app 52 | run: pnpm --filter @7nohe/react-app test:generated 53 | 54 | - name: Run tsc in nextjs-app 55 | run: pnpm --filter nextjs-app test:generated 56 | 57 | - name: Run tsc in tanstack-router-app 58 | run: pnpm --filter tanstack-router-app test:generated 59 | 60 | - name: Run biome 61 | run: pnpm biome check . 62 | if: ${{ matrix.os == 'ubuntu-latest' }} 63 | 64 | - name: Run test 65 | run: pnpm test 66 | 67 | - name: Report coverage 68 | if: always() && matrix.os == 'ubuntu-latest' 69 | uses: davelosert/vitest-coverage-report-action@v2 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | openapi 27 | *.tsbuildinfo 28 | coverage 29 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daiki Urata 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 | # OpenAPI React Query Codegen 2 | 3 | > Code generator for creating [React Query (also known as TanStack Query)](https://tanstack.com/query) hooks based on your OpenAPI schema. 4 | 5 | [![npm version](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen.svg)](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen) 6 | 7 | ## Features 8 | 9 | - Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks 10 | - Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions 11 | - Generates query keys and functions for query caching 12 | - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) 13 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "files": { 7 | "ignore": [ 8 | ".vscode", 9 | "dist", 10 | "examples/react-app/openapi", 11 | "coverage", 12 | "examples/nextjs-app/openapi", 13 | "examples/nextjs-app/.next", 14 | "examples/tanstack-router-app/openapi", 15 | "examples/tanstack-router-app/src/routeTree.gen.ts", 16 | "examples/react-router-6-app/openapi", 17 | "examples/react-router-7-app/openapi", 18 | "docs/.astro" 19 | ] 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true 25 | } 26 | }, 27 | "formatter": { 28 | "enabled": true, 29 | "indentStyle": "space", 30 | "indentWidth": 2 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This documentation is built using [Astro Starlight](https://starlight.astro.build/). 2 | 3 | You can edit the documentation in the `docs/src/content` directory. 4 | 5 | ## Development 6 | 7 | Preview the docs: 8 | 9 | ```bash 10 | pnpm --filter docs dev 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import starlight from "@astrojs/starlight"; 2 | // @ts-check 3 | import { defineConfig } from "astro/config"; 4 | 5 | const site = "https://openapi-react-query-codegen.vercel.app"; 6 | const ogUrl = new URL("og.jpg", site).href; 7 | const ogImageAlt = "OpenAPI React Query Codegen"; 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | integrations: [ 12 | starlight({ 13 | title: "OpenAPI React Query Codegen", 14 | social: { 15 | github: "https://github.com/7nohe/openapi-react-query-codegen", 16 | }, 17 | head: [ 18 | { 19 | tag: "meta", 20 | attrs: { property: "og:image", content: ogUrl }, 21 | }, 22 | { 23 | tag: "meta", 24 | attrs: { property: "og:image:alt", content: ogImageAlt }, 25 | }, 26 | ], 27 | sidebar: [ 28 | { 29 | label: "Guides", 30 | items: [ 31 | { slug: "guides/introduction" }, 32 | { slug: "guides/usage" }, 33 | { slug: "guides/cli-options" }, 34 | ], 35 | }, 36 | { 37 | label: "Examples", 38 | autogenerate: { directory: "examples" }, 39 | }, 40 | { 41 | slug: "contributing", 42 | }, 43 | { 44 | slug: "license", 45 | }, 46 | ], 47 | }), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/starlight": "^0.28.3", 14 | "astro": "^4.15.3", 15 | "sharp": "^0.32.5", 16 | "@astrojs/check": "^0.9.4", 17 | "typescript": "^5.6.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7nohe/openapi-react-query-codegen/b884ade1ded7db0d47f524e223060019339349d7/docs/public/og.jpg -------------------------------------------------------------------------------- /docs/src/assets/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7nohe/openapi-react-query-codegen/b884ade1ded7db0d47f524e223060019339349d7/docs/src/assets/logo.webp -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { docsSchema } from "@astrojs/starlight/schema"; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing 3 | description: Contributing to OpenAPI React Query Codegen. 4 | --- 5 | 6 | ## Prerequisites 7 | 8 | - Node.js v20.16.0 or later 9 | - pnpm v9 10 | 11 | ## Install dependencies 12 | 13 | ```bash 14 | pnpm install 15 | ``` 16 | 17 | ## Run tests 18 | ```bash 19 | pnpm test 20 | ``` 21 | 22 | ## Run linter 23 | ```bash 24 | pnpm lint 25 | ``` 26 | 27 | ## Run linter and fix 28 | ```bash 29 | pnpm lint:fix 30 | ``` 31 | 32 | ## Update snapshots 33 | ```bash 34 | pnpm snapshot 35 | ``` 36 | 37 | ## Build example and validate generated code 38 | 39 | ```bash 40 | npm run build && pnpm --filter @7nohe/react-app generate:api && pnpm --filter @7nohe/react-app test:generated 41 | ``` 42 | 43 | ## Preview the docs 44 | 45 | ```bash 46 | pnpm --filter docs dev 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/src/content/docs/examples/nextjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Next.js Example 3 | description: A simple example of using Next.js with OpenAPI React Query Codegen. 4 | --- 5 | 6 | Example of using Next.js can be found in the [`examples/nextjs-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/nextjs-app) directory of the repository. 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/examples/react-router-6.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Router 6 Example 3 | description: A simple example of using React Router 6 with OpenAPI React Query Codegen. 4 | --- 5 | 6 | Example of using React Router 6 can be found in the [`examples/react-router-6-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/react-router-6-app) directory of the repository. 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/examples/react-router-7.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Router 7 Example 3 | description: A simple example of using React Router 7 with OpenAPI React Query Codegen. 4 | --- 5 | 6 | Example of using React Router 7 can be found in the [`examples/react-router-7-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/react-router-7-app) directory of the repository. 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/examples/tanstack-router.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TanStack Router Example 3 | description: A simple example of using TanStack Router with OpenAPI React Query Codegen. 4 | --- 5 | 6 | Example of using Next.js can be found in the [`examples/tanstack-router-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/tanstack-router-app) directory of the repository. 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/examples/vite-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vite + React Example 3 | description: A simple example of using Vite with React. 4 | --- 5 | 6 | Example of using Vite with React can be found in the [`examples/react-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/react-app) directory of the repository. 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/cli-options.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: CLI Options 3 | description: CLI options for OpenAPI React Query Codegen. 4 | --- 5 | 6 | ## Options 7 | 8 | ### -i, --input 9 | 10 | The input file path of the OpenAPI schema file. This is a required option. 11 | 12 | ### -o, --output \ 13 | 14 | The output directory path for the generated files. The default value is `openapi`. 15 | 16 | ### --pageParam \ 17 | 18 | Name of the query parameter used for pagination (infinite query). The default value is `page`. 19 | 20 | ### --nextPageParam \ 21 | 22 | Name of the response parameter used for next page. The default value is `nextPage`. 23 | 24 | ## --initialPageParam \ 25 | 26 | Initial page value to infinite query. The default value is `1`. 27 | 28 | ## Client Options 29 | 30 | Due to the generated clients (Under the `openapi/requests` directory) being based on Hey API, you can pass some options to the Hey API client generator. 31 | 32 | You can find what options are passed to the Hey API client generator in the [generate](https://github.com/7nohe/openapi-react-query-codegen/blob/main/src/generate.mts) function. 33 | 34 | ### -c, --client \ 35 | 36 | The HTTP client to use for the generated hooks. The default value is `@hey-api/client-fetch`. 37 | The available options are: 38 | 39 | - `@hey-api/client-fetch` 40 | - `@hey-api/client-axios` 41 | 42 | More details about the clients can be found in [Hey API Documentation](https://heyapi.vercel.app/openapi-ts/clients.html) 43 | 44 | ### --format \ 45 | 46 | Process output folder with formatter? The default value is `false`. 47 | The available options are: 48 | 49 | - `biome` 50 | - `prettier` 51 | 52 | ### --lint \ 53 | 54 | Process output folder with linter? The default value is `false`. 55 | The available options are: 56 | 57 | - `biome` 58 | - `eslint` 59 | 60 | ### --noOperationId 61 | 62 | Do not use operation ID to generate operation names. The default value is `true`. 63 | 64 | ### --enums \ 65 | 66 | Generate enum definitions? The default value is `false`. 67 | The available options are: 68 | 69 | - `javascript` 70 | - `typescript` 71 | 72 | ### --useDateType 73 | 74 | Use Date type instead of string for date. The default value is `false`. 75 | 76 | ### --debug 77 | 78 | Run in debug mode? The default value is `false`. 79 | 80 | ### --noSchemas 81 | 82 | Disable generating JSON schemas. The default value is `false`. 83 | 84 | ### --schemaTypes \ 85 | 86 | Type of JSON schema. The default value is `json`. 87 | The available options are: 88 | 89 | - `json` 90 | - `form` 91 | 92 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: OpenAPI React Query Codegen is a code generator for creating React Query (also known as TanStack Query) hooks based on your OpenAPI schema. 4 | --- 5 | import { Code, Tabs, TabItem, FileTree } from '@astrojs/starlight/components'; 6 | 7 | OpenAPI React Query Codegen is a code generator for creating React Query (also known as TanStack Query) hooks based on your OpenAPI schema. 8 | 9 | ## Features 10 | 11 | - Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks 12 | - Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions 13 | - Generates query keys and functions for query caching 14 | - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) 15 | 16 | 17 | ## Installation 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Register the command to the `scripts` property in your package.json file. 33 | 34 | ```json 35 | { 36 | "scripts": { 37 | "codegen": "openapi-rq -i ./petstore.yaml" 38 | } 39 | } 40 | ``` 41 | 42 | You can also run the command without installing it in your project using the npx command. 43 | 44 | ```bash 45 | $ npx --package @7nohe/openapi-react-query-codegen openapi-rq -i ./petstore.yaml 46 | ``` 47 | 48 | 49 | ## Usage 50 | 51 | For example, let's generate React Query hooks for [the Petstore API](https://github.com/7nohe/openapi-react-query-codegen/blob/main/examples/petstore.yaml). 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Then you will see the generated code in the `openapi` directory. 67 | 68 | ```bash 69 | $ tree openapi 70 | openapi/ 71 | ├── queries 72 | │ ├── common.ts 73 | │ ├── ensureQueryData.ts 74 | │ ├── index.ts 75 | │ ├── infiniteQueries.ts 76 | │ ├── prefetch.ts 77 | │ ├── queries.ts 78 | │ └── suspense.ts 79 | └── requests 80 | ├── index.ts 81 | ├── schemas.gen.ts 82 | ├── services.gen.ts 83 | └── types.gen.ts 84 | ``` 85 | 86 | Before using the generated hooks, you need to install the required dependencies. 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | Then, add the `QueryClientProvider` to your application's entry point. 101 | 102 | Also, you can set the base URL and interceptors for the client generated by `@hey-api/openapi-ts`. 103 | 104 | ```tsx 105 | // src/main.tsx 106 | import React from "react"; 107 | import ReactDOM from "react-dom/client"; 108 | import App from "./App"; 109 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 110 | import { client } from "../openapi/requests/services.gen"; 111 | 112 | client.setConfig({ 113 | baseUrl: "YOUR_BASE_URL", 114 | throwOnError: true, // If you want to handle errors on `onError` callback of `useQuery` and `useMutation`, set this to `true` 115 | }); 116 | 117 | client.interceptors.request.use((config) => { 118 | // Add your request interceptor logic here 119 | return config; 120 | }); 121 | 122 | client.interceptors.response.use((response) => { 123 | // Add your response interceptor logic here 124 | return response; 125 | }); 126 | 127 | export const queryClient = new QueryClient(); 128 | 129 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 130 | 131 | 132 | 133 | 134 | 135 | ); 136 | ``` 137 | 138 | In your React application, you can import the generated hooks and use them like this: 139 | 140 | ```tsx 141 | // src/App.tsx 142 | import { useFindPets } from "../openapi/queries"; 143 | 144 | function App() { 145 | const { data, error, refetch } = useFindPets(); 146 | 147 | if (error) 148 | return ( 149 |
150 |

Failed to fetch pets

151 | 154 |
155 | ); 156 | 157 | return ( 158 | <> 159 |

Pet List

160 |
    {data?.map((pet) =>
  • {pet.name}
  • )}
161 | 162 | ); 163 | } 164 | 165 | export default App; 166 | 167 | ``` 168 | 169 | ## Output directory structure 170 | 171 | 172 | 173 | - openapi 174 | - queries 175 | - index.ts Main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks 176 | - common.ts Common types 177 | - ensureQueryData.ts Generated ensureQueryData functions 178 | - queries.ts Generated query/mutation hooks 179 | - infiniteQueries.ts Generated infinite query hooks 180 | - suspenses.ts Generated suspense hooks 181 | - prefetch.ts Generated prefetch functions 182 | - requests Output code generated by `@hey-api/openapi-ts` 183 | 184 | 185 | 186 | To learn more about `prefetchQuery` and `ensureQueryData` functions, check out the following links: 187 | 188 | - [Prefetching & Router Integration](https://tanstack.com/query/latest/docs/framework/react/guides/prefetching) 189 | - [Server Rendering & Hydration](https://tanstack.com/query/latest/docs/framework/react/guides/ssr) 190 | - [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr) 191 | 192 | 193 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | description: Usage of OpenAPI React Query Codegen. 4 | --- 5 | 6 | After generating the React Query hooks and functions, you can start using them in your React application. 7 | 8 | ## Using the generated `useQuery` hooks 9 | 10 | ```tsx 11 | import { useFindPets } from "../openapi/queries"; 12 | function App() { 13 | const { data } = useFindPets(); 14 | 15 | return ( 16 |
17 |

Pet List

18 |
    {data?.map((pet) =>
  • {pet.name}
  • )}
19 |
20 | ); 21 | } 22 | 23 | export default App; 24 | ``` 25 | 26 | Optionally, you can also use the pure ts client in `openapi/requests/services.gen.ts` to customize your query. 27 | 28 | ```tsx 29 | import { useQuery } from "@tanstack/react-query"; 30 | import { findPets } from "../openapi/requests/services.gen"; 31 | import { useFindPetsKey } from "../openapi/queries"; 32 | 33 | function App() { 34 | // You can still use the auto-generated query key 35 | const { data } = useQuery({ 36 | queryKey: [useFindPetsKey], 37 | queryFn: () => { 38 | // Do something here 39 | return findPets(); 40 | }, 41 | }); 42 | 43 | return
{/* .... */}
; 44 | } 45 | 46 | export default App; 47 | ``` 48 | 49 | ## Using the generated `useQuerySuspense` hooks 50 | 51 | ```tsx 52 | import { useFindPetsSuspense } from "../openapi/queries/suspense"; 53 | function ChildComponent() { 54 | const { data } = useFindPetsSuspense({ 55 | query: { tags: [], limit: 10 }, 56 | }); 57 | 58 | return
    {data?.map((pet, index) =>
  • {pet.name}
  • )}
; 59 | } 60 | 61 | function ParentComponent() { 62 | return ( 63 | <> 64 | loading...}> 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | function App() { 72 | return ( 73 |
74 |

Pet List

75 | 76 |
77 | ); 78 | } 79 | 80 | export default App; 81 | ``` 82 | 83 | ## Using the generated `useMutation` hooks 84 | 85 | ```tsx 86 | import { useAddPet } from "../openapi/queries"; 87 | 88 | function App() { 89 | const { mutate } = useAddPet(); 90 | 91 | const handleAddPet = () => { 92 | mutate({ body: { name: "Fluffy" } }); 93 | }; 94 | 95 | return ( 96 |
97 |

Add Pet

98 | 99 |
100 | ); 101 | } 102 | 103 | export default App; 104 | ``` 105 | 106 | Invalidating queries after a mutation is important to ensure the cache is updated with the new data. This is done by calling the `queryClient.invalidateQueries` function with the query key used by the query hook. 107 | 108 | Learn more about invalidating queries [here](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation). 109 | 110 | To ensure the query key is created the same way as the query hook, you can use the query key function exported by the generated query hooks. 111 | 112 | ```tsx 113 | import { 114 | useFindPetsByStatus, 115 | useAddPet, 116 | UseFindPetsByStatusKeyFn, 117 | } from "../openapi/queries"; 118 | 119 | function App() { 120 | const [status, setStatus] = React.useState(["available"]); 121 | const { data } = useFindPetsByStatus({ query: { status } }); 122 | const { mutate } = useAddPet({ 123 | onSuccess: () => { 124 | queryClient.invalidateQueries({ 125 | // Call the query key function to get the query key 126 | // This is important to ensure the query key is created the same way as the query hook 127 | // This insures the cache is invalidated correctly and is typed correctly 128 | queryKey: [UseFindPetsByStatusKeyFn({ 129 | status 130 | })], 131 | }); 132 | }, 133 | }); 134 | 135 | return ( 136 |
137 |

Pet List

138 |
    {data?.map((pet) =>
  • {pet.name}
  • )}
139 | 146 |
147 | ); 148 | } 149 | 150 | export default App; 151 | ``` 152 | 153 | 154 | ## Using the generated `useInfiniteQuery` hooks 155 | 156 | This feature will generate a function in infiniteQueries.ts when the name specified by the `pageParam` option exists in the query parameters and the name specified by the `nextPageParam` option exists in the response. 157 | 158 | The `initialPageParam` option can be specified to set the intial page to load, defaults to 1. The `nextPageParam` supports dot notation for nested values (i.e. `meta.next`). 159 | 160 | Example Schema: 161 | 162 | ```yml /name: page|nextPage:/ 163 | paths: 164 | /paginated-pets: 165 | get: 166 | description: | 167 | Returns paginated pets from the system that the user has access to 168 | operationId: findPaginatedPets 169 | parameters: 170 | - name: page 171 | in: query 172 | description: page number 173 | required: false 174 | schema: 175 | type: integer 176 | format: int32 177 | - name: tags 178 | in: query 179 | description: tags to filter by 180 | required: false 181 | style: form 182 | schema: 183 | type: array 184 | items: 185 | type: string 186 | - name: limit 187 | in: query 188 | description: maximum number of results to return 189 | required: false 190 | schema: 191 | type: integer 192 | format: int32 193 | responses: 194 | '200': 195 | description: pet response 196 | content: 197 | application/json: 198 | schema: 199 | type: object 200 | properties: 201 | pets: 202 | type: array 203 | items: 204 | $ref: '#/components/schemas/Pet' 205 | nextPage: 206 | type: integer 207 | format: int32 208 | minimum: 1 209 | ``` 210 | 211 | Usage of Generated Hooks: 212 | 213 | ```ts 214 | import { useFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries"; 215 | 216 | const { data, fetchNextPage } = useFindPaginatedPetsInfinite({ 217 | query: { tags: [], limit: 10 } 218 | }); 219 | ``` 220 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: OpenAPI React Query Codegen 3 | description: Get started building your docs site with Starlight. 4 | template: splash 5 | hero: 6 | tagline: Code generator for creating React Query (also known as TanStack Query) hooks based on your OpenAPI schema. 7 | image: 8 | file: ../../assets/logo.webp 9 | actions: 10 | - text: Get Started 11 | link: /guides/introduction/ 12 | icon: right-arrow 13 | - text: View on GitHub 14 | link: https://github.com/7nohe/openapi-react-query-codegen 15 | icon: external 16 | variant: minimal 17 | --- 18 | 19 | import { Card, CardGrid } from '@astrojs/starlight/components'; 20 | 21 | 22 | 23 | 24 | Generates custom react hooks that use React(TanStack) Query's useQuery, useSuspenseQuery, useMutation and useInfiniteQuery hooks. 25 | 26 | 27 | Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions to integrate into frameworks like Next.js and Remix. 28 | 29 | 30 | Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) in case you still want to do type-safe API calls without React Query. 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/src/content/docs/license.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: License 3 | description: License of OpenAPI React Query Codegen. 4 | --- 5 | 6 | Our library uses the MIT license. 7 | However, our library heavily depends on `@hey-api/openapi-ts` which uses the FSL license. 8 | Please be aware of this when using our library. 9 | You can find more information about the license of `@hey-api/openapi-ts` [here](https://heyapi.vercel.app/license.html). -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/components/PaginatedPets.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import React from "react"; 6 | 7 | export default function PaginatedPets() { 8 | const { data, fetchNextPage } = useFindPaginatedPetsInfinite({ 9 | query: { 10 | limit: 10, 11 | tags: [], 12 | }, 13 | }); 14 | 15 | return ( 16 | <> 17 |

Pet List with Pagination

18 |
    19 | {data?.pages.map((group, i) => ( 20 | 21 | {group?.pets?.map((pet) => ( 22 |
  • {pet.name}
  • 23 | ))} 24 |
    25 | ))} 26 |
27 | {data?.pages.at(-1)?.nextPage && ( 28 | 35 | )} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/components/Pets.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFindPets } from "@/openapi/queries"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | 6 | export default function Pets() { 7 | const { data } = useFindPets({ 8 | query: { tags: [], limit: 10 }, 9 | }); 10 | 11 | return ( 12 | <> 13 |

Pet List

14 |
    15 | {Array.isArray(data) && 16 | data?.map((pet, index) => ( 17 |
  • {pet.name}
  • 18 | ))} 19 |
20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7nohe/openapi-react-query-codegen/b884ade1ded7db0d47f524e223060019339349d7/examples/nextjs-app/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs-app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/infinite-loader/page.tsx: -------------------------------------------------------------------------------- 1 | import PaginatedPets from "../components/PaginatedPets"; 2 | 3 | export default async function InfiniteLoaderPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Providers from "./providers"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Next App", 10 | description: "Generated by create next app", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode; 17 | }>) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | HydrationBoundary, 3 | QueryClient, 4 | dehydrate, 5 | } from "@tanstack/react-query"; 6 | import Link from "next/link"; 7 | import { prefetchUseFindPets } from "../openapi/queries/prefetch"; 8 | import Pets from "./components/Pets"; 9 | 10 | export default async function Home() { 11 | const queryClient = new QueryClient(); 12 | 13 | await prefetchUseFindPets(queryClient, { 14 | query: { tags: [], limit: 10 }, 15 | }); 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | Go to Infinite Loader → 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/nextjs-app/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import "../fetchClient"; 5 | 6 | // We can not useState or useRef in a server component, which is why we are 7 | // extracting this part out into 8 | 9 | function makeQueryClient() { 10 | return new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | // With SSR, we usually want to set some default staleTime 14 | // above 0 to avoid refetching immediately on the client 15 | staleTime: 60 * 1000, 16 | }, 17 | }, 18 | }); 19 | } 20 | 21 | let browserQueryClient: QueryClient | undefined = undefined; 22 | 23 | function getQueryClient() { 24 | if (typeof window === "undefined") { 25 | // Server: always make a new query client 26 | return makeQueryClient(); 27 | } 28 | // Browser: make a new query client if we don't already have one 29 | // This is very important so we don't re-make a new client if React 30 | // suspends during the initial render. This may not be needed if we 31 | // have a suspense boundary BELOW the creation of the query client 32 | if (!browserQueryClient) browserQueryClient = makeQueryClient(); 33 | return browserQueryClient; 34 | } 35 | 36 | export default function Providers({ children }: { children: React.ReactNode }) { 37 | // NOTE: Avoid useState when initializing the query client if you don't 38 | // have a suspense boundary between this and the code that may 39 | // suspend because React will throw away the client on the initial 40 | // render if it suspends and there is no boundary 41 | const queryClient = getQueryClient(); 42 | 43 | return ( 44 | {children} 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /examples/nextjs-app/fetchClient.ts: -------------------------------------------------------------------------------- 1 | import { client } from "@/openapi/requests/services.gen"; 2 | 3 | client.setConfig({ 4 | baseUrl: "http://localhost:4010", 5 | }); 6 | -------------------------------------------------------------------------------- /examples/nextjs-app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /examples/nextjs-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "run-p dev:mock dev:next", 7 | "dev:next": "next dev", 8 | "dev:mock": "prism mock ../petstore.yaml --dynamic", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml --format=biome --lint=biome", 13 | "test:generated": "tsc -p ./tsconfig.json --noEmit" 14 | }, 15 | "dependencies": { 16 | "@tanstack/react-query": "^5.59.13", 17 | "@tanstack/react-query-devtools": "^5.32.1", 18 | "next": "^14.2.3", 19 | "react": "^18", 20 | "react-dom": "^18" 21 | }, 22 | "devDependencies": { 23 | "@stoplight/prism-cli": "^5.5.2", 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "npm-run-all": "^4.1.5", 28 | "postcss": "^8", 29 | "tailwindcss": "^3.4.1", 30 | "typescript": "^5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/nextjs-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /examples/nextjs-app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/nextjs-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | required: false 30 | style: form 31 | schema: 32 | type: array 33 | items: 34 | type: string 35 | - name: limit 36 | in: query 37 | description: maximum number of results to return 38 | required: false 39 | schema: 40 | type: integer 41 | format: int32 42 | responses: 43 | '200': 44 | description: pet response 45 | content: 46 | application/json: 47 | schema: 48 | type: array 49 | items: 50 | $ref: '#/components/schemas/Pet' 51 | default: 52 | description: unexpected error 53 | content: 54 | application/json: 55 | schema: 56 | $ref: '#/components/schemas/Error' 57 | post: 58 | description: Creates a new pet in the store. Duplicates are allowed 59 | operationId: addPet 60 | requestBody: 61 | description: Pet to add to the store 62 | required: true 63 | content: 64 | application/json: 65 | schema: 66 | $ref: '#/components/schemas/NewPet' 67 | responses: 68 | '200': 69 | description: pet response 70 | content: 71 | application/json: 72 | schema: 73 | $ref: '#/components/schemas/Pet' 74 | default: 75 | description: unexpected error 76 | content: 77 | application/json: 78 | schema: 79 | $ref: '#/components/schemas/Error' 80 | /not-defined: 81 | get: 82 | deprecated: true 83 | description: This path is not fully defined. 84 | responses: 85 | default: 86 | description: unexpected error 87 | post: 88 | deprecated: true 89 | description: This path is not defined at all. 90 | responses: 91 | default: 92 | description: unexpected error 93 | /pets/{id}: 94 | get: 95 | description: Returns a user based on a single ID, if the user does not have access to the pet 96 | operationId: find pet by id 97 | parameters: 98 | - name: id 99 | in: path 100 | description: ID of pet to fetch 101 | required: true 102 | schema: 103 | type: integer 104 | format: int64 105 | responses: 106 | '200': 107 | description: pet response 108 | content: 109 | application/json: 110 | schema: 111 | $ref: '#/components/schemas/Pet' 112 | default: 113 | description: unexpected error 114 | content: 115 | application/json: 116 | schema: 117 | $ref: '#/components/schemas/Error' 118 | delete: 119 | description: deletes a single pet based on the ID supplied 120 | operationId: deletePet 121 | parameters: 122 | - name: id 123 | in: path 124 | description: ID of pet to delete 125 | required: true 126 | schema: 127 | type: integer 128 | format: int64 129 | responses: 130 | '204': 131 | description: pet deleted 132 | default: 133 | description: unexpected error 134 | content: 135 | application/json: 136 | schema: 137 | $ref: '#/components/schemas/Error' 138 | /paginated-pets: 139 | get: 140 | description: | 141 | Returns paginated pets from the system that the user has access to 142 | operationId: findPaginatedPets 143 | parameters: 144 | - name: page 145 | in: query 146 | description: page number 147 | required: false 148 | schema: 149 | type: integer 150 | format: int32 151 | - name: tags 152 | in: query 153 | description: tags to filter by 154 | required: false 155 | style: form 156 | schema: 157 | type: array 158 | items: 159 | type: string 160 | - name: limit 161 | in: query 162 | description: maximum number of results to return 163 | required: false 164 | schema: 165 | type: integer 166 | format: int32 167 | responses: 168 | '200': 169 | description: pet response 170 | content: 171 | application/json: 172 | schema: 173 | type: object 174 | properties: 175 | pets: 176 | type: array 177 | items: 178 | $ref: '#/components/schemas/Pet' 179 | nextPage: 180 | type: integer 181 | format: int32 182 | minimum: 1 183 | 184 | components: 185 | schemas: 186 | Pet: 187 | allOf: 188 | - $ref: '#/components/schemas/NewPet' 189 | - type: object 190 | required: 191 | - id 192 | properties: 193 | id: 194 | type: integer 195 | format: int64 196 | 197 | NewPet: 198 | type: object 199 | required: 200 | - name 201 | properties: 202 | name: 203 | type: string 204 | tag: 205 | type: string 206 | 207 | Error: 208 | type: object 209 | required: 210 | - code 211 | - message 212 | properties: 213 | code: 214 | type: integer 215 | format: int32 216 | message: 217 | type: string 218 | -------------------------------------------------------------------------------- /examples/react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@7nohe/react-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "run-p dev:mock dev:client", 8 | "dev:client": "vite --clearScreen=false", 9 | "dev:mock": "prism mock ../petstore.yaml --dynamic", 10 | "build": "tsc && vite build", 11 | "preview": "vite preview", 12 | "generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml --format=biome --lint=biome -c @hey-api/client-axios", 13 | "test:generated": "tsc -p ./tsconfig.json --noEmit" 14 | }, 15 | "dependencies": { 16 | "@hey-api/client-axios": "^0.2.7", 17 | "@tanstack/react-query": "^5.59.13", 18 | "@tanstack/react-query-devtools": "^5.32.1", 19 | "axios": "^1.7.7", 20 | "form-data": "~4.0.0", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1" 23 | }, 24 | "devDependencies": { 25 | "@biomejs/biome": "^1.7.2", 26 | "@stoplight/prism-cli": "^5.5.2", 27 | "@types/react": "^18.3.1", 28 | "@types/react-dom": "^18.2.18", 29 | "@vitejs/plugin-react": "^4.2.1", 30 | "npm-run-all": "^4.1.5", 31 | "typescript": "^5.4.5", 32 | "vite": "^5.0.12" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/react-app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-app/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /examples/react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { useState } from "react"; 3 | 4 | import { createClient } from "@hey-api/client-fetch"; 5 | import { 6 | UseFindPetsKeyFn, 7 | useAddPet, 8 | useFindPets, 9 | useGetNotDefined, 10 | usePostNotDefined, 11 | } from "../openapi/queries"; 12 | import { SuspenseParent } from "./components/SuspenseParent"; 13 | import { queryClient } from "./queryClient"; 14 | 15 | function App() { 16 | createClient({ baseUrl: "http://localhost:4010" }); 17 | 18 | const [tags, _setTags] = useState([]); 19 | const [limit, _setLimit] = useState(10); 20 | 21 | const { data, error, refetch } = useFindPets({ query: { tags, limit } }); 22 | // This is an example of using a hook that has all parameters optional; 23 | // Here we do not have to pass in an object 24 | const { data: _ } = useFindPets(); 25 | 26 | // This is an example of a query that is not defined in the OpenAPI spec 27 | // this defaults to any - here we are showing how to override the type 28 | // Note - this is marked as deprecated in the OpenAPI spec and being passed to the client 29 | const { data: notDefined } = useGetNotDefined(); 30 | const { mutate: mutateNotDefined } = usePostNotDefined(); 31 | 32 | const { mutate: addPet, isError } = useAddPet(); 33 | 34 | const [text, setText] = useState(""); 35 | const [errorText, setErrorText] = useState(); 36 | 37 | if (error) 38 | return ( 39 |
40 |

Failed to fetch pets

41 | 44 |
45 | ); 46 | 47 | return ( 48 |
49 |

Pet List

50 | setText(e.target.value)} 55 | /> 56 | 80 | {isError && ( 81 |

86 | {errorText} 87 |

88 | )} 89 |
    90 | {Array.isArray(data) && 91 | data?.map((pet, index) => ( 92 |
  • {pet.name}
  • 93 | ))} 94 |
95 |
96 |

Suspense Components

97 | 98 |
99 |
100 | ); 101 | } 102 | 103 | export default App; 104 | -------------------------------------------------------------------------------- /examples/react-app/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-app/src/axios.ts: -------------------------------------------------------------------------------- 1 | import { client } from "../openapi/requests/services.gen"; 2 | 3 | client.setConfig({ 4 | baseURL: "http://localhost:4010", 5 | throwOnError: true, 6 | }); 7 | -------------------------------------------------------------------------------- /examples/react-app/src/components/SuspenseChild.tsx: -------------------------------------------------------------------------------- 1 | import { useFindPetsSuspense } from "../../openapi/queries/suspense"; 2 | 3 | export const SuspenseChild = () => { 4 | const { data, error } = useFindPetsSuspense({ 5 | query: { tags: [], limit: 10 }, 6 | }); 7 | console.log({ error }); 8 | if (!Array.isArray(data)) { 9 | return
Error!
; 10 | } 11 | 12 | return ( 13 |
    14 | {data?.map((pet) => ( 15 |
  • {pet.name}
  • 16 | ))} 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-app/src/components/SuspenseParent.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import { SuspenseChild } from "./SuspenseChild"; 3 | 4 | export const SuspenseParent = () => { 5 | return ( 6 | loading...}> 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/react-app/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | 72 | input { 73 | border-radius: 8px; 74 | border: 1px solid #ccc; 75 | padding: 0.5em; 76 | font-size: 1em; 77 | font-family: inherit; 78 | background-color: #fff; 79 | color: #000; 80 | transition: border-color 0.25s; 81 | margin: 1em; 82 | } 83 | 84 | input:focus { 85 | border-color: #646cff; 86 | outline: none; 87 | } 88 | -------------------------------------------------------------------------------- /examples/react-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import { QueryClientProvider } from "@tanstack/react-query"; 6 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 7 | import { queryClient } from "./queryClient"; 8 | import "./axios"; 9 | 10 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 11 | 12 | 13 | 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /examples/react-app/src/queryClient.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient(); 4 | -------------------------------------------------------------------------------- /examples/react-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [ 21 | { 22 | "path": "./tsconfig.node.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-router-6-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-router-6-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-router-6-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@7nohe/react-router-6-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "run-p dev:mock dev:client", 8 | "dev:client": "vite --clearScreen=false", 9 | "dev:mock": "prism mock ../petstore.yaml --dynamic", 10 | "build": "tsc && vite build", 11 | "preview": "vite preview", 12 | "generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml --format=biome --lint=biome", 13 | "test:generated": "tsc -p ./tsconfig.json --noEmit" 14 | }, 15 | "dependencies": { 16 | "@hey-api/client-axios": "^0.2.7", 17 | "@tanstack/react-query": "^5.59.13", 18 | "@tanstack/react-query-devtools": "^5.32.1", 19 | "axios": "^1.7.7", 20 | "form-data": "~4.0.0", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1", 23 | "react-router-dom": "^6.27.0" 24 | }, 25 | "devDependencies": { 26 | "@biomejs/biome": "^1.7.2", 27 | "@stoplight/prism-cli": "^5.5.2", 28 | "@types/react": "^18.3.1", 29 | "@types/react-dom": "^18.2.18", 30 | "@vitejs/plugin-react": "^4.2.1", 31 | "npm-run-all": "^4.1.5", 32 | "typescript": "^5.4.5", 33 | "vite": "^5.0.12" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/react-router-6-app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import type { QueryClient } from "@tanstack/react-query"; 3 | import { useState } from "react"; 4 | import { type LoaderFunctionArgs, useLoaderData } from "react-router-dom"; 5 | import { UseFindPetsKeyFn, useAddPet } from "../openapi/queries"; 6 | import { ensureUseFindPetsData } from "../openapi/queries/ensureQueryData"; 7 | import { useFindPetsSuspense } from "../openapi/queries/suspense"; 8 | import { queryClient } from "./queryClient"; 9 | 10 | export const loader = 11 | (queryClient: QueryClient) => async (_: LoaderFunctionArgs) => { 12 | const queryParameters = { 13 | query: { tags: [], limit: 10 }, 14 | }; 15 | 16 | await ensureUseFindPetsData(queryClient, { 17 | query: { tags: [], limit: 10 }, 18 | }); 19 | return queryParameters; 20 | }; 21 | 22 | export function Compoment() { 23 | const queryParameters = useLoaderData() as Awaited< 24 | ReturnType> 25 | >; 26 | 27 | const { data, error, refetch } = useFindPetsSuspense(queryParameters); 28 | 29 | const { mutate: addPet, isError } = useAddPet(); 30 | 31 | const [text, setText] = useState(""); 32 | const [errorText, setErrorText] = useState(); 33 | 34 | if (error) 35 | return ( 36 |
37 |

Failed to fetch pets

38 | 41 |
42 | ); 43 | 44 | return ( 45 |
46 |

Pet List

47 | setText(e.target.value)} 52 | /> 53 | 77 | {isError && ( 78 |

83 | {errorText} 84 |

85 | )} 86 |
    87 | {Array.isArray(data) && 88 | data?.map((pet, index) => ( 89 |
  • {pet.name}
  • 90 | ))} 91 |
92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/axios.ts: -------------------------------------------------------------------------------- 1 | import { client } from "../openapi/requests/services.gen"; 2 | 3 | client.setConfig({ 4 | baseUrl: "http://localhost:4010", 5 | throwOnError: true, 6 | }); 7 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | 72 | input { 73 | border-radius: 8px; 74 | border: 1px solid #ccc; 75 | padding: 0.5em; 76 | font-size: 1em; 77 | font-family: inherit; 78 | background-color: #fff; 79 | color: #000; 80 | transition: border-color 0.25s; 81 | margin: 1em; 82 | } 83 | 84 | input:focus { 85 | border-color: #646cff; 86 | outline: none; 87 | } 88 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Compoment, loader } from "./App"; 4 | import "./index.css"; 5 | import { QueryClientProvider } from "@tanstack/react-query"; 6 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 7 | import { queryClient } from "./queryClient"; 8 | import "./axios"; 9 | import { RouterProvider, createBrowserRouter } from "react-router-dom"; 10 | 11 | const router = createBrowserRouter([ 12 | { 13 | path: "/", 14 | element: , 15 | loader: loader(queryClient), 16 | }, 17 | ]); 18 | 19 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 20 | 21 | 22 | 23 | 24 | 25 | , 26 | ); 27 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/queryClient.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient(); 4 | -------------------------------------------------------------------------------- /examples/react-router-6-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router-6-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [ 21 | { 22 | "path": "./tsconfig.node.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-router-6-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-router-6-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-router-7-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .react-router 27 | build -------------------------------------------------------------------------------- /examples/react-router-7-app/app/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | 72 | input { 73 | border-radius: 8px; 74 | border: 1px solid #ccc; 75 | padding: 0.5em; 76 | font-size: 1em; 77 | font-family: inherit; 78 | background-color: #fff; 79 | color: #000; 80 | transition: border-color 0.25s; 81 | margin: 1em; 82 | } 83 | 84 | input:focus { 85 | border-color: #646cff; 86 | outline: none; 87 | } 88 | -------------------------------------------------------------------------------- /examples/react-router-7-app/app/root.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { Outlet, Scripts, ScrollRestoration } from "react-router"; 3 | import Providers from "../providers"; 4 | 5 | export default function Root() { 6 | return ( 7 | 8 | 9 | 10 | 11 | Vite + React + TS 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-router-7-app/app/routes.ts: -------------------------------------------------------------------------------- 1 | import type { RouteConfig } from "@react-router/dev/routes"; 2 | import { flatRoutes } from "@react-router/fs-routes"; 3 | 4 | export const routes: RouteConfig = flatRoutes(); 5 | -------------------------------------------------------------------------------- /examples/react-router-7-app/app/routes/_index/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /examples/react-router-7-app/app/routes/_index/route.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { 4 | HydrationBoundary, 5 | QueryClient, 6 | dehydrate, 7 | useQueryClient, 8 | } from "@tanstack/react-query"; 9 | import { 10 | UseFindPetsKeyFn, 11 | useAddPet, 12 | useFindPets, 13 | } from "../../../openapi/queries"; 14 | import { prefetchUseFindPets } from "../../../openapi/queries/prefetch"; 15 | import "./App.css"; 16 | import type * as Route from "./+types.route"; 17 | 18 | export async function loader({ params }: Route.LoaderArgs) { 19 | const queryClient = new QueryClient(); 20 | 21 | await prefetchUseFindPets(queryClient, { 22 | query: { tags: [], limit: 10 }, 23 | }); 24 | 25 | return { dehydratedState: dehydrate(queryClient) }; 26 | } 27 | 28 | function Pets() { 29 | const queryClient = useQueryClient(); 30 | const { data, error, refetch } = useFindPets({ 31 | query: { tags: [], limit: 10 }, 32 | }); 33 | 34 | const { mutate: addPet, isError } = useAddPet(); 35 | 36 | const [text, setText] = useState(""); 37 | const [errorText, setErrorText] = useState(); 38 | 39 | if (error) 40 | return ( 41 |
42 |

Failed to fetch pets

43 | 46 |
47 | ); 48 | 49 | return ( 50 |
51 |

Pet List

52 | setText(e.target.value)} 57 | /> 58 | 83 | {isError && ( 84 |

89 | {errorText} 90 |

91 | )} 92 |
    93 | {Array.isArray(data) && 94 | data?.map((pet, index) => ( 95 |
  • {pet.name}
  • 96 | ))} 97 |
98 |
99 | ); 100 | } 101 | 102 | export default function Index({ loaderData }: Route.ComponentProps) { 103 | const { dehydratedState } = loaderData; 104 | return ( 105 | 106 | 107 | 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /examples/react-router-7-app/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-router-7-app/fetchClient.ts: -------------------------------------------------------------------------------- 1 | import { client } from "./openapi/requests/services.gen"; 2 | 3 | client.setConfig({ 4 | baseUrl: "http://localhost:4010", 5 | throwOnError: true, 6 | }); 7 | -------------------------------------------------------------------------------- /examples/react-router-7-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@7nohe/react-router-7-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "run-p dev:mock dev:client", 8 | "dev:client": "react-router dev", 9 | "dev:mock": "prism mock ../petstore.yaml --dynamic", 10 | "build": "pnpm typecheck && react-router build", 11 | "preview": "react-router-serve ./build/server/index.js", 12 | "typegen": "react-router typegen", 13 | "typecheck": "pnpm typegen && tsc", 14 | "generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml --format=biome --lint=biome", 15 | "test:generated": "tsc -p ./tsconfig.json --noEmit" 16 | }, 17 | "dependencies": { 18 | "@react-router/fs-routes": "^7.0.0-pre.2", 19 | "@react-router/node": "^7.0.0-pre.2", 20 | "@react-router/serve": "^7.0.0-pre.2", 21 | "@tanstack/react-query": "^5.59.13", 22 | "@tanstack/react-query-devtools": "^5.32.1", 23 | "form-data": "~4.0.0", 24 | "isbot": "^5", 25 | "react": "^18.3.1", 26 | "react-dom": "^18.3.1", 27 | "react-router": "^7.0.0-pre.2", 28 | "react-router-dom": "^7.0.0-pre.2" 29 | }, 30 | "devDependencies": { 31 | "@biomejs/biome": "^1.7.2", 32 | "@react-router/dev": "^7.0.0-pre.2", 33 | "@stoplight/prism-cli": "^5.5.2", 34 | "@types/react": "^18.3.1", 35 | "@types/react-dom": "^18.2.18", 36 | "@vitejs/plugin-react": "^4.2.1", 37 | "npm-run-all": "^4.1.5", 38 | "typescript": "^5.4.5", 39 | "vite": "^5.0.12" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/react-router-7-app/providers.tsx: -------------------------------------------------------------------------------- 1 | import "./fetchClient"; 2 | import { QueryClientProvider } from "@tanstack/react-query"; 3 | import { QueryClient } from "@tanstack/react-query"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import React from "react"; 6 | 7 | export default function Providers({ children }: { children: React.ReactNode }) { 8 | const [queryClient] = React.useState( 9 | () => 10 | new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | // With SSR, we usually want to set some default staleTime 14 | // above 0 to avoid refetching immediately on the client 15 | staleTime: 30 * 1000, 16 | }, 17 | }, 18 | }), 19 | ); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/react-router-7-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7nohe/openapi-react-query-codegen/b884ade1ded7db0d47f524e223060019339349d7/examples/react-router-7-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-router-7-app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-router-7-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx", 9 | ".react-router/types/**/*" 10 | ], 11 | "compilerOptions": { 12 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 13 | "types": ["@react-router/node", "vite/client"], 14 | "isolatedModules": true, 15 | "esModuleInterop": true, 16 | "jsx": "react-jsx", 17 | "module": "ESNext", 18 | "moduleResolution": "Bundler", 19 | "resolveJsonModule": true, 20 | "target": "ES2022", 21 | "strict": true, 22 | "allowJs": true, 23 | "skipLibCheck": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "baseUrl": ".", 26 | "paths": { 27 | "~/*": ["./app/*"] 28 | }, 29 | "noEmit": true, 30 | "rootDirs": [".", "./.react-router/types"], 31 | "plugins": [{ "name": "@react-router/dev" }] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/react-router-7-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-router-7-app/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router-7-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | reactRouter({ 8 | ssr: true, 9 | }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist/ 9 | .vinxi 10 | .output 11 | .vercel 12 | .netlify 13 | .wrangler 14 | 15 | # IDE 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tanstack-router-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "run-p dev:mock dev:tanstack", 8 | "dev:mock": "prism mock ../petstore.yaml --dynamic", 9 | "typecheck": "tsc --noEmit", 10 | "dev:tanstack": "vite --port=3001", 11 | "build": "vite build", 12 | "serve": "vite preview", 13 | "start": "vite", 14 | "generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ../petstore.yaml --format=biome --lint=biome", 15 | "test:generated": "tsc -p ./tsconfig.json --noEmit" 16 | }, 17 | "devDependencies": { 18 | "@stoplight/prism-cli": "^5.5.2", 19 | "@tanstack/router-plugin": "^1.58.4", 20 | "@types/react": "^18.3.3", 21 | "@types/react-dom": "^18.3.0", 22 | "@vitejs/plugin-react": "^4.3.1", 23 | "npm-run-all": "^4.1.5", 24 | "vite": "^5.4.4" 25 | }, 26 | "dependencies": { 27 | "@tanstack/react-query": "^5.59.13", 28 | "@tanstack/react-query-devtools": "^5.32.1", 29 | "@tanstack/react-router": "^1.58.7", 30 | "@tanstack/react-router-with-query": "^1.58.7", 31 | "@tanstack/router-devtools": "^1.58.7", 32 | "react": "^18.3.1", 33 | "react-dom": "^18.3.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/src/fetchClient.ts: -------------------------------------------------------------------------------- 1 | import { client } from "../openapi/requests/services.gen"; 2 | 3 | client.setConfig({ 4 | baseUrl: "http://localhost:4010", 5 | }); 6 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { RouterProvider, createRouter } from "@tanstack/react-router"; 3 | import "./fetchClient"; 4 | import ReactDOM from "react-dom/client"; 5 | import { routeTree } from "./routeTree.gen"; 6 | 7 | const queryClient = new QueryClient({ 8 | defaultOptions: { 9 | queries: { 10 | staleTime: 60 * 1000, 11 | }, 12 | }, 13 | }); 14 | 15 | // Set up a Router instance 16 | const router = createRouter({ 17 | routeTree, 18 | defaultPreload: "intent", 19 | // Since we're using React Query, we don't want loader calls to ever be stale 20 | // This will ensure that the loader is always called when the route is preloaded or visited 21 | defaultPreloadStaleTime: 0, 22 | context: { 23 | queryClient, 24 | }, 25 | }); 26 | 27 | // Register things for typesafety 28 | declare module "@tanstack/react-router" { 29 | interface Register { 30 | router: typeof router; 31 | } 32 | } 33 | 34 | // biome-ignore lint/style/noNonNullAssertion: This is a demo app 35 | const rootElement = document.getElementById("app")!; 36 | 37 | if (!rootElement.innerHTML) { 38 | const root = ReactDOM.createRoot(rootElement); 39 | root.render( 40 | 41 | 42 | , 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/src/routeTree.gen.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | 5 | // @ts-nocheck 6 | 7 | // noinspection JSUnusedGlobalSymbols 8 | 9 | // This file is auto-generated by TanStack Router 10 | 11 | // Import Routes 12 | 13 | import { Route as rootRoute } from "./routes/__root"; 14 | import { Route as AboutImport } from "./routes/about"; 15 | import { Route as IndexImport } from "./routes/index"; 16 | 17 | // Create/Update Routes 18 | 19 | const AboutRoute = AboutImport.update({ 20 | path: "/about", 21 | getParentRoute: () => rootRoute, 22 | } as any); 23 | 24 | const IndexRoute = IndexImport.update({ 25 | path: "/", 26 | getParentRoute: () => rootRoute, 27 | } as any); 28 | 29 | // Populate the FileRoutesByPath interface 30 | 31 | declare module "@tanstack/react-router" { 32 | interface FileRoutesByPath { 33 | "/": { 34 | id: "/"; 35 | path: "/"; 36 | fullPath: "/"; 37 | preLoaderRoute: typeof IndexImport; 38 | parentRoute: typeof rootRoute; 39 | }; 40 | "/about": { 41 | id: "/about"; 42 | path: "/about"; 43 | fullPath: "/about"; 44 | preLoaderRoute: typeof AboutImport; 45 | parentRoute: typeof rootRoute; 46 | }; 47 | } 48 | } 49 | 50 | // Create and export the route tree 51 | 52 | export interface FileRoutesByFullPath { 53 | "/": typeof IndexRoute; 54 | "/about": typeof AboutRoute; 55 | } 56 | 57 | export interface FileRoutesByTo { 58 | "/": typeof IndexRoute; 59 | "/about": typeof AboutRoute; 60 | } 61 | 62 | export interface FileRoutesById { 63 | __root__: typeof rootRoute; 64 | "/": typeof IndexRoute; 65 | "/about": typeof AboutRoute; 66 | } 67 | 68 | export interface FileRouteTypes { 69 | fileRoutesByFullPath: FileRoutesByFullPath; 70 | fullPaths: "/" | "/about"; 71 | fileRoutesByTo: FileRoutesByTo; 72 | to: "/" | "/about"; 73 | id: "__root__" | "/" | "/about"; 74 | fileRoutesById: FileRoutesById; 75 | } 76 | 77 | export interface RootRouteChildren { 78 | IndexRoute: typeof IndexRoute; 79 | AboutRoute: typeof AboutRoute; 80 | } 81 | 82 | const rootRouteChildren: RootRouteChildren = { 83 | IndexRoute: IndexRoute, 84 | AboutRoute: AboutRoute, 85 | }; 86 | 87 | export const routeTree = rootRoute 88 | ._addFileChildren(rootRouteChildren) 89 | ._addFileTypes(); 90 | 91 | /* prettier-ignore-end */ 92 | 93 | /* ROUTE_MANIFEST_START 94 | { 95 | "routes": { 96 | "__root__": { 97 | "filePath": "__root.tsx", 98 | "children": [ 99 | "/", 100 | "/about" 101 | ] 102 | }, 103 | "/": { 104 | "filePath": "index.tsx" 105 | }, 106 | "/about": { 107 | "filePath": "about.tsx" 108 | } 109 | } 110 | } 111 | ROUTE_MANIFEST_END */ 112 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import type { QueryClient } from "@tanstack/react-query"; 2 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 3 | import { 4 | Link, 5 | Outlet, 6 | createRootRouteWithContext, 7 | } from "@tanstack/react-router"; 8 | import { TanStackRouterDevtools } from "@tanstack/router-devtools"; 9 | 10 | export const Route = createRootRouteWithContext<{ 11 | queryClient: QueryClient; 12 | }>()({ 13 | component: RootComponent, 14 | }); 15 | 16 | function RootComponent() { 17 | return ( 18 | <> 19 |
20 | 27 | Home 28 | {" "} 29 | 35 | About 36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/src/routes/about.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | 3 | export const Route = createFileRoute("/about")({ 4 | component: AboutComponent, 5 | }); 6 | 7 | function AboutComponent() { 8 | return ( 9 |
10 |

About

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import { ensureUseFindPetsData } from "../../openapi/queries/ensureQueryData"; 3 | import { useFindPetsSuspense } from "../../openapi/queries/suspense"; 4 | 5 | export const Route = createFileRoute("/")({ 6 | loader: async ({ context }) => { 7 | await ensureUseFindPetsData(context.queryClient); 8 | }, 9 | component: HomeComponent, 10 | }); 11 | function HomeComponent() { 12 | const petsSuspense = useFindPetsSuspense(); 13 | return ( 14 |
15 |
    16 | {petsSuspense.data?.map?.((post) => { 17 | return ( 18 |
  • 19 |
    {post.name}
    20 |
  • 21 | ); 22 | })} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "jsx": "react-jsx", 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/tanstack-router-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [TanStackRouterVite({}), react()], 8 | }); 9 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | check: 4 | glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" 5 | run: npx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true ./ && git update-index --again 6 | test: 7 | glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" 8 | run: npx vitest run 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@7nohe/openapi-react-query-codegen", 3 | "version": "2.0.0-beta.3", 4 | "description": "OpenAPI React Query Codegen", 5 | "bin": { 6 | "openapi-rq": "dist/cli.mjs" 7 | }, 8 | "private": false, 9 | "type": "module", 10 | "workspaces": ["examples/*"], 11 | "scripts": { 12 | "build": "rimraf dist && tsc -p tsconfig.json", 13 | "lint": "biome check .", 14 | "lint:fix": "biome check --write .", 15 | "preview:react": "npm run build && npm -C examples/react-app run generate:api", 16 | "preview:nextjs": "npm run build && npm -C examples/nextjs-app run generate:api", 17 | "preview:tanstack-router": "npm run build && npm -C examples/tanstack-router-app run generate:api", 18 | "prepublishOnly": "npm run build", 19 | "release": "npx git-ensure -a && npx bumpp --commit --tag --push", 20 | "test": "vitest --coverage.enabled true", 21 | "snapshot": "vitest --update" 22 | }, 23 | "exports": [ 24 | { 25 | "import": "./dist/generate.mjs", 26 | "require": "./dist/generate.mjs", 27 | "types": "./dist/generate.d.mts" 28 | } 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/7nohe/openapi-react-query-codegen.git" 33 | }, 34 | "homepage": "https://github.com/7nohe/openapi-react-query-codegen", 35 | "bugs": "https://github.com/7nohe/openapi-react-query-codegen/issues", 36 | "files": ["dist"], 37 | "keywords": [ 38 | "codegen", 39 | "react-query", 40 | "react", 41 | "openapi", 42 | "swagger", 43 | "typescript", 44 | "openapi-typescript-codegen", 45 | "@hey-api/openapi-ts" 46 | ], 47 | "license": "MIT", 48 | "author": "Daiki Urata (@7nohe)", 49 | "dependencies": { 50 | "@hey-api/client-fetch": "0.4.0", 51 | "@hey-api/openapi-ts": "0.53.8", 52 | "cross-spawn": "^7.0.3" 53 | }, 54 | "devDependencies": { 55 | "@biomejs/biome": "^1.9.3", 56 | "@types/cross-spawn": "^6.0.6", 57 | "@types/node": "^22.7.4", 58 | "@vitest/coverage-v8": "^1.5.0", 59 | "commander": "^12.0.0", 60 | "lefthook": "^1.6.10", 61 | "rimraf": "^5.0.5", 62 | "ts-morph": "^23.0.0", 63 | "typescript": "^5.5.4", 64 | "vitest": "^1.5.0" 65 | }, 66 | "peerDependencies": { 67 | "commander": "12.x", 68 | "ts-morph": "23.x", 69 | "typescript": "5.x" 70 | }, 71 | "packageManager": "pnpm@9.6.0", 72 | "engines": { 73 | "node": ">=14", 74 | "pnpm": ">=9" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ./* 3 | - examples/**/* -------------------------------------------------------------------------------- /src/cli.mts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { readFile } from "node:fs/promises"; 3 | import { dirname, join } from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import { Command, Option } from "commander"; 6 | import { defaultOutputPath } from "./constants.mjs"; 7 | import { generate } from "./generate.mjs"; 8 | 9 | const program = new Command(); 10 | 11 | export type LimitedUserConfig = { 12 | input: string; 13 | output: string; 14 | client?: "@hey-api/client-fetch" | "@hey-api/client-axios"; 15 | format?: "biome" | "prettier"; 16 | lint?: "biome" | "eslint"; 17 | noOperationId?: boolean; 18 | enums?: "javascript" | "typescript" | false; 19 | useDateType?: boolean; 20 | debug?: boolean; 21 | noSchemas?: boolean; 22 | schemaType?: "form" | "json"; 23 | pageParam: string; 24 | nextPageParam: string; 25 | initialPageParam: string | number; 26 | }; 27 | 28 | async function setupProgram() { 29 | const __filename = fileURLToPath(import.meta.url); 30 | const __dirname = dirname(__filename); 31 | const file = await readFile(join(__dirname, "../package.json"), "utf-8"); 32 | const packageJson = JSON.parse(file); 33 | const version = packageJson.version; 34 | 35 | program 36 | .name("openapi-rq") 37 | .version(version) 38 | .description("Generate React Query code based on OpenAPI") 39 | .requiredOption( 40 | "-i, --input ", 41 | "OpenAPI specification, can be a path, url or string content (required)", 42 | ) 43 | .option("-o, --output ", "Output directory", defaultOutputPath) 44 | .addOption( 45 | new Option("-c, --client ", "HTTP client to generate") 46 | .choices(["@hey-api/client-fetch", "@hey-api/client-axios"]) 47 | .default("@hey-api/client-fetch"), 48 | ) 49 | .addOption( 50 | new Option( 51 | "--format ", 52 | "Process output folder with formatter?", 53 | ).choices(["biome", "prettier"]), 54 | ) 55 | .addOption( 56 | new Option( 57 | "--lint ", 58 | "Process output folder with linter?", 59 | ).choices(["biome", "eslint"]), 60 | ) 61 | .option( 62 | "--noOperationId", 63 | "Do not use operationId to generate operation names", 64 | ) 65 | .addOption( 66 | new Option( 67 | "--enums ", 68 | "Generate JavaScript objects from enum definitions?", 69 | ).choices(["javascript", "typescript"]), 70 | ) 71 | .option( 72 | "--useDateType", 73 | "Use Date type instead of string for date types for models, this will not convert the data to a Date object", 74 | ) 75 | .option("--debug", "Run in debug mode?") 76 | .option("--noSchemas", "Disable generating JSON schemas") 77 | .addOption( 78 | new Option( 79 | "--schemaType ", 80 | "Type of JSON schema [Default: 'json']", 81 | ).choices(["form", "json"]), 82 | ) 83 | .option( 84 | "--pageParam ", 85 | "Name of the query parameter used for pagination", 86 | "page", 87 | ) 88 | .option( 89 | "--nextPageParam ", 90 | "Name of the response parameter used for next page", 91 | "nextPage", 92 | ) 93 | .option("--initialPageParam ", "Initial page value to query", "1") 94 | .parse(); 95 | 96 | const options = program.opts(); 97 | 98 | await generate(options, version); 99 | } 100 | 101 | setupProgram(); 102 | -------------------------------------------------------------------------------- /src/constants.mts: -------------------------------------------------------------------------------- 1 | export const defaultOutputPath = "openapi"; 2 | export const queriesOutputPath = "queries"; 3 | export const requestsOutputPath = "requests"; 4 | 5 | export const serviceFileName = "services.gen"; 6 | export const modelsFileName = "types.gen"; 7 | 8 | export const OpenApiRqFiles = { 9 | queries: "queries", 10 | infiniteQueries: "infiniteQueries", 11 | common: "common", 12 | suspense: "suspense", 13 | index: "index", 14 | prefetch: "prefetch", 15 | ensureQueryData: "ensureQueryData", 16 | } as const; 17 | -------------------------------------------------------------------------------- /src/createExports.mts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from "@hey-api/openapi-ts"; 2 | import type { Project } from "ts-morph"; 3 | import ts from "typescript"; 4 | import { capitalizeFirstLetter } from "./common.mjs"; 5 | import { modelsFileName } from "./constants.mjs"; 6 | import { createPrefetchOrEnsure } from "./createPrefetchOrEnsure.mjs"; 7 | import { createUseMutation } from "./createUseMutation.mjs"; 8 | import { createUseQuery } from "./createUseQuery.mjs"; 9 | import type { Service } from "./service.mjs"; 10 | 11 | export const createExports = ({ 12 | service, 13 | client, 14 | project, 15 | pageParam, 16 | nextPageParam, 17 | initialPageParam, 18 | }: { 19 | service: Service; 20 | client: UserConfig["client"]; 21 | project: Project; 22 | pageParam: string; 23 | nextPageParam: string; 24 | initialPageParam: string; 25 | }) => { 26 | const { methods } = service; 27 | const methodDataNames = methods.reduce( 28 | (acc, data) => { 29 | const methodName = data.method.getName(); 30 | acc[`${capitalizeFirstLetter(methodName)}Data`] = methodName; 31 | return acc; 32 | }, 33 | {} as { [key: string]: string }, 34 | ); 35 | const modelsFile = project 36 | .getSourceFiles?.() 37 | .find((sourceFile) => sourceFile.getFilePath().includes(modelsFileName)); 38 | 39 | const modelDeclarations = modelsFile?.getExportedDeclarations(); 40 | const entries = modelDeclarations?.entries(); 41 | const modelNames: string[] = []; 42 | const paginatableMethods: string[] = []; 43 | for (const [key, value] of entries ?? []) { 44 | modelNames.push(key); 45 | const node = value[0].compilerNode; 46 | if (ts.isTypeAliasDeclaration(node) && methodDataNames[key] !== undefined) { 47 | // get the type alias declaration 48 | const typeAliasDeclaration = node.type; 49 | if (typeAliasDeclaration.kind === ts.SyntaxKind.TypeLiteral) { 50 | const query = (typeAliasDeclaration as ts.TypeLiteralNode).members.find( 51 | (m) => 52 | m.kind === ts.SyntaxKind.PropertySignature && 53 | m.name?.getText() === "query", 54 | ); 55 | if ( 56 | query && 57 | ((query as ts.PropertySignature).type as ts.TypeLiteralNode).members 58 | .map((m) => m.name?.getText()) 59 | .includes(pageParam) 60 | ) { 61 | paginatableMethods.push(methodDataNames[key]); 62 | } 63 | } 64 | } 65 | } 66 | 67 | const allGet = methods.filter((m) => 68 | m.httpMethodName.toUpperCase().includes("GET"), 69 | ); 70 | const allPost = methods.filter((m) => 71 | m.httpMethodName.toUpperCase().includes("POST"), 72 | ); 73 | const allPut = methods.filter((m) => 74 | m.httpMethodName.toUpperCase().includes("PUT"), 75 | ); 76 | const allPatch = methods.filter((m) => 77 | m.httpMethodName.toUpperCase().includes("PATCH"), 78 | ); 79 | const allDelete = methods.filter((m) => 80 | m.httpMethodName.toUpperCase().includes("DELETE"), 81 | ); 82 | 83 | const allGetQueries = allGet.map((m) => 84 | createUseQuery({ 85 | functionDescription: m, 86 | client, 87 | pageParam, 88 | nextPageParam, 89 | initialPageParam, 90 | paginatableMethods, 91 | modelNames, 92 | }), 93 | ); 94 | const allPrefetchQueries = allGet.map((m) => 95 | createPrefetchOrEnsure({ ...m, functionType: "prefetch", modelNames }), 96 | ); 97 | const allEnsureQueries = allGet.map((m) => 98 | createPrefetchOrEnsure({ ...m, functionType: "ensure", modelNames }), 99 | ); 100 | 101 | const allPostMutations = allPost.map((m) => 102 | createUseMutation({ functionDescription: m, modelNames, client }), 103 | ); 104 | const allPutMutations = allPut.map((m) => 105 | createUseMutation({ functionDescription: m, modelNames, client }), 106 | ); 107 | const allPatchMutations = allPatch.map((m) => 108 | createUseMutation({ functionDescription: m, modelNames, client }), 109 | ); 110 | const allDeleteMutations = allDelete.map((m) => 111 | createUseMutation({ functionDescription: m, modelNames, client }), 112 | ); 113 | 114 | const allQueries = [...allGetQueries]; 115 | const allMutations = [ 116 | ...allPostMutations, 117 | ...allPutMutations, 118 | ...allPatchMutations, 119 | ...allDeleteMutations, 120 | ]; 121 | 122 | const commonInQueries = allQueries.flatMap( 123 | ({ apiResponse, returnType, key, queryKeyFn }) => [ 124 | apiResponse, 125 | returnType, 126 | key, 127 | queryKeyFn, 128 | ], 129 | ); 130 | const commonInMutations = allMutations.flatMap( 131 | ({ mutationResult, key, mutationKeyFn }) => [ 132 | mutationResult, 133 | key, 134 | mutationKeyFn, 135 | ], 136 | ); 137 | 138 | const allCommon = [...commonInQueries, ...commonInMutations]; 139 | 140 | const mainQueries = allQueries.flatMap(({ queryHook }) => [queryHook]); 141 | const mainMutations = allMutations.flatMap(({ mutationHook }) => [ 142 | mutationHook, 143 | ]); 144 | 145 | const mainExports = [...mainQueries, ...mainMutations]; 146 | 147 | const infiniteQueriesExports = allQueries 148 | .flatMap(({ infiniteQueryHook }) => [infiniteQueryHook]) 149 | .filter(Boolean) as ts.VariableStatement[]; 150 | 151 | const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [ 152 | suspenseQueryHook, 153 | ]); 154 | 155 | const suspenseExports = [...suspenseQueries]; 156 | 157 | const allPrefetches = allPrefetchQueries.flatMap(({ hook }) => [hook]); 158 | 159 | const allEnsures = allEnsureQueries.flatMap(({ hook }) => [hook]); 160 | 161 | const allPrefetchExports = [...allPrefetches]; 162 | 163 | return { 164 | /** 165 | * Common types and variables between queries (regular and suspense) and mutations 166 | */ 167 | allCommon, 168 | /** 169 | * Main exports are the hooks that are used in the components 170 | */ 171 | mainExports, 172 | /** 173 | * Infinite queries exports are the hooks that are used in the infinite scroll components 174 | */ 175 | infiniteQueriesExports, 176 | /** 177 | * Suspense exports are the hooks that are used in the suspense components 178 | */ 179 | suspenseExports, 180 | /** 181 | * Prefetch exports are the hooks that are used in the prefetch components 182 | */ 183 | allPrefetchExports, 184 | 185 | /** 186 | * Ensure exports are the hooks that are used in the loader components 187 | */ 188 | allEnsures, 189 | }; 190 | }; 191 | -------------------------------------------------------------------------------- /src/createImports.mts: -------------------------------------------------------------------------------- 1 | import { posix } from "node:path"; 2 | import type { UserConfig } from "@hey-api/openapi-ts"; 3 | import type { Project } from "ts-morph"; 4 | import ts from "typescript"; 5 | import { modelsFileName, serviceFileName } from "./constants.mjs"; 6 | 7 | const { join } = posix; 8 | 9 | export const createImports = ({ 10 | project, 11 | client, 12 | }: { 13 | project: Project; 14 | client: UserConfig["client"]; 15 | }) => { 16 | const modelsFile = project 17 | .getSourceFiles() 18 | .find((sourceFile) => sourceFile.getFilePath().includes(modelsFileName)); 19 | 20 | const serviceFile = project.getSourceFileOrThrow(`${serviceFileName}.ts`); 21 | 22 | if (!modelsFile) { 23 | console.warn(` 24 | ⚠️ WARNING: No models file found. 25 | This may be an error if \`.components.schemas\` or \`.components.parameters\` is defined in your OpenAPI input.`); 26 | } 27 | 28 | const modelNames = modelsFile 29 | ? Array.from(modelsFile.getExportedDeclarations().keys()) 30 | : []; 31 | 32 | const serviceExports = Array.from( 33 | serviceFile.getExportedDeclarations().keys(), 34 | ); 35 | 36 | const serviceNames = serviceExports; 37 | 38 | const imports = [ 39 | ts.factory.createImportDeclaration( 40 | undefined, 41 | ts.factory.createImportClause( 42 | false, 43 | undefined, 44 | ts.factory.createNamedImports([ 45 | ts.factory.createImportSpecifier( 46 | true, 47 | undefined, 48 | ts.factory.createIdentifier("Options"), 49 | ), 50 | ]), 51 | ), 52 | ts.factory.createStringLiteral( 53 | client === "@hey-api/client-axios" 54 | ? "@hey-api/client-axios" 55 | : "@hey-api/client-fetch", 56 | ), 57 | undefined, 58 | ), 59 | ts.factory.createImportDeclaration( 60 | undefined, 61 | ts.factory.createImportClause( 62 | false, 63 | undefined, 64 | ts.factory.createNamedImports([ 65 | ts.factory.createImportSpecifier( 66 | true, 67 | undefined, 68 | ts.factory.createIdentifier("QueryClient"), 69 | ), 70 | ts.factory.createImportSpecifier( 71 | false, 72 | undefined, 73 | ts.factory.createIdentifier("useQuery"), 74 | ), 75 | ts.factory.createImportSpecifier( 76 | false, 77 | undefined, 78 | ts.factory.createIdentifier("useSuspenseQuery"), 79 | ), 80 | ts.factory.createImportSpecifier( 81 | false, 82 | undefined, 83 | ts.factory.createIdentifier("useMutation"), 84 | ), 85 | ts.factory.createImportSpecifier( 86 | false, 87 | undefined, 88 | ts.factory.createIdentifier("UseQueryResult"), 89 | ), 90 | ts.factory.createImportSpecifier( 91 | false, 92 | undefined, 93 | ts.factory.createIdentifier("UseQueryOptions"), 94 | ), 95 | ts.factory.createImportSpecifier( 96 | false, 97 | undefined, 98 | ts.factory.createIdentifier("UseMutationOptions"), 99 | ), 100 | ts.factory.createImportSpecifier( 101 | false, 102 | undefined, 103 | ts.factory.createIdentifier("UseMutationResult"), 104 | ), 105 | ]), 106 | ), 107 | ts.factory.createStringLiteral("@tanstack/react-query"), 108 | undefined, 109 | ), 110 | ts.factory.createImportDeclaration( 111 | undefined, 112 | ts.factory.createImportClause( 113 | false, 114 | undefined, 115 | ts.factory.createNamedImports([ 116 | // import all class names from service file 117 | ...serviceNames.map((serviceName) => 118 | ts.factory.createImportSpecifier( 119 | false, 120 | undefined, 121 | ts.factory.createIdentifier(serviceName), 122 | ), 123 | ), 124 | ]), 125 | ), 126 | ts.factory.createStringLiteral(join("../requests", serviceFileName)), 127 | undefined, 128 | ), 129 | ]; 130 | if (modelsFile) { 131 | // import all the models by name 132 | imports.push( 133 | ts.factory.createImportDeclaration( 134 | undefined, 135 | ts.factory.createImportClause( 136 | false, 137 | undefined, 138 | ts.factory.createNamedImports([ 139 | ...modelNames.map((modelName) => 140 | ts.factory.createImportSpecifier( 141 | false, 142 | undefined, 143 | ts.factory.createIdentifier(modelName), 144 | ), 145 | ), 146 | ]), 147 | ), 148 | ts.factory.createStringLiteral(join("../requests/", modelsFileName)), 149 | undefined, 150 | ), 151 | ); 152 | } 153 | 154 | if (client === "@hey-api/client-axios") { 155 | imports.push( 156 | ts.factory.createImportDeclaration( 157 | undefined, 158 | ts.factory.createImportClause( 159 | false, 160 | undefined, 161 | ts.factory.createNamedImports([ 162 | ts.factory.createImportSpecifier( 163 | false, 164 | undefined, 165 | ts.factory.createIdentifier("AxiosError"), 166 | ), 167 | ]), 168 | ), 169 | ts.factory.createStringLiteral("axios"), 170 | ), 171 | ); 172 | } 173 | return imports; 174 | }; 175 | -------------------------------------------------------------------------------- /src/createPrefetchOrEnsure.mts: -------------------------------------------------------------------------------- 1 | import type { VariableDeclaration } from "ts-morph"; 2 | import ts from "typescript"; 3 | import { 4 | BuildCommonTypeName, 5 | EqualsOrGreaterThanToken, 6 | getNameFromVariable, 7 | getQueryKeyFnName, 8 | getRequestParamFromMethod, 9 | getVariableArrowFunctionParameters, 10 | } from "./common.mjs"; 11 | import type { FunctionDescription } from "./common.mjs"; 12 | import { 13 | createQueryKeyFromMethod, 14 | hookNameFromMethod, 15 | } from "./createUseQuery.mjs"; 16 | import { addJSDocToNode } from "./util.mjs"; 17 | 18 | /** 19 | * Creates a prefetch/ensure function for a query 20 | */ 21 | function createPrefetchOrEnsureHook({ 22 | requestParams, 23 | method, 24 | functionType, 25 | }: { 26 | requestParams: ts.ParameterDeclaration[]; 27 | method: VariableDeclaration; 28 | functionType: "prefetch" | "ensure"; 29 | }) { 30 | const methodName = getNameFromVariable(method); 31 | const queryName = hookNameFromMethod({ method }); 32 | let customHookName = `prefetch${ 33 | queryName.charAt(0).toUpperCase() + queryName.slice(1) 34 | }`; 35 | 36 | if (functionType === "ensure") { 37 | customHookName = `ensure${ 38 | queryName.charAt(0).toUpperCase() + queryName.slice(1) 39 | }Data`; 40 | } 41 | const queryKey = createQueryKeyFromMethod({ method }); 42 | 43 | // const 44 | const hookExport = ts.factory.createVariableStatement( 45 | // export 46 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 47 | ts.factory.createVariableDeclarationList( 48 | [ 49 | ts.factory.createVariableDeclaration( 50 | ts.factory.createIdentifier(customHookName), 51 | undefined, 52 | undefined, 53 | ts.factory.createArrowFunction( 54 | undefined, 55 | undefined, 56 | [ 57 | ts.factory.createParameterDeclaration( 58 | undefined, 59 | undefined, 60 | "queryClient", 61 | undefined, 62 | ts.factory.createTypeReferenceNode( 63 | ts.factory.createIdentifier("QueryClient"), 64 | ), 65 | ), 66 | ...requestParams, 67 | ], 68 | undefined, 69 | EqualsOrGreaterThanToken, 70 | ts.factory.createCallExpression( 71 | ts.factory.createIdentifier( 72 | `queryClient.${functionType === "prefetch" ? "prefetchQuery" : "ensureQueryData"}`, 73 | ), 74 | undefined, 75 | [ 76 | ts.factory.createObjectLiteralExpression([ 77 | ts.factory.createPropertyAssignment( 78 | ts.factory.createIdentifier("queryKey"), 79 | ts.factory.createCallExpression( 80 | BuildCommonTypeName(getQueryKeyFnName(queryKey)), 81 | undefined, 82 | 83 | [ts.factory.createIdentifier("clientOptions")], 84 | ), 85 | ), 86 | ts.factory.createPropertyAssignment( 87 | ts.factory.createIdentifier("queryFn"), 88 | ts.factory.createArrowFunction( 89 | undefined, 90 | undefined, 91 | [], 92 | undefined, 93 | EqualsOrGreaterThanToken, 94 | ts.factory.createCallExpression( 95 | ts.factory.createPropertyAccessExpression( 96 | ts.factory.createCallExpression( 97 | ts.factory.createIdentifier(methodName), 98 | 99 | undefined, 100 | // { ...clientOptions } 101 | getVariableArrowFunctionParameters(method).length 102 | ? [ 103 | ts.factory.createObjectLiteralExpression([ 104 | ts.factory.createSpreadAssignment( 105 | ts.factory.createIdentifier( 106 | "clientOptions", 107 | ), 108 | ), 109 | ]), 110 | ] 111 | : undefined, 112 | ), 113 | ts.factory.createIdentifier("then"), 114 | ), 115 | undefined, 116 | [ 117 | ts.factory.createArrowFunction( 118 | undefined, 119 | undefined, 120 | [ 121 | ts.factory.createParameterDeclaration( 122 | undefined, 123 | undefined, 124 | ts.factory.createIdentifier("response"), 125 | undefined, 126 | undefined, 127 | undefined, 128 | ), 129 | ], 130 | undefined, 131 | ts.factory.createToken( 132 | ts.SyntaxKind.EqualsGreaterThanToken, 133 | ), 134 | ts.factory.createPropertyAccessExpression( 135 | ts.factory.createIdentifier("response"), 136 | ts.factory.createIdentifier("data"), 137 | ), 138 | ), 139 | ], 140 | ), 141 | ), 142 | ), 143 | ]), 144 | ], 145 | ), 146 | ), 147 | ), 148 | ], 149 | ts.NodeFlags.Const, 150 | ), 151 | ); 152 | return hookExport; 153 | } 154 | 155 | export const createPrefetchOrEnsure = ({ 156 | method, 157 | jsDoc, 158 | functionType, 159 | modelNames, 160 | }: FunctionDescription & { 161 | functionType: "prefetch" | "ensure"; 162 | modelNames: string[]; 163 | }) => { 164 | const requestParam = getRequestParamFromMethod(method, undefined, modelNames); 165 | 166 | const requestParams = requestParam ? [requestParam] : []; 167 | 168 | const prefetchOrEnsureHook = createPrefetchOrEnsureHook({ 169 | requestParams, 170 | method, 171 | functionType, 172 | }); 173 | 174 | const hookWithJsDoc = addJSDocToNode(prefetchOrEnsureHook, jsDoc); 175 | 176 | return { 177 | hook: hookWithJsDoc, 178 | }; 179 | }; 180 | -------------------------------------------------------------------------------- /src/createSource.mts: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | import type { UserConfig } from "@hey-api/openapi-ts"; 3 | import { Project } from "ts-morph"; 4 | import ts from "typescript"; 5 | import { OpenApiRqFiles } from "./constants.mjs"; 6 | import { createExports } from "./createExports.mjs"; 7 | import { createImports } from "./createImports.mjs"; 8 | import { getServices } from "./service.mjs"; 9 | 10 | const createSourceFile = async ({ 11 | outputPath, 12 | client, 13 | pageParam, 14 | nextPageParam, 15 | initialPageParam, 16 | }: { 17 | outputPath: string; 18 | client: UserConfig["client"]; 19 | pageParam: string; 20 | nextPageParam: string; 21 | initialPageParam: string; 22 | }) => { 23 | const project = new Project({ 24 | // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here. 25 | // If you initialize with a tsconfig.json, then it will automatically populate the project 26 | // with the associated source files. 27 | // Read more: https://ts-morph.com/setup/ 28 | skipAddingFilesFromTsConfig: true, 29 | }); 30 | 31 | const sourceFiles = join(process.cwd(), outputPath); 32 | project.addSourceFilesAtPaths(`${sourceFiles}/**/*`); 33 | 34 | const service = await getServices(project); 35 | 36 | const imports = createImports({ 37 | project, 38 | client, 39 | }); 40 | 41 | const exports = createExports({ 42 | service, 43 | client, 44 | project, 45 | pageParam, 46 | nextPageParam, 47 | initialPageParam, 48 | }); 49 | 50 | const commonSource = ts.factory.createSourceFile( 51 | [...imports, ...exports.allCommon], 52 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 53 | ts.NodeFlags.None, 54 | ); 55 | 56 | const commonImport = ts.factory.createImportDeclaration( 57 | undefined, 58 | ts.factory.createImportClause( 59 | false, 60 | ts.factory.createIdentifier("* as Common"), 61 | undefined, 62 | ), 63 | ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), 64 | undefined, 65 | ); 66 | 67 | const commonExport = ts.factory.createExportDeclaration( 68 | undefined, 69 | false, 70 | undefined, 71 | ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), 72 | undefined, 73 | ); 74 | 75 | const queriesExport = ts.factory.createExportDeclaration( 76 | undefined, 77 | false, 78 | undefined, 79 | ts.factory.createStringLiteral(`./${OpenApiRqFiles.queries}`), 80 | undefined, 81 | ); 82 | 83 | const mainSource = ts.factory.createSourceFile( 84 | [commonImport, ...imports, ...exports.mainExports], 85 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 86 | ts.NodeFlags.None, 87 | ); 88 | 89 | const infiniteQueriesSource = ts.factory.createSourceFile( 90 | [commonImport, ...imports, ...exports.infiniteQueriesExports], 91 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 92 | ts.NodeFlags.None, 93 | ); 94 | 95 | const suspenseSource = ts.factory.createSourceFile( 96 | [commonImport, ...imports, ...exports.suspenseExports], 97 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 98 | ts.NodeFlags.None, 99 | ); 100 | 101 | const indexSource = ts.factory.createSourceFile( 102 | [commonExport, queriesExport], 103 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 104 | ts.NodeFlags.None, 105 | ); 106 | 107 | const prefetchSource = ts.factory.createSourceFile( 108 | [commonImport, ...imports, ...exports.allPrefetchExports], 109 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 110 | ts.NodeFlags.None, 111 | ); 112 | 113 | const ensureSource = ts.factory.createSourceFile( 114 | [commonImport, ...imports, ...exports.allEnsures], 115 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 116 | ts.NodeFlags.None, 117 | ); 118 | 119 | return { 120 | commonSource, 121 | infiniteQueriesSource, 122 | mainSource, 123 | suspenseSource, 124 | indexSource, 125 | prefetchSource, 126 | ensureSource, 127 | }; 128 | }; 129 | 130 | export const createSource = async ({ 131 | outputPath, 132 | client, 133 | version, 134 | pageParam, 135 | nextPageParam, 136 | initialPageParam, 137 | }: { 138 | outputPath: string; 139 | client: UserConfig["client"]; 140 | version: string; 141 | pageParam: string; 142 | nextPageParam: string; 143 | initialPageParam: string; 144 | }) => { 145 | const queriesFile = ts.createSourceFile( 146 | `${OpenApiRqFiles.queries}.ts`, 147 | "", 148 | ts.ScriptTarget.Latest, 149 | false, 150 | ts.ScriptKind.TS, 151 | ); 152 | const infiniteQueriesFile = ts.createSourceFile( 153 | `${OpenApiRqFiles.infiniteQueries}.ts`, 154 | "", 155 | ts.ScriptTarget.Latest, 156 | false, 157 | ts.ScriptKind.TS, 158 | ); 159 | const commonFile = ts.createSourceFile( 160 | `${OpenApiRqFiles.common}.ts`, 161 | "", 162 | ts.ScriptTarget.Latest, 163 | false, 164 | ts.ScriptKind.TS, 165 | ); 166 | const suspenseFile = ts.createSourceFile( 167 | `${OpenApiRqFiles.suspense}.ts`, 168 | "", 169 | ts.ScriptTarget.Latest, 170 | false, 171 | ts.ScriptKind.TS, 172 | ); 173 | 174 | const indexFile = ts.createSourceFile( 175 | `${OpenApiRqFiles.index}.ts`, 176 | "", 177 | ts.ScriptTarget.Latest, 178 | false, 179 | ts.ScriptKind.TS, 180 | ); 181 | 182 | const prefetchFile = ts.createSourceFile( 183 | `${OpenApiRqFiles.prefetch}.ts`, 184 | "", 185 | ts.ScriptTarget.Latest, 186 | false, 187 | ts.ScriptKind.TS, 188 | ); 189 | 190 | const ensureQueryDataFile = ts.createSourceFile( 191 | `${OpenApiRqFiles.ensureQueryData}.ts`, 192 | "", 193 | ts.ScriptTarget.Latest, 194 | false, 195 | ts.ScriptKind.TS, 196 | ); 197 | 198 | const printer = ts.createPrinter({ 199 | newLine: ts.NewLineKind.LineFeed, 200 | removeComments: false, 201 | }); 202 | 203 | const { 204 | commonSource, 205 | mainSource, 206 | infiniteQueriesSource, 207 | suspenseSource, 208 | indexSource, 209 | prefetchSource, 210 | ensureSource, 211 | } = await createSourceFile({ 212 | outputPath, 213 | client, 214 | pageParam, 215 | nextPageParam, 216 | initialPageParam, 217 | }); 218 | 219 | const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`; 220 | 221 | const commonResult = 222 | comment + 223 | printer.printNode(ts.EmitHint.Unspecified, commonSource, commonFile); 224 | 225 | const mainResult = 226 | comment + 227 | printer.printNode(ts.EmitHint.Unspecified, mainSource, queriesFile); 228 | 229 | const infiniteQueriesResult = 230 | comment + 231 | printer.printNode( 232 | ts.EmitHint.Unspecified, 233 | infiniteQueriesSource, 234 | infiniteQueriesFile, 235 | ); 236 | 237 | const suspenseResult = 238 | comment + 239 | printer.printNode(ts.EmitHint.Unspecified, suspenseSource, suspenseFile); 240 | 241 | const indexResult = 242 | comment + 243 | printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile); 244 | 245 | const prefetchResult = 246 | comment + 247 | printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile); 248 | 249 | const enqureResult = 250 | comment + 251 | printer.printNode( 252 | ts.EmitHint.Unspecified, 253 | ensureSource, 254 | ensureQueryDataFile, 255 | ); 256 | 257 | return [ 258 | { 259 | name: `${OpenApiRqFiles.index}.ts`, 260 | content: indexResult, 261 | }, 262 | { 263 | name: `${OpenApiRqFiles.common}.ts`, 264 | content: commonResult, 265 | }, 266 | { 267 | name: `${OpenApiRqFiles.infiniteQueries}.ts`, 268 | content: infiniteQueriesResult, 269 | }, 270 | { 271 | name: `${OpenApiRqFiles.queries}.ts`, 272 | content: mainResult, 273 | }, 274 | { 275 | name: `${OpenApiRqFiles.suspense}.ts`, 276 | content: suspenseResult, 277 | }, 278 | { 279 | name: `${OpenApiRqFiles.prefetch}.ts`, 280 | content: prefetchResult, 281 | }, 282 | { 283 | name: `${OpenApiRqFiles.ensureQueryData}.ts`, 284 | content: enqureResult, 285 | }, 286 | ]; 287 | }; 288 | -------------------------------------------------------------------------------- /src/createUseMutation.mts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from "@hey-api/openapi-ts"; 2 | import ts from "typescript"; 3 | import { 4 | BuildCommonTypeName, 5 | EqualsOrGreaterThanToken, 6 | type FunctionDescription, 7 | TContext, 8 | TData, 9 | TError, 10 | capitalizeFirstLetter, 11 | createQueryKeyExport, 12 | createQueryKeyFnExport, 13 | getNameFromVariable, 14 | getQueryKeyFnName, 15 | getVariableArrowFunctionParameters, 16 | queryKeyConstraint, 17 | queryKeyGenericType, 18 | } from "./common.mjs"; 19 | import { createQueryKeyFromMethod } from "./createUseQuery.mjs"; 20 | import { addJSDocToNode } from "./util.mjs"; 21 | 22 | /** 23 | * Awaited> 24 | */ 25 | function generateAwaitedReturnType({ methodName }: { methodName: string }) { 26 | return ts.factory.createTypeReferenceNode( 27 | ts.factory.createIdentifier("Awaited"), 28 | [ 29 | ts.factory.createTypeReferenceNode( 30 | ts.factory.createIdentifier("ReturnType"), 31 | [ 32 | ts.factory.createTypeQueryNode( 33 | ts.factory.createIdentifier(methodName), 34 | 35 | undefined, 36 | ), 37 | ], 38 | ), 39 | ], 40 | ); 41 | } 42 | 43 | export const createUseMutation = ({ 44 | functionDescription: { method, jsDoc }, 45 | modelNames, 46 | client, 47 | }: { 48 | functionDescription: FunctionDescription; 49 | modelNames: string[]; 50 | client: UserConfig["client"]; 51 | }) => { 52 | const methodName = getNameFromVariable(method); 53 | const mutationKey = createQueryKeyFromMethod({ method }); 54 | const awaitedResponseDataType = generateAwaitedReturnType({ 55 | methodName, 56 | }); 57 | 58 | const mutationResult = ts.factory.createTypeAliasDeclaration( 59 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 60 | ts.factory.createIdentifier( 61 | `${capitalizeFirstLetter(methodName)}MutationResult`, 62 | ), 63 | undefined, 64 | awaitedResponseDataType, 65 | ); 66 | 67 | // `TData = Common.AddPetMutationResult` 68 | const responseDataType = ts.factory.createTypeParameterDeclaration( 69 | undefined, 70 | TData, 71 | undefined, 72 | ts.factory.createTypeReferenceNode( 73 | BuildCommonTypeName(mutationResult.name), 74 | ), 75 | ); 76 | 77 | // @hey-api/client-axios -> `TError = AxiosError` 78 | // @hey-api/client-fetch -> `TError = AddPetError` 79 | const responseErrorType = ts.factory.createTypeParameterDeclaration( 80 | undefined, 81 | TError, 82 | undefined, 83 | client === "@hey-api/client-axios" 84 | ? ts.factory.createTypeReferenceNode( 85 | ts.factory.createIdentifier("AxiosError"), 86 | [ 87 | ts.factory.createTypeReferenceNode( 88 | ts.factory.createIdentifier( 89 | `${capitalizeFirstLetter(methodName)}Error`, 90 | ), 91 | ), 92 | ], 93 | ) 94 | : ts.factory.createTypeReferenceNode( 95 | `${capitalizeFirstLetter(methodName)}Error`, 96 | ), 97 | ); 98 | 99 | const methodParameters = 100 | getVariableArrowFunctionParameters(method).length !== 0 101 | ? ts.factory.createTypeReferenceNode( 102 | ts.factory.createIdentifier("Options"), 103 | [ 104 | ts.factory.createTypeReferenceNode( 105 | modelNames.includes(`${capitalizeFirstLetter(methodName)}Data`) 106 | ? `${capitalizeFirstLetter(methodName)}Data` 107 | : "unknown", 108 | ), 109 | ts.factory.createLiteralTypeNode(ts.factory.createTrue()), 110 | ], 111 | ) 112 | : ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); 113 | 114 | const exportHook = ts.factory.createVariableStatement( 115 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 116 | ts.factory.createVariableDeclarationList( 117 | [ 118 | ts.factory.createVariableDeclaration( 119 | ts.factory.createIdentifier( 120 | `use${capitalizeFirstLetter(methodName)}`, 121 | ), 122 | undefined, 123 | undefined, 124 | ts.factory.createArrowFunction( 125 | undefined, 126 | ts.factory.createNodeArray([ 127 | responseDataType, 128 | responseErrorType, 129 | ts.factory.createTypeParameterDeclaration( 130 | undefined, 131 | "TQueryKey", 132 | queryKeyConstraint, 133 | ts.factory.createArrayTypeNode( 134 | ts.factory.createKeywordTypeNode( 135 | ts.SyntaxKind.UnknownKeyword, 136 | ), 137 | ), 138 | ), 139 | ts.factory.createTypeParameterDeclaration( 140 | undefined, 141 | TContext, 142 | undefined, 143 | ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 144 | ), 145 | ]), 146 | [ 147 | ts.factory.createParameterDeclaration( 148 | undefined, 149 | undefined, 150 | ts.factory.createIdentifier("mutationKey"), 151 | ts.factory.createToken(ts.SyntaxKind.QuestionToken), 152 | queryKeyGenericType, 153 | ), 154 | ts.factory.createParameterDeclaration( 155 | undefined, 156 | undefined, 157 | ts.factory.createIdentifier("options"), 158 | ts.factory.createToken(ts.SyntaxKind.QuestionToken), 159 | ts.factory.createTypeReferenceNode( 160 | ts.factory.createIdentifier("Omit"), 161 | [ 162 | ts.factory.createTypeReferenceNode( 163 | ts.factory.createIdentifier("UseMutationOptions"), 164 | [ 165 | ts.factory.createTypeReferenceNode(TData), 166 | ts.factory.createTypeReferenceNode(TError), 167 | methodParameters, 168 | ts.factory.createTypeReferenceNode(TContext), 169 | ], 170 | ), 171 | ts.factory.createUnionTypeNode([ 172 | ts.factory.createLiteralTypeNode( 173 | ts.factory.createStringLiteral("mutationKey"), 174 | ), 175 | ts.factory.createLiteralTypeNode( 176 | ts.factory.createStringLiteral("mutationFn"), 177 | ), 178 | ]), 179 | ], 180 | ), 181 | undefined, 182 | ), 183 | ], 184 | undefined, 185 | ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 186 | ts.factory.createCallExpression( 187 | ts.factory.createIdentifier("useMutation"), 188 | [ 189 | ts.factory.createTypeReferenceNode(TData), 190 | ts.factory.createTypeReferenceNode(TError), 191 | methodParameters, 192 | ts.factory.createTypeReferenceNode(TContext), 193 | ], 194 | [ 195 | ts.factory.createObjectLiteralExpression([ 196 | ts.factory.createPropertyAssignment( 197 | ts.factory.createIdentifier("mutationKey"), 198 | ts.factory.createCallExpression( 199 | BuildCommonTypeName(getQueryKeyFnName(mutationKey)), 200 | undefined, 201 | [ts.factory.createIdentifier("mutationKey")], 202 | ), 203 | ), 204 | ts.factory.createPropertyAssignment( 205 | ts.factory.createIdentifier("mutationFn"), 206 | // (clientOptions) => addPet(clientOptions).then(response => response.data as TData) as unknown as Promise 207 | ts.factory.createArrowFunction( 208 | undefined, 209 | undefined, 210 | [ 211 | ts.factory.createParameterDeclaration( 212 | undefined, 213 | undefined, 214 | ts.factory.createIdentifier("clientOptions"), 215 | undefined, 216 | undefined, 217 | undefined, 218 | ), 219 | ], 220 | undefined, 221 | EqualsOrGreaterThanToken, 222 | ts.factory.createAsExpression( 223 | ts.factory.createAsExpression( 224 | ts.factory.createCallExpression( 225 | ts.factory.createIdentifier(methodName), 226 | undefined, 227 | getVariableArrowFunctionParameters(method).length > 228 | 0 229 | ? [ts.factory.createIdentifier("clientOptions")] 230 | : undefined, 231 | ), 232 | ts.factory.createKeywordTypeNode( 233 | ts.SyntaxKind.UnknownKeyword, 234 | ), 235 | ), 236 | 237 | ts.factory.createTypeReferenceNode( 238 | ts.factory.createIdentifier("Promise"), 239 | [ts.factory.createTypeReferenceNode(TData)], 240 | ), 241 | ), 242 | ), 243 | ), 244 | ts.factory.createSpreadAssignment( 245 | ts.factory.createIdentifier("options"), 246 | ), 247 | ]), 248 | ], 249 | ), 250 | ), 251 | ), 252 | ], 253 | ts.NodeFlags.Const, 254 | ), 255 | ); 256 | 257 | const hookWithJsDoc = addJSDocToNode(exportHook, jsDoc); 258 | 259 | const mutationKeyExport = createQueryKeyExport({ 260 | methodName, 261 | queryKey: mutationKey, 262 | }); 263 | 264 | const mutationKeyFn = createQueryKeyFnExport(mutationKey, method, "mutation"); 265 | 266 | return { 267 | mutationResult, 268 | key: mutationKeyExport, 269 | mutationHook: hookWithJsDoc, 270 | mutationKeyFn, 271 | }; 272 | }; 273 | -------------------------------------------------------------------------------- /src/format.mts: -------------------------------------------------------------------------------- 1 | import { sync } from "cross-spawn"; 2 | import { IndentationText, NewLineKind, Project, QuoteKind } from "ts-morph"; 3 | import type { LimitedUserConfig } from "./cli.mjs"; 4 | 5 | export const formatOutput = async (outputPath: string) => { 6 | const project = new Project({ 7 | skipAddingFilesFromTsConfig: true, 8 | manipulationSettings: { 9 | indentationText: IndentationText.TwoSpaces, 10 | newLineKind: NewLineKind.LineFeed, 11 | quoteKind: QuoteKind.Double, 12 | usePrefixAndSuffixTextForRename: false, 13 | useTrailingCommas: true, 14 | }, 15 | }); 16 | 17 | const sourceFiles = project.addSourceFilesAtPaths(`${outputPath}/**/*`); 18 | 19 | const tasks = sourceFiles.map((sourceFile) => { 20 | sourceFile.formatText(); 21 | sourceFile.fixMissingImports(); 22 | sourceFile.organizeImports(); 23 | return sourceFile.save(); 24 | }); 25 | 26 | await Promise.all(tasks); 27 | }; 28 | 29 | type OutputProcesser = { 30 | args: (path: string) => ReadonlyArray; 31 | command: string; 32 | name: string; 33 | }; 34 | 35 | const formatters: Record< 36 | Extract, 37 | OutputProcesser 38 | > = { 39 | biome: { 40 | args: (path) => ["format", "--write", path], 41 | command: "biome", 42 | name: "Biome (Format)", 43 | }, 44 | prettier: { 45 | args: (path) => [ 46 | "--ignore-unknown", 47 | path, 48 | "--write", 49 | "--ignore-path", 50 | "./.prettierignore", 51 | ], 52 | command: "prettier", 53 | name: "Prettier", 54 | }, 55 | }; 56 | 57 | /** 58 | * Map of supported linters 59 | */ 60 | const linters: Record< 61 | Extract, 62 | OutputProcesser 63 | > = { 64 | biome: { 65 | args: (path) => ["lint", "--write", path], 66 | command: "biome", 67 | name: "Biome (Lint)", 68 | }, 69 | eslint: { 70 | args: (path) => [path, "--fix"], 71 | command: "eslint", 72 | name: "ESLint", 73 | }, 74 | }; 75 | 76 | export const processOutput = async ({ 77 | output, 78 | format, 79 | lint, 80 | }: { 81 | output: string; 82 | format?: "prettier" | "biome"; 83 | lint?: "biome" | "eslint"; 84 | }) => { 85 | if (format) { 86 | const module = formatters[format]; 87 | console.log(`✨ Running ${module.name} on queries`); 88 | sync(module.command, module.args(output)); 89 | } 90 | 91 | if (lint) { 92 | const module = linters[lint]; 93 | console.log(`✨ Running ${module.name} on queries`); 94 | sync(module.command, module.args(output)); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/generate.mts: -------------------------------------------------------------------------------- 1 | import { type UserConfig, createClient } from "@hey-api/openapi-ts"; 2 | import type { LimitedUserConfig } from "./cli.mjs"; 3 | import { 4 | buildQueriesOutputPath, 5 | buildRequestsOutputPath, 6 | formatOptions, 7 | } from "./common.mjs"; 8 | import { createSource } from "./createSource.mjs"; 9 | import { formatOutput, processOutput } from "./format.mjs"; 10 | import { print } from "./print.mjs"; 11 | 12 | export async function generate(options: LimitedUserConfig, version: string) { 13 | const openApiOutputPath = buildRequestsOutputPath(options.output); 14 | const formattedOptions = formatOptions(options); 15 | 16 | const config: UserConfig = { 17 | client: formattedOptions.client, 18 | debug: formattedOptions.debug, 19 | dryRun: false, 20 | exportCore: true, 21 | output: { 22 | format: formattedOptions.format, 23 | lint: formattedOptions.lint, 24 | path: openApiOutputPath, 25 | }, 26 | input: formattedOptions.input, 27 | schemas: { 28 | export: !formattedOptions.noSchemas, 29 | type: formattedOptions.schemaType, 30 | }, 31 | services: { 32 | export: true, 33 | asClass: false, 34 | operationId: !formattedOptions.noOperationId, 35 | }, 36 | types: { 37 | dates: formattedOptions.useDateType, 38 | export: true, 39 | enums: formattedOptions.enums, 40 | }, 41 | useOptions: true, 42 | }; 43 | await createClient(config); 44 | const source = await createSource({ 45 | outputPath: openApiOutputPath, 46 | client: formattedOptions.client, 47 | version, 48 | pageParam: formattedOptions.pageParam, 49 | nextPageParam: formattedOptions.nextPageParam, 50 | initialPageParam: formattedOptions.initialPageParam.toString(), 51 | }); 52 | await print(source, formattedOptions); 53 | const queriesOutputPath = buildQueriesOutputPath(options.output); 54 | await formatOutput(queriesOutputPath); 55 | await processOutput({ 56 | output: queriesOutputPath, 57 | format: formattedOptions.format, 58 | lint: formattedOptions.lint, 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/print.mts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "node:fs/promises"; 2 | import path from "node:path"; 3 | import type { LimitedUserConfig } from "./cli.mjs"; 4 | import { buildQueriesOutputPath, exists } from "./common.mjs"; 5 | 6 | async function printGeneratedTS( 7 | result: { 8 | name: string; 9 | content: string; 10 | }, 11 | options: Pick, 12 | ) { 13 | const dir = buildQueriesOutputPath(options.output); 14 | const dirExists = await exists(dir); 15 | if (!dirExists) { 16 | await mkdir(dir, { recursive: true }); 17 | } 18 | await writeFile(path.join(dir, result.name), result.content); 19 | } 20 | 21 | export async function print( 22 | results: { 23 | name: string; 24 | content: string; 25 | }[], 26 | options: Pick, 27 | ) { 28 | const outputPath = options.output; 29 | const dirExists = await exists(outputPath); 30 | if (!dirExists) { 31 | await mkdir(outputPath); 32 | } 33 | 34 | const promises = results.map(async (result) => { 35 | await printGeneratedTS(result, options); 36 | }); 37 | 38 | await Promise.all(promises); 39 | } 40 | -------------------------------------------------------------------------------- /src/service.mts: -------------------------------------------------------------------------------- 1 | import type { Project, SourceFile } from "ts-morph"; 2 | import ts from "typescript"; 3 | import type { FunctionDescription } from "./common.mjs"; 4 | import { serviceFileName } from "./constants.mjs"; 5 | 6 | export type Service = { 7 | node: SourceFile; 8 | methods: Array; 9 | }; 10 | 11 | export async function getServices(project: Project): Promise { 12 | const node = project 13 | .getSourceFiles() 14 | .find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName)); 15 | 16 | if (!node) { 17 | throw new Error("No service node found"); 18 | } 19 | 20 | const methods = getMethodsFromService(node); 21 | return { 22 | methods, 23 | node, 24 | } satisfies Service; 25 | } 26 | 27 | export function getMethodsFromService(node: SourceFile): FunctionDescription[] { 28 | const variableStatements = node.getVariableStatements(); 29 | 30 | // The first variable statement is `const client = createClient(createConfig())`, so we skip it 31 | return variableStatements.splice(1).flatMap((variableStatement) => { 32 | const declarations = variableStatement.getDeclarations(); 33 | return declarations.map((declaration) => { 34 | if (!ts.isVariableDeclaration(declaration.compilerNode)) { 35 | throw new Error("Variable declaration not found"); 36 | } 37 | const initializer = declaration.getInitializer(); 38 | if (!initializer) { 39 | throw new Error("Initializer not found"); 40 | } 41 | if (!ts.isArrowFunction(initializer.compilerNode)) { 42 | throw new Error("Arrow function not found"); 43 | } 44 | const methodBlockNode = initializer.compilerNode.body; 45 | if (!methodBlockNode || !ts.isBlock(methodBlockNode)) { 46 | throw new Error("Method block not found"); 47 | } 48 | const foundReturnStatement = methodBlockNode.statements.find( 49 | (s) => s.kind === ts.SyntaxKind.ReturnStatement, 50 | ); 51 | if (!foundReturnStatement) { 52 | throw new Error("Return statement not found"); 53 | } 54 | const returnStatement = foundReturnStatement as ts.ReturnStatement; 55 | const foundCallExpression = returnStatement.expression; 56 | if (!foundCallExpression) { 57 | throw new Error("Call expression not found"); 58 | } 59 | const callExpression = foundCallExpression as ts.CallExpression; 60 | 61 | const propertyAccessExpression = 62 | callExpression.expression as ts.PropertyAccessExpression; 63 | const httpMethodName = propertyAccessExpression.name.getText(); 64 | 65 | if (!httpMethodName) { 66 | throw new Error("httpMethodName not found"); 67 | } 68 | 69 | const getAllChildren = (tsNode: ts.Node): Array => { 70 | const childItems = tsNode.getChildren(node.compilerNode); 71 | if (childItems.length) { 72 | const allChildren = childItems.map(getAllChildren); 73 | return [tsNode].concat(allChildren.flat()); 74 | } 75 | return [tsNode]; 76 | }; 77 | 78 | const children = getAllChildren(initializer.compilerNode); 79 | // get all JSDoc comments 80 | // this should be an array of 1 or 0 81 | const jsDocs = children 82 | .filter((c) => c.kind === ts.SyntaxKind.JSDoc) 83 | .map((c) => c.getText(node.compilerNode)); 84 | // get the first JSDoc comment 85 | const jsDoc = jsDocs?.[0]; 86 | const isDeprecated = children.some( 87 | (c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag, 88 | ); 89 | 90 | const methodDescription: FunctionDescription = { 91 | node, 92 | method: declaration, 93 | methodBlock: methodBlockNode, 94 | httpMethodName, 95 | jsDoc, 96 | isDeprecated, 97 | } satisfies FunctionDescription; 98 | 99 | return methodDescription; 100 | }); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/util.mts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function addJSDocToNode( 4 | node: T, 5 | jsDoc: string | undefined, 6 | ): T { 7 | if (!jsDoc) { 8 | return node; 9 | } 10 | // replace the first /** with * 11 | // we do this because ts.addSyntheticLeadingComment will add /* to the beginning but we want /** 12 | const removedFirstLine = jsDoc.trim().replace(/^\/\*\*/, "*"); 13 | // remove the last */ because ts.addSyntheticLeadingComment will add it 14 | const removedSecondLine = removedFirstLine.replace(/\*\/$/, ""); 15 | 16 | const split = removedSecondLine.split("\n"); 17 | const trimmed = split.map((line) => line.trim()); 18 | const joined = trimmed.join("\n"); 19 | 20 | const nodeWithJSDoc = ts.addSyntheticLeadingComment( 21 | node, 22 | ts.SyntaxKind.MultiLineCommentTrivia, 23 | joined, 24 | true, 25 | ); 26 | 27 | return nodeWithJSDoc; 28 | } 29 | -------------------------------------------------------------------------------- /tests/createExports.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { Project, SyntaxKind } from "ts-morph"; 3 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 4 | import { createExports } from "../src/createExports.mts"; 5 | import { getServices } from "../src/service.mts"; 6 | import { cleanOutputs, generateTSClients, outputPath } from "./utils"; 7 | 8 | const fileName = "createExports"; 9 | 10 | describe(fileName, () => { 11 | beforeAll(async () => await generateTSClients(fileName)); 12 | afterAll(async () => await cleanOutputs(fileName)); 13 | 14 | test("createExports", async () => { 15 | const project = new Project({ 16 | skipAddingFilesFromTsConfig: true, 17 | }); 18 | project.addSourceFilesAtPaths(path.join(outputPath(fileName), "**", "*")); 19 | const service = await getServices(project); 20 | const exports = createExports({ 21 | service, 22 | project, 23 | pageParam: "page", 24 | nextPageParam: "nextPage", 25 | initialPageParam: "initial", 26 | client: "@hey-api/client-fetch", 27 | }); 28 | 29 | const commonTypes = exports.allCommon 30 | .filter((c) => c.kind === SyntaxKind.TypeAliasDeclaration) 31 | // @ts-ignore 32 | .map((e) => e.name.escapedText); 33 | expect(commonTypes).toStrictEqual([ 34 | "FindPetsDefaultResponse", 35 | "FindPetsQueryResult", 36 | "GetNotDefinedDefaultResponse", 37 | "GetNotDefinedQueryResult", 38 | "FindPetByIdDefaultResponse", 39 | "FindPetByIdQueryResult", 40 | "FindPaginatedPetsDefaultResponse", 41 | "FindPaginatedPetsQueryResult", 42 | "AddPetMutationResult", 43 | "PostNotDefinedMutationResult", 44 | "DeletePetMutationResult", 45 | ]); 46 | 47 | const constants = exports.allCommon 48 | .filter((c) => c.kind === SyntaxKind.VariableStatement) 49 | // @ts-ignore 50 | .map((c) => c.declarationList.declarations[0].name.escapedText); 51 | expect(constants).toStrictEqual([ 52 | "useFindPetsKey", 53 | "UseFindPetsKeyFn", 54 | "useGetNotDefinedKey", 55 | "UseGetNotDefinedKeyFn", 56 | "useFindPetByIdKey", 57 | "UseFindPetByIdKeyFn", 58 | "useFindPaginatedPetsKey", 59 | "UseFindPaginatedPetsKeyFn", 60 | "useAddPetKey", 61 | "UseAddPetKeyFn", 62 | "usePostNotDefinedKey", 63 | "UsePostNotDefinedKeyFn", 64 | "useDeletePetKey", 65 | "UseDeletePetKeyFn", 66 | ]); 67 | 68 | const mainExports = exports.mainExports.map( 69 | // @ts-ignore 70 | (e) => e.declarationList.declarations[0].name.escapedText, 71 | ); 72 | expect(mainExports).toStrictEqual([ 73 | "useFindPets", 74 | "useGetNotDefined", 75 | "useFindPetById", 76 | "useFindPaginatedPets", 77 | "useAddPet", 78 | "usePostNotDefined", 79 | "useDeletePet", 80 | ]); 81 | 82 | const suspenseExports = exports.suspenseExports.map( 83 | // @ts-ignore 84 | (e) => e.declarationList.declarations[0].name.escapedText, 85 | ); 86 | expect(suspenseExports).toStrictEqual([ 87 | "useFindPetsSuspense", 88 | "useGetNotDefinedSuspense", 89 | "useFindPetByIdSuspense", 90 | "useFindPaginatedPetsSuspense", 91 | ]); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /tests/createImports.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { Project } from "ts-morph"; 3 | import { describe, expect, test } from "vitest"; 4 | import { createImports } from "../src/createImports.mts"; 5 | import { cleanOutputs, generateTSClients, outputPath } from "./utils"; 6 | 7 | const fileName = "createImports"; 8 | 9 | describe(fileName, () => { 10 | test("createImports", async () => { 11 | await generateTSClients(fileName); 12 | const project = new Project({ 13 | skipAddingFilesFromTsConfig: true, 14 | }); 15 | project.addSourceFilesAtPaths(path.join(outputPath(fileName), "**", "*")); 16 | const imports = createImports({ 17 | project, 18 | }); 19 | 20 | // @ts-ignore 21 | const moduleNames = imports.map((i) => i.moduleSpecifier.text); 22 | expect(moduleNames).toStrictEqual([ 23 | "@hey-api/client-fetch", 24 | "@tanstack/react-query", 25 | "../requests/services.gen", 26 | "../requests/types.gen", 27 | ]); 28 | await cleanOutputs(fileName); 29 | }); 30 | 31 | test("createImports (No models)", async () => { 32 | const fileName = "createImportsNoModels"; 33 | await generateTSClients(fileName, "no-models.yaml"); 34 | const project = new Project({ 35 | skipAddingFilesFromTsConfig: true, 36 | }); 37 | project.addSourceFilesAtPaths(path.join(outputPath(fileName), "**", "*")); 38 | const imports = createImports({ 39 | project, 40 | }); 41 | 42 | // @ts-ignore 43 | const moduleNames = imports.map((i) => i.moduleSpecifier.text); 44 | expect(moduleNames).toStrictEqual([ 45 | "@hey-api/client-fetch", 46 | "@tanstack/react-query", 47 | "../requests/services.gen", 48 | "../requests/types.gen", 49 | ]); 50 | await cleanOutputs(fileName); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/createSource.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 2 | import { createSource } from "../src/createSource.mjs"; 3 | import { cleanOutputs, generateTSClients, outputPath } from "./utils"; 4 | const fileName = "createSource"; 5 | describe(fileName, () => { 6 | beforeAll(async () => await generateTSClients(fileName)); 7 | afterAll(async () => await cleanOutputs(fileName)); 8 | 9 | test("createSource - @hey-api/client-fetch", async () => { 10 | const source = await createSource({ 11 | outputPath: outputPath(fileName), 12 | version: "1.0.0", 13 | pageParam: "page", 14 | nextPageParam: "nextPage", 15 | initialPageParam: "1", 16 | client: "@hey-api/client-fetch", 17 | }); 18 | 19 | const indexTs = source.find((s) => s.name === "index.ts"); 20 | expect(indexTs?.content).toMatchSnapshot(); 21 | 22 | const commonTs = source.find((s) => s.name === "common.ts"); 23 | expect(commonTs?.content).toMatchSnapshot(); 24 | 25 | const queriesTs = source.find((s) => s.name === "queries.ts"); 26 | expect(queriesTs?.content).toMatchSnapshot(); 27 | 28 | const suspenseTs = source.find((s) => s.name === "suspense.ts"); 29 | expect(suspenseTs?.content).toMatchSnapshot(); 30 | 31 | const prefetchTs = source.find((s) => s.name === "prefetch.ts"); 32 | expect(prefetchTs?.content).toMatchSnapshot(); 33 | }); 34 | 35 | test("createSource - @hey-api/client-axios", async () => { 36 | const source = await createSource({ 37 | outputPath: outputPath(fileName), 38 | version: "1.0.0", 39 | pageParam: "page", 40 | nextPageParam: "nextPage", 41 | initialPageParam: "1", 42 | client: "@hey-api/client-axios", 43 | }); 44 | 45 | const indexTs = source.find((s) => s.name === "index.ts"); 46 | expect(indexTs?.content).toMatchSnapshot(); 47 | 48 | const commonTs = source.find((s) => s.name === "common.ts"); 49 | expect(commonTs?.content).toMatchSnapshot(); 50 | 51 | const queriesTs = source.find((s) => s.name === "queries.ts"); 52 | expect(queriesTs?.content).toMatchSnapshot(); 53 | 54 | const suspenseTs = source.find((s) => s.name === "suspense.ts"); 55 | expect(suspenseTs?.content).toMatchSnapshot(); 56 | 57 | const prefetchTs = source.find((s) => s.name === "prefetch.ts"); 58 | expect(prefetchTs?.content).toMatchSnapshot(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/generate.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from "node:fs"; 2 | import { rm } from "node:fs/promises"; 3 | import path from "node:path"; 4 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 5 | import type { LimitedUserConfig } from "../src/cli.mts"; 6 | import { generate } from "../src/generate.mjs"; 7 | 8 | const readOutput = (fileName: string) => { 9 | return readFileSync( 10 | path.join(__dirname, "outputs", "queries", fileName), 11 | "utf-8", 12 | ); 13 | }; 14 | 15 | describe("generate", () => { 16 | beforeAll(async () => { 17 | const options: LimitedUserConfig = { 18 | input: path.join(__dirname, "inputs", "petstore.yaml"), 19 | output: path.join("tests", "outputs"), 20 | client: "@hey-api/client-fetch", 21 | lint: "biome", 22 | format: "biome", 23 | pageParam: "page", 24 | nextPageParam: "meta.next", 25 | initialPageParam: "initial", 26 | operationId: true, 27 | }; 28 | await generate(options, "1.0.0"); 29 | }); 30 | 31 | afterAll(async () => { 32 | if (existsSync(path.join(__dirname, "outputs"))) { 33 | await rm(path.join(__dirname, "outputs"), { 34 | recursive: true, 35 | }); 36 | } 37 | }); 38 | 39 | test("common.ts", () => { 40 | expect(readOutput("common.ts")).toMatchSnapshot(); 41 | }); 42 | 43 | test("queries.ts", () => { 44 | expect(readOutput("queries.ts")).toMatchSnapshot(); 45 | }); 46 | 47 | test("infiniteQueries.ts", () => { 48 | expect(readOutput("infiniteQueries.ts")).toMatchSnapshot(); 49 | }); 50 | 51 | test("index.ts", () => { 52 | expect(readOutput("index.ts")).toMatchSnapshot(); 53 | }); 54 | 55 | test("suspense.ts", () => { 56 | expect(readOutput("suspense.ts")).toMatchSnapshot(); 57 | }); 58 | 59 | test("prefetch.ts", () => { 60 | expect(readOutput("prefetch.ts")).toMatchSnapshot(); 61 | }); 62 | 63 | test("ensureQueryData.ts", () => { 64 | expect(readOutput("ensureQueryData.ts")).toMatchSnapshot(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/inputs/no-models.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | required: false 30 | style: form 31 | schema: 32 | type: array 33 | items: 34 | type: string 35 | - name: limit 36 | in: query 37 | description: maximum number of results to return 38 | required: false 39 | schema: 40 | type: integer 41 | format: int32 42 | responses: 43 | '200': 44 | description: pet response 45 | default: 46 | description: unexpected error 47 | post: 48 | description: Creates a new pet in the store. Duplicates are allowed 49 | operationId: addPet 50 | requestBody: 51 | description: Pet to add to the store 52 | required: true 53 | content: 54 | application/json: 55 | responses: 56 | '200': 57 | description: pet response 58 | default: 59 | description: unexpected error 60 | /not-defined: 61 | get: 62 | deprecated: true 63 | description: This path is not fully defined. 64 | responses: 65 | default: 66 | description: unexpected error 67 | post: 68 | deprecated: true 69 | description: This path is not defined at all. 70 | responses: 71 | default: 72 | description: unexpected error 73 | /pets/{id}: 74 | get: 75 | description: Returns a user based on a single ID, if the user does not have access to the pet 76 | operationId: find pet by id 77 | parameters: 78 | - name: id 79 | in: path 80 | description: ID of pet to fetch 81 | required: true 82 | schema: 83 | type: integer 84 | format: int64 85 | responses: 86 | '200': 87 | description: pet response 88 | default: 89 | description: unexpected error 90 | delete: 91 | description: deletes a single pet based on the ID supplied 92 | operationId: deletePet 93 | parameters: 94 | - name: id 95 | in: path 96 | description: ID of pet to delete 97 | required: true 98 | schema: 99 | type: integer 100 | format: int64 101 | responses: 102 | '204': 103 | description: pet deleted 104 | default: 105 | description: unexpected error 106 | -------------------------------------------------------------------------------- /tests/inputs/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | required: false 30 | style: form 31 | schema: 32 | type: array 33 | items: 34 | type: string 35 | - name: limit 36 | in: query 37 | description: maximum number of results to return 38 | required: false 39 | schema: 40 | type: integer 41 | format: int32 42 | responses: 43 | '200': 44 | description: pet response 45 | content: 46 | application/json: 47 | schema: 48 | type: array 49 | items: 50 | $ref: '#/components/schemas/Pet' 51 | default: 52 | description: unexpected error 53 | content: 54 | application/json: 55 | schema: 56 | $ref: '#/components/schemas/Error' 57 | post: 58 | description: Creates a new pet in the store. Duplicates are allowed 59 | operationId: addPet 60 | requestBody: 61 | description: Pet to add to the store 62 | required: true 63 | content: 64 | application/json: 65 | schema: 66 | $ref: '#/components/schemas/NewPet' 67 | responses: 68 | '200': 69 | description: pet response 70 | content: 71 | application/json: 72 | schema: 73 | $ref: '#/components/schemas/Pet' 74 | default: 75 | description: unexpected error 76 | content: 77 | application/json: 78 | schema: 79 | $ref: '#/components/schemas/Error' 80 | /not-defined: 81 | get: 82 | deprecated: true 83 | description: This path is not fully defined. 84 | responses: 85 | default: 86 | description: unexpected error 87 | post: 88 | deprecated: true 89 | description: This path is not defined at all. 90 | responses: 91 | default: 92 | description: unexpected error 93 | /pets/{id}: 94 | get: 95 | description: Returns a user based on a single ID, if the user does not have access to the pet 96 | operationId: find pet by id 97 | parameters: 98 | - name: id 99 | in: path 100 | description: ID of pet to fetch 101 | required: true 102 | schema: 103 | type: integer 104 | format: int64 105 | responses: 106 | '200': 107 | description: pet response 108 | content: 109 | application/json: 110 | schema: 111 | $ref: '#/components/schemas/Pet' 112 | default: 113 | description: unexpected error 114 | content: 115 | application/json: 116 | schema: 117 | $ref: '#/components/schemas/Error' 118 | delete: 119 | description: deletes a single pet based on the ID supplied 120 | operationId: deletePet 121 | parameters: 122 | - name: id 123 | in: path 124 | description: ID of pet to delete 125 | required: true 126 | schema: 127 | type: integer 128 | format: int64 129 | responses: 130 | '204': 131 | description: pet deleted 132 | default: 133 | description: unexpected error 134 | content: 135 | application/json: 136 | schema: 137 | $ref: '#/components/schemas/Error' 138 | /paginated-pets: 139 | get: 140 | description: | 141 | Returns paginated pets from the system that the user has access to 142 | operationId: findPaginatedPets 143 | parameters: 144 | - name: page 145 | in: query 146 | description: page number 147 | required: false 148 | schema: 149 | type: integer 150 | format: int32 151 | - name: tags 152 | in: query 153 | description: tags to filter by 154 | required: false 155 | style: form 156 | schema: 157 | type: array 158 | items: 159 | type: string 160 | - name: limit 161 | in: query 162 | description: maximum number of results to return 163 | required: false 164 | schema: 165 | type: integer 166 | format: int32 167 | responses: 168 | '200': 169 | description: pet response 170 | content: 171 | application/json: 172 | schema: 173 | type: object 174 | properties: 175 | pets: 176 | type: array 177 | items: 178 | $ref: '#/components/schemas/Pet' 179 | nextPage: 180 | type: integer 181 | format: int32 182 | minimum: 1 183 | components: 184 | schemas: 185 | Pet: 186 | allOf: 187 | - $ref: '#/components/schemas/NewPet' 188 | - type: object 189 | required: 190 | - id 191 | properties: 192 | id: 193 | type: integer 194 | format: int64 195 | 196 | NewPet: 197 | type: object 198 | required: 199 | - name 200 | properties: 201 | name: 202 | type: string 203 | tag: 204 | type: string 205 | 206 | Error: 207 | type: object 208 | required: 209 | - code 210 | - message 211 | properties: 212 | code: 213 | type: integer 214 | format: int32 215 | message: 216 | type: string -------------------------------------------------------------------------------- /tests/print.test.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "node:fs/promises"; 2 | import { beforeEach, describe, expect, test, vi } from "vitest"; 3 | import * as common from "../src/common.mjs"; 4 | import { print } from "../src/print.mjs"; 5 | 6 | vi.mock("fs/promises", () => { 7 | return { 8 | mkdir: vi.fn(() => Promise.resolve()), 9 | writeFile: vi.fn(() => Promise.resolve()), 10 | }; 11 | }); 12 | 13 | describe("print", () => { 14 | beforeEach(() => { 15 | vi.resetAllMocks(); 16 | }); 17 | test("print - doesn't create folders if folders exist", async () => { 18 | const exists = vi.spyOn(common, "exists"); 19 | exists.mockImplementation(() => Promise.resolve(true)); 20 | const result = await print( 21 | [ 22 | { 23 | name: "test.ts", 24 | content: 'console.log("test")', 25 | }, 26 | ], 27 | { 28 | output: "dist", 29 | }, 30 | ); 31 | expect(exists).toBeCalledTimes(2); 32 | expect(result).toBeUndefined(); 33 | expect(mkdir).toBeCalledTimes(0); 34 | expect(writeFile).toBeCalledTimes(1); 35 | }); 36 | 37 | test("print - creates folders if folders don't exist", async () => { 38 | const exists = vi.spyOn(common, "exists"); 39 | exists.mockImplementation(() => Promise.resolve(false)); 40 | const result = await print( 41 | [ 42 | { 43 | name: "test.ts", 44 | content: 'console.log("test")', 45 | }, 46 | ], 47 | { 48 | output: "dist", 49 | }, 50 | ); 51 | expect(exists).toBeCalledTimes(2); 52 | expect(result).toBeUndefined(); 53 | expect(mkdir).toBeCalledTimes(2); 54 | expect(writeFile).toBeCalledTimes(1); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/service.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { Project } from "ts-morph"; 3 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 4 | import { getMethodsFromService, getServices } from "../src/service.mjs"; 5 | import { cleanOutputs, generateTSClients } from "./utils"; 6 | const fileName = "service"; 7 | describe(fileName, () => { 8 | beforeAll(async () => await generateTSClients(fileName)); 9 | afterAll(async () => await cleanOutputs(fileName)); 10 | 11 | test("getServices", async () => { 12 | const project = new Project({ 13 | skipAddingFilesFromTsConfig: true, 14 | }); 15 | project.addSourceFilesAtPaths( 16 | path.join("tests", `${fileName}-outputs`, "**", "*"), 17 | ); 18 | const service = await getServices(project); 19 | 20 | const methodNames = service.methods.map((m) => m.method.getName()); 21 | expect(methodNames).toEqual([ 22 | "findPets", 23 | "addPet", 24 | "getNotDefined", 25 | "postNotDefined", 26 | "findPetById", 27 | "deletePet", 28 | "findPaginatedPets", 29 | ]); 30 | }); 31 | 32 | test("getServices (No service node found)", async () => { 33 | const project = new Project({ 34 | skipAddingFilesFromTsConfig: true, 35 | }); 36 | project.addSourceFilesAtPaths("no/services/**/*"); 37 | await expect(() => getServices(project)).rejects.toThrowError( 38 | "No service node found", 39 | ); 40 | }); 41 | 42 | test('getMethodsFromService - throw error "Arrow function not found"', async () => { 43 | const source = ` 44 | const client = createClient(createConfig()) 45 | const foo = "bar" 46 | `; 47 | const project = new Project(); 48 | const sourceFile = project.createSourceFile("test.ts", source); 49 | 50 | await expect(() => getMethodsFromService(sourceFile)).toThrowError( 51 | "Arrow function not found", 52 | ); 53 | }); 54 | 55 | test('getMethodsFromService - throw error "Initializer not found"', async () => { 56 | const source = ` 57 | const client = createClient(createConfig()) 58 | const foo 59 | `; 60 | const project = new Project(); 61 | const sourceFile = project.createSourceFile("test.ts", source); 62 | 63 | await expect(() => getMethodsFromService(sourceFile)).toThrowError( 64 | "Initializer not found", 65 | ); 66 | }); 67 | 68 | test('getMethodsFromService - throw error "Return statement not found"', async () => { 69 | const source = ` 70 | const client = createClient(createConfig()) 71 | const foo = () => {} 72 | `; 73 | const project = new Project(); 74 | const sourceFile = project.createSourceFile("test.ts", source); 75 | 76 | await expect(() => getMethodsFromService(sourceFile)).toThrowError( 77 | "Return statement not found", 78 | ); 79 | }); 80 | 81 | test('getMethodsFromService - throw error "Call expression not found"', async () => { 82 | const source = ` 83 | const client = createClient(createConfig()) 84 | const foo = () => { return } 85 | `; 86 | const project = new Project(); 87 | const sourceFile = project.createSourceFile("test.ts", source); 88 | 89 | await expect(() => getMethodsFromService(sourceFile)).toThrowError( 90 | "Call expression not found", 91 | ); 92 | }); 93 | 94 | test('getMethodsFromService - throw error "Method block not found"', async () => { 95 | const source = ` 96 | const client = createClient(createConfig()) 97 | const foo = () => 98 | `; 99 | const project = new Project(); 100 | const sourceFile = project.createSourceFile("test.ts", source); 101 | 102 | await expect(() => getMethodsFromService(sourceFile)).toThrowError( 103 | "Method block not found", 104 | ); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "ts-morph"; 2 | import ts from "typescript"; 3 | import { describe, expect, test } from "vitest"; 4 | import { addJSDocToNode } from "../src/util.mts"; 5 | 6 | describe("utils", () => { 7 | test("addJSDocToNode - deprecated", () => { 8 | const project = new Project({ 9 | skipAddingFilesFromTsConfig: true, 10 | }); 11 | 12 | // create class 13 | const node = ts.factory.createClassDeclaration( 14 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 15 | "TestClass", 16 | undefined, 17 | undefined, 18 | [], 19 | ); 20 | // create file source 21 | const tsFile = ts.createSourceFile( 22 | "test.ts", 23 | "", 24 | ts.ScriptTarget.Latest, 25 | false, 26 | ts.ScriptKind.TS, 27 | ); 28 | 29 | // create source file 30 | const tsSource = ts.factory.createSourceFile( 31 | [node], 32 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 33 | ts.NodeFlags.None, 34 | ); 35 | 36 | // print source file 37 | const fileString = ts 38 | .createPrinter() 39 | .printNode(ts.EmitHint.Unspecified, tsSource, tsFile); 40 | 41 | // create ts-morph source file 42 | const sourceFile = project.createSourceFile("test.ts", fileString); 43 | 44 | if (!sourceFile) { 45 | throw new Error("Source file not found"); 46 | } 47 | 48 | // add jsdoc to node 49 | const jsDoc = `/** 50 | * @deprecated 51 | * This is a test 52 | * This is a test 2 53 | */`; 54 | 55 | const deprecated = true; 56 | 57 | // find class node 58 | const foundNode = sourceFile.getClasses()[0]; 59 | 60 | // add jsdoc to node 61 | const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc); 62 | 63 | // print node 64 | const nodetext = ts 65 | .createPrinter() 66 | .printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile); 67 | 68 | expect(nodetext).toMatchInlineSnapshot(` 69 | "/** 70 | * @deprecated 71 | * This is a test 72 | * This is a test 2 73 | */ 74 | export class TestClass { 75 | }" 76 | `); 77 | }); 78 | 79 | test("addJSDocToNode - not deprecated", () => { 80 | const project = new Project({ 81 | skipAddingFilesFromTsConfig: true, 82 | }); 83 | 84 | // create class 85 | const node = ts.factory.createClassDeclaration( 86 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 87 | "TestClass", 88 | undefined, 89 | undefined, 90 | [], 91 | ); 92 | // create file source 93 | const tsFile = ts.createSourceFile( 94 | "test.ts", 95 | "", 96 | ts.ScriptTarget.Latest, 97 | false, 98 | ts.ScriptKind.TS, 99 | ); 100 | 101 | // create source file 102 | const tsSource = ts.factory.createSourceFile( 103 | [node], 104 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 105 | ts.NodeFlags.None, 106 | ); 107 | 108 | // print source file 109 | const fileString = ts 110 | .createPrinter() 111 | .printNode(ts.EmitHint.Unspecified, tsSource, tsFile); 112 | 113 | // create ts-morph source file 114 | const sourceFile = project.createSourceFile("test.ts", fileString); 115 | 116 | if (!sourceFile) { 117 | throw new Error("Source file not found"); 118 | } 119 | 120 | // add jsdoc to node 121 | const jsDoc = `/** 122 | * This is a test 123 | * This is a test 2 124 | */`; 125 | 126 | // find class node 127 | const foundNode = sourceFile.getClasses()[0]; 128 | 129 | // add jsdoc to node 130 | const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc); 131 | 132 | // print node 133 | const nodetext = ts 134 | .createPrinter() 135 | .printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile); 136 | 137 | expect(nodetext).toMatchInlineSnapshot(` 138 | "/** 139 | * This is a test 140 | * This is a test 2 141 | */ 142 | export class TestClass { 143 | }" 144 | `); 145 | }); 146 | 147 | test("addJSDocToNode - does not add comment if no jsdoc", () => { 148 | const project = new Project({ 149 | skipAddingFilesFromTsConfig: true, 150 | }); 151 | 152 | // create class 153 | const node = ts.factory.createClassDeclaration( 154 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 155 | "TestClass", 156 | undefined, 157 | undefined, 158 | [], 159 | ); 160 | // create file source 161 | const tsFile = ts.createSourceFile( 162 | "test.ts", 163 | "", 164 | ts.ScriptTarget.Latest, 165 | false, 166 | ts.ScriptKind.TS, 167 | ); 168 | 169 | // create source file 170 | const tsSource = ts.factory.createSourceFile( 171 | [node], 172 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 173 | ts.NodeFlags.None, 174 | ); 175 | 176 | // print source file 177 | const fileString = ts 178 | .createPrinter() 179 | .printNode(ts.EmitHint.Unspecified, tsSource, tsFile); 180 | 181 | // create ts-morph source file 182 | const sourceFile = project.createSourceFile("test.ts", fileString); 183 | 184 | if (!sourceFile) { 185 | throw new Error("Source file not found"); 186 | } 187 | 188 | // add jsdoc to node 189 | const jsDoc = undefined; 190 | 191 | // find class node 192 | const foundNode = sourceFile.getClasses()[0]; 193 | 194 | // add jsdoc to node 195 | const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc); 196 | 197 | // print node 198 | const nodetext = ts 199 | .createPrinter() 200 | .printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile); 201 | 202 | expect(nodetext).toMatchInlineSnapshot(` 203 | "export class TestClass { 204 | }" 205 | `); 206 | }); 207 | 208 | test("addJSDocToNode - adds comment if no jsdoc and deprecated true", () => { 209 | const project = new Project({ 210 | skipAddingFilesFromTsConfig: true, 211 | }); 212 | 213 | // create class 214 | const node = ts.factory.createClassDeclaration( 215 | [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 216 | "TestClass", 217 | undefined, 218 | undefined, 219 | [], 220 | ); 221 | // create file source 222 | const tsFile = ts.createSourceFile( 223 | "test.ts", 224 | "", 225 | ts.ScriptTarget.Latest, 226 | false, 227 | ts.ScriptKind.TS, 228 | ); 229 | 230 | // create source file 231 | const tsSource = ts.factory.createSourceFile( 232 | [node], 233 | ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), 234 | ts.NodeFlags.None, 235 | ); 236 | 237 | // print source file 238 | const fileString = ts 239 | .createPrinter() 240 | .printNode(ts.EmitHint.Unspecified, tsSource, tsFile); 241 | 242 | // create ts-morph source file 243 | const sourceFile = project.createSourceFile("test.ts", fileString); 244 | 245 | if (!sourceFile) { 246 | throw new Error("Source file not found"); 247 | } 248 | 249 | // add jsdoc to node 250 | const jsDoc = undefined; 251 | 252 | // find class node 253 | const foundNode = sourceFile.getClasses()[0]; 254 | 255 | // add jsdoc to node 256 | const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc); 257 | 258 | // print node 259 | const nodetext = ts 260 | .createPrinter() 261 | .printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile); 262 | 263 | expect(nodetext).toMatchInlineSnapshot(` 264 | "export class TestClass { 265 | }" 266 | `); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { rm } from "node:fs/promises"; 3 | import path from "node:path"; 4 | import { type UserConfig, createClient } from "@hey-api/openapi-ts"; 5 | export const outputPath = (prefix: string) => 6 | path.join("tests", `${prefix}-outputs`); 7 | 8 | export const generateTSClients = async (prefix: string, inputFile?: string) => { 9 | const options: UserConfig = { 10 | input: path.join(__dirname, "inputs", inputFile ?? "petstore.yaml"), 11 | client: "@hey-api/client-fetch", 12 | output: outputPath(prefix), 13 | services: { 14 | asClass: false, 15 | }, 16 | }; 17 | await createClient(options); 18 | }; 19 | 20 | export const cleanOutputs = async (prefix: string) => { 21 | const output = `${prefix}-outputs`; 22 | if (existsSync(path.join(__dirname, output))) { 23 | await rm(path.join(__dirname, output), { recursive: true }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "allowSyntheticDefaultImports": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "resolveJsonModule": true, 11 | "outDir": "dist", 12 | "lib": ["ESNext", "DOM"], 13 | "target": "ESNext", 14 | "baseUrl": ".", 15 | "rootDir": "src" 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ["text", "json-summary", "json", "html"], 7 | exclude: ["src/cli.mts", "examples/**", "tests/**", "docs/**"], 8 | reportOnFailure: true, 9 | thresholds: { 10 | lines: 95, 11 | functions: 95, 12 | statements: 95, 13 | branches: 90, 14 | }, 15 | }, 16 | }, 17 | }); 18 | --------------------------------------------------------------------------------