├── .changeset
├── README.md
└── config.json
├── .github
├── publish.yml
└── workflows
│ ├── build-and-test.yaml
│ └── publish.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc
├── LICENSE
├── README.md
├── package.json
├── packages
├── typed-openapi
│ ├── CHANGELOG.md
│ ├── bin.js
│ ├── package.json
│ ├── src
│ │ ├── asserts.ts
│ │ ├── box-factory.ts
│ │ ├── box.ts
│ │ ├── cli.ts
│ │ ├── format.ts
│ │ ├── generate-client-files.ts
│ │ ├── generator.ts
│ │ ├── index.ts
│ │ ├── is-reference-object.ts
│ │ ├── map-openapi-endpoints.ts
│ │ ├── node.export.ts
│ │ ├── openapi-schema-to-ts.ts
│ │ ├── ref-resolver.ts
│ │ ├── string-utils.ts
│ │ ├── tanstack-query.generator.ts
│ │ ├── topological-sort.ts
│ │ ├── ts-factory.ts
│ │ └── types.ts
│ ├── tests
│ │ ├── generate-runtime.test.ts
│ │ ├── generator-basic-schemas.test.ts
│ │ ├── generator.test.ts
│ │ ├── map-openapi-endpoints.test.ts
│ │ ├── openapi-schema-to-ts.test.ts
│ │ ├── ref-resolver.test.ts
│ │ ├── samples
│ │ │ ├── docker.openapi.yaml
│ │ │ ├── long-operation-id.yaml
│ │ │ ├── parameters.yaml
│ │ │ ├── petstore.yaml
│ │ │ └── springboot.actuator.yaml
│ │ ├── snapshots
│ │ │ ├── docker.openapi.client.ts
│ │ │ ├── docker.openapi.io-ts.ts
│ │ │ ├── docker.openapi.typebox.ts
│ │ │ ├── docker.openapi.valibot.ts
│ │ │ ├── docker.openapi.yup.ts
│ │ │ ├── docker.openapi.zod.ts
│ │ │ ├── long-operation-id.arktype.ts
│ │ │ ├── long-operation-id.client.ts
│ │ │ ├── long-operation-id.io-ts.ts
│ │ │ ├── long-operation-id.typebox.ts
│ │ │ ├── long-operation-id.valibot.ts
│ │ │ ├── long-operation-id.yup.ts
│ │ │ ├── long-operation-id.zod.ts
│ │ │ ├── package.json
│ │ │ ├── petstore.arktype.ts
│ │ │ ├── petstore.client.ts
│ │ │ ├── petstore.io-ts.ts
│ │ │ ├── petstore.typebox.ts
│ │ │ ├── petstore.valibot.ts
│ │ │ ├── petstore.yup.ts
│ │ │ ├── petstore.zod.ts
│ │ │ └── pnpm-lock.yaml
│ │ └── tanstack-query.generator.test.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── vitest.config.ts
└── web
│ ├── declarations
│ ├── arktype.d.ts
│ ├── io-ts.d.ts
│ ├── typebox.d.ts
│ ├── valibot.d.ts
│ ├── yup.d.ts
│ └── zod.d.ts
│ ├── fs.shim.ts
│ ├── get-ts-declarations.ts
│ ├── index.html
│ ├── module.shim.ts
│ ├── package.json
│ ├── panda.config.ts
│ ├── postcss.config.cjs
│ ├── public
│ ├── favicon.ico
│ └── github-icon.svg
│ ├── react.d.ts
│ ├── src
│ ├── Playground
│ │ ├── Playground.machine.ts
│ │ ├── Playground.machine.typegen.ts
│ │ ├── Playground.tsx
│ │ ├── PlaygroundMachineProvider.ts
│ │ ├── PlaygroundWithMachine.tsx
│ │ ├── ResizeHandle.tsx
│ │ ├── format.ts
│ │ ├── petstore.yaml
│ │ └── url-saver.ts
│ ├── components
│ │ ├── button.tsx
│ │ ├── color-mode-switch.tsx
│ │ ├── github-icon.tsx
│ │ ├── icon-button.tsx
│ │ ├── select-demo.tsx
│ │ ├── select.tsx
│ │ └── twitter-icon.tsx
│ ├── main.tsx
│ ├── pages
│ │ └── Home.tsx
│ ├── run-if-fn.ts
│ ├── styles.css
│ └── vite-themes
│ │ ├── provider.tsx
│ │ └── vite-themes-types.ts
│ ├── theme
│ ├── preset.ts
│ ├── semantic-tokens.ts
│ ├── text-styles.ts
│ └── tokens.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "restricted",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": ["typed-openapi-web"]
11 | }
12 |
--------------------------------------------------------------------------------
/.github/publish.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v3
17 |
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version-file: ".nvmrc"
22 |
23 | - name: Setup pnpm
24 | uses: pnpm/action-setup@v4
25 |
26 | - name: Install dependencies
27 | run: pnpm install --frozen-lockfile
28 |
29 | - name: Build
30 | run: pnpm build
31 |
32 | - name: Test
33 | run: pnpm test
34 |
35 | - name: Create Release Pull Request or Publish to npm
36 | id: changesets
37 | uses: changesets/action@v1
38 | with:
39 | publish: pnpm release
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-test.yaml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build-and-test:
7 | name: Build and Test
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Setup pnpm
13 | uses: pnpm/action-setup@v4
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version-file: ".nvmrc"
19 |
20 | - name: Install dependencies
21 | run: pnpm install --frozen-lockfile
22 |
23 | - name: Build
24 | run: pnpm build
25 |
26 | - name: Test
27 | run: pnpm test
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v3
17 |
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version-file: ".nvmrc"
22 |
23 | - name: Setup pnpm
24 | uses: pnpm/action-setup@v4
25 |
26 | - name: Install dependencies
27 | run: pnpm install --frozen-lockfile
28 |
29 | - name: Build
30 | run: pnpm build
31 |
32 | - name: Test
33 | run: pnpm test
34 |
35 | - name: Create Release Pull Request or Publish to npm
36 | id: changesets
37 | uses: changesets/action@v1
38 | with:
39 | publish: pnpm release
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |
--------------------------------------------------------------------------------
/.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 |
23 | ## Panda
24 | styled-system
25 | styled-system-static
26 | .vercel
27 | *.tsbuildinfo
28 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 | prefer-offline=true
3 | strict-peer-dependencies=true
4 | resolve-peers-from-workspace-root=true
5 | enable-pre-post-scripts=true
6 | auto-install-peers=true
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v22.11.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "printWidth": 120,
4 | "bracketSpacing": true,
5 | "jsxSingleQuote": false,
6 | "proseWrap": "always",
7 | "semi": true,
8 | "tabWidth": 2,
9 | "trailingComma": "all"
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2025 Alexandre Stahmer
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # typed-openapi
2 |
3 | Generate a Typescript API client from an OpenAPI spec
4 |
5 | See [the online playground](https://typed-openapi-astahmer.vercel.app/)
6 |
7 | 
8 |
9 | ## Features
10 |
11 | - Headless API client, bring your own fetcher ! (fetch, axios, ky, etc...)
12 | - Generates a fully typesafe API client with just types by default (instant suggestions)
13 | - Or you can also generate a client with runtime validation using one of the following runtimes:
14 | - [zod](https://zod.dev/)
15 | - [typebox](https://github.com/sinclairzx81/typebox)
16 | - [arktype](https://arktype.io/)
17 | - [valibot](https://valibot.dev/)
18 | - [io-ts](https://gcanti.github.io/io-ts/)
19 | - [yup](https://github.com/jquense/yup)
20 |
21 | The generated client is a single file that can be used in the browser or in node. Runtime validation schemas are
22 | provided by the excellent [typebox-codegen](https://github.com/sinclairzx81/typebox-codegen)
23 |
24 | ## Install & usage
25 |
26 | ```sh
27 | pnpm add typed-openapi
28 | ```
29 |
30 | It exports a bunch of functions that can be used to build your own tooling on top of it. You can look at the
31 | [CLI code](packages/typed-openapi/src/cli.ts) so see how to use them.
32 |
33 | ## CLI
34 |
35 | ```sh
36 | npx typed-openapi -h
37 | ```
38 |
39 | ```sh
40 | typed-openapi/0.1.3
41 |
42 | Usage: $ typed-openapi
43 |
44 | Commands: Generate
45 |
46 | For more info, run any command with the `--help` flag: $ typed-openapi --help
47 |
48 | Options: -o, --output Output path for the api client ts file (defaults to `..ts`) -r, --runtime
49 | Runtime to use for validation; defaults to `none`; available: 'none' | 'arktype' | 'io-ts' | 'typebox' |
50 | 'valibot' | 'yup' | 'zod' (default: none) -h, --help Display this message -v, --version Display version number
51 | ```
52 |
53 | ## Non-goals
54 |
55 | - Caring too much about the runtime validation code. If that works (thanks to
56 | [typebox-codegen](https://github.com/sinclairzx81/typebox-codegen)), that's great, otherwise I'm not really interested
57 | in fixing it. If you are, feel free to open a PR.
58 |
59 | - Supporting all the OpenAPI spec. Regex, dates, files, whatever, that's not the point here.
60 | [openapi-zod-client](https://github.com/astahmer/openapi-zod-client) does a great job at that, but it's slow to
61 | generate the client and the suggestions in the IDE are not instant. I'm only interested in supporting the subset of
62 | the spec that makes the API client typesafe and fast to provide suggetions in the IDE.
63 |
64 | - Splitting the generated client into multiple files. Nope. Been there, done that. Let's keep it simple.
65 |
66 | Basically, let's focus on having a fast and typesafe API client generation instead.
67 |
68 | ## Alternatives
69 |
70 | [openapi-zod-client](https://github.com/astahmer/openapi-zod-client), which generates a
71 | [zodios](https://github.com/ecyrbe/zodios) client but can be slow to provide IDE suggestions when the OpenAPI spec is
72 | large. Also, you might not always want to use zod or even runtime validation, hence this project.
73 |
74 | ## Contributing
75 |
76 | - `pnpm i`
77 | - `pnpm build`
78 | - `pnpm test`
79 |
80 | When you're done with your changes, please run `pnpm changeset` in the root of the repo and follow the instructions
81 | described [here](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md).
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typed-openapi",
3 | "private": true,
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/astahmer/typed-openapi.git"
7 | },
8 | "license": "MIT",
9 | "scripts": {
10 | "build": "cd packages/typed-openapi && pnpm build",
11 | "build:all": "pnpm -r run build",
12 | "release": "changeset publish",
13 | "release-dev": "changeset version --snapshot dev && changeset publish --tag dev",
14 | "test": "cd packages/typed-openapi && pnpm run test"
15 | },
16 | "devDependencies": {
17 | "@changesets/cli": "^2.29.4"
18 | },
19 | "packageManager": "pnpm@9.6.0+sha256.dae0f7e822c56b20979bb5965e3b73b8bdabb6b8b8ef121da6d857508599ca35"
20 | }
21 |
--------------------------------------------------------------------------------
/packages/typed-openapi/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # typed-openapi
2 |
3 | ## 1.4.3
4 |
5 | ### Patch Changes
6 |
7 | - fb0fe07: closes: #79 by handling singleton enum of type number
8 |
9 | ## 1.4.2
10 |
11 | ### Patch Changes
12 |
13 | - 9f70b13: Ensure dir is created before generating files
14 |
15 | ## 1.4.1
16 |
17 | ### Patch Changes
18 |
19 | - f367a04: Treat boolean values as literal in enum
20 |
21 | ## 1.4.0
22 |
23 | ### Minor Changes
24 |
25 | - dad912c: feat: add build-and-test github workflow
26 | - 0440b2b: add `?:` to get optional parameters instead of having to set those to undefined
27 | - a718a33: Add CLI option `--schemas-only` to allow generation of only the schema without endpoints and api client
28 |
29 | ## 1.3.2
30 |
31 | ### Patch Changes
32 |
33 | - ceb15f6: Export generateClientFiles fn (same used as in the CLI)
34 |
35 | ## 1.3.1
36 |
37 | ### Patch Changes
38 |
39 | - 86a384f: add mutation selectFn + endpoint type-only property in .mutation
40 |
41 | ## 1.3.0
42 |
43 | ### Minor Changes
44 |
45 | - 91b005f: add parenthesis to handle priority between union/intersection
46 |
47 | this fixes an issue where `(A | B | C) & D` would be ambiguous and could be interpreted as `A | B | (C & D`
48 |
49 | ## 1.2.0
50 |
51 | ### Minor Changes
52 |
53 | - ed15081: Rename .options to .queryOptions
54 |
55 | ## 1.1.2
56 |
57 | ### Patch Changes
58 |
59 | - 4846bc4: fix mutationOptions parameters typings
60 |
61 | ## 1.1.1
62 |
63 | ### Patch Changes
64 |
65 | - 73c1ef1: feat: mutationOptions + .mutation (if input is not available before)
66 |
67 | ## 1.1.0
68 |
69 | ### Minor Changes
70 |
71 | - f029e94: Fetcher is now expected to return a Response, so that the api client can have a .request method that returns
72 | the raw object
73 |
74 | all methods (get post etc) will be parsed using the overridable "parseResponse" api client fn property
75 |
76 | - c1b9dcb: fix: anyOf to ts
77 |
78 | https://github.com/astahmer/typed-openapi/issues/31
79 |
80 | ### Patch Changes
81 |
82 | - d7eda3d: rm AllEndpoints type
83 | - 2abc8b4: chore: export Fetcher type
84 | - 6dfbd19: fix: tanstack client output path
85 | - f66571d: chore: make "endpoint" a type-only property
86 | - 93bd157: better endpoint alias
87 | - da6af35: fix: unused QueryClient import
88 |
89 | ## 1.0.1
90 |
91 | ### Patch Changes
92 |
93 | - 4a909eb: Fix CLI & package.json by removing CJS usage
94 |
95 | ## 1.0.0
96 |
97 | ### Major Changes
98 |
99 | - 8ec5d0b: bump all deps
100 |
101 | ### Minor Changes
102 |
103 | - 8ec5d0b: Add @tanstack/react-query generated client
104 | - 8ec5d0b: Fix `Schemas.null` references in TS output
105 | - 8ec5d0b: Better output when using `schema.additionalProperties`, especially when specifying
106 | `additionalProperties.type`
107 |
108 | ## 0.10.1
109 |
110 | ### Patch Changes
111 |
112 | - dd91027: Move changesets to devDeps
113 |
114 | ## 0.10.0
115 |
116 | ### Minor Changes
117 |
118 | - be0ba5f: Bump @sinclair/typebox-codegen version
119 |
120 | ### Patch Changes
121 |
122 | - 739e5b5: Add options to `Method` type in `generateApiClient` function as fix for
123 | [#55](https://github.com/astahmer/typed-openapi/issues/55)
124 |
125 | ## 0.9.0
126 |
127 | ### Minor Changes
128 |
129 | - b122616: Add requestFormat property to endpoint schema.
130 |
131 | - json
132 | - form-data
133 | - form-url
134 | - binary
135 | - text
136 |
137 | ## 0.8.0
138 |
139 | ### Minor Changes
140 |
141 | - d260cd4: Fix zod and yup runtime generated endpoint schema type errors due to long operationId
142 |
143 | ## 0.7.0
144 |
145 | ### Minor Changes
146 |
147 | - cf83e52: Add type cast in ApiClient methods to match the desired type
148 |
149 | ## 0.6.0
150 |
151 | ### Minor Changes
152 |
153 | - c5daa58: Upgraded codegen dependency to provide newer runtime validator output
154 |
155 | This is a BREAKING CHANGE for valibot/yup users
156 |
157 | ## 0.5.0
158 |
159 | ### Minor Changes
160 |
161 | - f0886a0: Thanks to @0237h:
162 |
163 | Allow for finer marking of optional parameters Current behavior allows only for marking _all_ parameters as optional,
164 | or none.
165 |
166 | This change checks first if all parameters are optional, keeping the old behavior if that's the case, otherwise
167 | iterates through the parameters to mark only those that **should** be optional from the OpenAPI spec.
168 |
169 | ## 0.4.1
170 |
171 | ### Patch Changes
172 |
173 | - 4fac0aa: Fix typecast in zod-based ApiClient methods
174 |
175 | ## 0.4.0
176 |
177 | ### Minor Changes
178 |
179 | - ffcdaa7: zod-runtime: add typecast in ApiClient methods to match the desired type
180 |
181 | ## 0.3.0
182 |
183 | ### Minor Changes
184 |
185 | - b9b4772: Fix default response behavior (only use "default" as a fallback)
186 | - 23f3dc3: Support path parameters
187 |
188 | ### Patch Changes
189 |
190 | - bb937d4: fix: refer Schema namespace in generated body type
191 |
192 | ## 0.2.0
193 |
194 | ### Minor Changes
195 |
196 | - 00eb659: Fixed parameter.body on post endpoints - #8.
197 |
198 | ## 0.1.5
199 |
200 | ### Patch Changes
201 |
202 | - 7f0ecd4: fix: query/path/headers parameters are all marked as required if one of them is required
203 |
204 | ## 0.1.4
205 |
206 | ### Patch Changes
207 |
208 | - ae34ed1: support OpenAPI v3.0 schema.nullable
209 |
210 | ```json
211 | {
212 | "type": "object",
213 | "properties": {
214 | "id": { "type": "integer" },
215 | "parent_id": {
216 | "type": "integer",
217 | "nullable": true
218 | },
219 | "children": {
220 | "type": "array",
221 | "items": {
222 | "$ref": "#/components/schemas/TestClass"
223 | }
224 | }
225 | },
226 | "required": ["id", "parent_id"]
227 | }
228 | ```
229 |
230 | output:
231 |
232 | ```diff
233 | export type TestClass = {
234 | id: number;
235 | - parent_id: number;
236 | + parent_id: number | null;
237 | children?: Array | undefined
238 | };
239 | ```
240 |
241 | - 088f3e4: Fix optional types
242 |
243 | ```json
244 | {
245 | "type": "object",
246 | "properties": { "str": { "type": "string" }, "nb": { "type": "number" } },
247 | "required": ["str"]
248 | }
249 | ```
250 |
251 | output:
252 |
253 | ```diff
254 | export type _Test = {
255 | str: string;
256 | - "nb?": number | undefined
257 | + nb?: number | undefined
258 | };
259 | ```
260 |
261 | ## 0.1.3
262 |
263 | ### Patch Changes
264 |
265 | - 8568d69: Not a CLI anymore ! Exposed functions & types to be used when installed from npm
266 |
267 | ## 0.1.2
268 |
269 | ### Patch Changes
270 |
271 | - 0947ac5: - replace dprint by prettier 2.X (cause v3 needs async and dprint has trouble with finding the wasm module)
272 | - only wrap in TS namespaces when NOT using a runtime (= generating TS types only)
273 |
274 | ## 0.1.1
275 |
276 | ### Patch Changes
277 |
278 | - 95e8477: init
279 |
--------------------------------------------------------------------------------
/packages/typed-openapi/bin.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import "./dist/cli.js";
4 |
--------------------------------------------------------------------------------
/packages/typed-openapi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typed-openapi",
3 | "type": "module",
4 | "version": "1.4.3",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "exports": {
8 | ".": "./dist/index.js",
9 | "./node": "./dist/node.export.js"
10 | },
11 | "bin": {
12 | "typed-openapi": "bin.js"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/astahmer/typed-openapi.git",
17 | "directory": "packages/typed-openapi"
18 | },
19 | "scripts": {
20 | "start": "node ./dist/cli.js",
21 | "dev": "tsup --watch",
22 | "build": "tsup",
23 | "test": "vitest",
24 | "fmt": "prettier --write src",
25 | "typecheck": "tsc -b ./tsconfig.build.json"
26 | },
27 | "dependencies": {
28 | "@apidevtools/swagger-parser": "^10.1.1",
29 | "@sinclair/typebox-codegen": "^0.11.1",
30 | "arktype": "2.1.20",
31 | "cac": "^6.7.14",
32 | "openapi3-ts": "^4.4.0",
33 | "pastable": "^2.2.1",
34 | "pathe": "^2.0.3",
35 | "prettier": "3.5.3",
36 | "ts-pattern": "^5.7.0"
37 | },
38 | "devDependencies": {
39 | "@changesets/cli": "^2.29.4",
40 | "@types/node": "^22.15.17",
41 | "@types/prettier": "3.0.0",
42 | "tsup": "^8.4.0",
43 | "typescript": "^5.8.3",
44 | "vitest": "^3.1.3"
45 | },
46 | "files": [
47 | "src",
48 | "dist",
49 | "cli",
50 | "README.md"
51 | ],
52 | "keywords": [
53 | "typescript",
54 | "openapi",
55 | "generator",
56 | "runtime",
57 | "typesafe",
58 | "zod",
59 | "arktype",
60 | "typebox",
61 | "valibot",
62 | "yup",
63 | "io-ts"
64 | ],
65 | "sideEffects": false,
66 | "publishConfig": {
67 | "access": "public"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/asserts.ts:
--------------------------------------------------------------------------------
1 | import type { LibSchemaObject } from "./types.ts";
2 |
3 | export type SingleType = Exclude;
4 | export const isPrimitiveType = (type: unknown): type is PrimitiveType => primitiveTypeList.includes(type as any);
5 |
6 | const primitiveTypeList = ["string", "number", "integer", "boolean", "null"] as const;
7 | export type PrimitiveType = (typeof primitiveTypeList)[number];
8 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/box-factory.ts:
--------------------------------------------------------------------------------
1 | import type { ReferenceObject } from "openapi3-ts/oas31";
2 | import { Box } from "./box.ts";
3 | import { AnyBoxDef, BoxFactory, OpenapiSchemaConvertContext, StringOrBox, type LibSchemaObject } from "./types.ts";
4 |
5 | export const unwrap = (param: StringOrBox) => (typeof param === "string" ? param : param.value);
6 | export const createFactory = (f: T) => f;
7 |
8 | /**
9 | * Create a box-factory using your schema provider and automatically add the input schema to each box.
10 | */
11 | export const createBoxFactory = (schema: LibSchemaObject | ReferenceObject, ctx: OpenapiSchemaConvertContext) => {
12 | const f = typeof ctx.factory === "function" ? ctx.factory(schema, ctx) : ctx.factory;
13 | const callback = (box: Box) => {
14 | if (f.callback) {
15 | box = f.callback(box) as Box;
16 | }
17 |
18 | if (ctx?.onBox) {
19 | box = ctx.onBox?.(box) as Box;
20 | }
21 |
22 | return box;
23 | };
24 |
25 | const box: BoxFactory = {
26 | union: (types) => callback(new Box({ ctx, schema, type: "union", params: { types }, value: f.union(types) })),
27 | intersection: (types) =>
28 | callback(new Box({ ctx, schema, type: "intersection", params: { types }, value: f.intersection(types) })),
29 | array: (type) => callback(new Box({ ctx, schema, type: "array", params: { type }, value: f.array(type) })),
30 | optional: (type) => callback(new Box({ ctx, schema, type: "optional", params: { type }, value: f.optional(type) })),
31 | reference: (name, generics) =>
32 | callback(
33 | new Box({
34 | ctx,
35 | schema,
36 | type: "ref",
37 | params: generics ? { name, generics } : { name },
38 | value: f.reference(name, generics),
39 | }),
40 | ),
41 | literal: (value) => callback(new Box({ ctx, schema, type: "literal", params: {}, value: f.literal(value) })),
42 | string: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "string" }, value: f.string() })),
43 | number: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "number" }, value: f.number() })),
44 | boolean: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "boolean" }, value: f.boolean() })),
45 | unknown: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "unknown" }, value: f.unknown() })),
46 | any: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "any" }, value: f.any() })),
47 | never: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "never" }, value: f.never() })),
48 | object: (props) => callback(new Box({ ctx, schema, type: "object", params: { props }, value: f.object(props) })),
49 | };
50 |
51 | return box;
52 | };
53 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/box.ts:
--------------------------------------------------------------------------------
1 | import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
2 | import {
3 | AnyBoxDef,
4 | BoxArray,
5 | BoxIntersection,
6 | BoxKeyword,
7 | BoxLiteral,
8 | BoxObject,
9 | BoxOptional,
10 | BoxRef,
11 | BoxUnion,
12 | OpenapiSchemaConvertContext,
13 | type LibSchemaObject,
14 | } from "./types.ts";
15 |
16 | // TODO rename SchemaBox
17 | export class Box {
18 | type: T["type"];
19 | value: T["value"];
20 | params: T["params"];
21 | schema: T["schema"];
22 | ctx: T["ctx"];
23 |
24 | constructor(public definition: T) {
25 | this.definition = definition;
26 | this.type = definition.type;
27 | this.value = definition.value;
28 | this.params = definition.params;
29 | this.schema = definition.schema;
30 | this.ctx = definition.ctx;
31 | }
32 |
33 | toJSON() {
34 | return { type: this.type, value: this.value };
35 | }
36 |
37 | toString() {
38 | return JSON.stringify(this.toJSON(), null, 2);
39 | }
40 |
41 | recompute(callback: OpenapiSchemaConvertContext["onBox"]) {
42 | return openApiSchemaToTs({ schema: this.schema as LibSchemaObject, ctx: { ...this.ctx, onBox: callback! } });
43 | }
44 |
45 | static fromJSON(json: string) {
46 | return new Box(JSON.parse(json));
47 | }
48 |
49 | static isBox(box: unknown): box is Box {
50 | return box instanceof Box;
51 | }
52 |
53 | static isUnion(box: Box): box is Box {
54 | return box.type === "union";
55 | }
56 |
57 | static isIntersection(box: Box): box is Box {
58 | return box.type === "intersection";
59 | }
60 |
61 | static isArray(box: Box): box is Box {
62 | return box.type === "array";
63 | }
64 |
65 | static isOptional(box: Box): box is Box {
66 | return box.type === "optional";
67 | }
68 |
69 | static isReference(box: Box): box is Box {
70 | return box.type === "ref";
71 | }
72 |
73 | static isKeyword(box: Box): box is Box {
74 | return box.type === "keyword";
75 | }
76 |
77 | static isObject(box: Box): box is Box {
78 | return box.type === "object";
79 | }
80 |
81 | static isLiteral(box: Box): box is Box {
82 | return box.type === "literal";
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/cli.ts:
--------------------------------------------------------------------------------
1 | import { cac } from "cac";
2 |
3 | import { readFileSync } from "fs";
4 | import { generateClientFiles } from "./generate-client-files.ts";
5 | import { allowedRuntimes } from "./generator.ts";
6 |
7 | const { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
8 | const cli = cac(name);
9 |
10 | cli
11 | .command("", "Generate")
12 | .option("-o, --output ", "Output path for the api client ts file (defaults to `..ts`)")
13 | .option(
14 | "-r, --runtime ",
15 | `Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
16 | { default: "none" },
17 | )
18 | .option(
19 | "--schemas-only",
20 | "Only generate schemas, skipping client generation (defaults to false)",
21 | { default: false },
22 | )
23 | .option(
24 | "--tanstack [name]",
25 | "Generate tanstack client, defaults to false, can optionally specify a name for the generated file",
26 | )
27 | .action(async (input, _options) => {
28 | return generateClientFiles(input, _options);
29 | });
30 |
31 | cli.help();
32 | cli.version(version);
33 | cli.parse();
34 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/format.ts:
--------------------------------------------------------------------------------
1 | import prettier, { type Options } from "prettier";
2 | import parserTypescript from "prettier/parser-typescript";
3 |
4 | /** @see https://github.dev/stephenh/ts-poet/blob/5ea0dbb3c9f1f4b0ee51a54abb2d758102eda4a2/src/Code.ts#L231 */
5 | function maybePretty(input: string, options?: Options | null) {
6 | try {
7 | return prettier.format(input, {
8 | parser: "typescript",
9 | plugins: [parserTypescript],
10 | ...options,
11 | });
12 | } catch (err) {
13 | console.warn("Failed to format code");
14 | console.warn(err);
15 | return input; // assume it's invalid syntax and ignore
16 | }
17 | }
18 |
19 | export const prettify = (str: string, options?: Options | null) =>
20 | maybePretty(str, { printWidth: 120, trailingComma: "all", ...options });
21 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/generate-client-files.ts:
--------------------------------------------------------------------------------
1 | import SwaggerParser from "@apidevtools/swagger-parser";
2 | import type { OpenAPIObject } from "openapi3-ts/oas31";
3 | import { basename, join, dirname } from "pathe";
4 | import { type } from "arktype";
5 | import { mkdir, writeFile } from "fs/promises";
6 | import { allowedRuntimes, generateFile } from "./generator.ts";
7 | import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
8 | import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
9 | import { prettify } from "./format.ts";
10 |
11 | const cwd = process.cwd();
12 | const now = new Date();
13 |
14 | async function ensureDir(dirPath: string): Promise {
15 | try {
16 | await mkdir(dirPath, { recursive: true });
17 | } catch (error) {
18 | console.error(`Error ensuring directory: ${(error as Error).message}`);
19 | }
20 | }
21 |
22 | export const optionsSchema = type({
23 | "output?": "string",
24 | runtime: allowedRuntimes,
25 | tanstack: "boolean | string",
26 | schemasOnly: "boolean",
27 | });
28 |
29 | export async function generateClientFiles(input: string, options: typeof optionsSchema.infer) {
30 | const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
31 |
32 | const ctx = mapOpenApiEndpoints(openApiDoc);
33 | console.log(`Found ${ctx.endpointList.length} endpoints`);
34 |
35 | const content = await prettify(generateFile({
36 | ...ctx,
37 | runtime: options.runtime,
38 | schemasOnly: options.schemasOnly,
39 | }));
40 | const outputPath = join(
41 | cwd,
42 | options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
43 | );
44 |
45 | console.log("Generating client...", outputPath);
46 | await ensureDir(dirname(outputPath));
47 | await writeFile(outputPath, content);
48 |
49 | if (options.tanstack) {
50 | const tanstackContent = await generateTanstackQueryFile({
51 | ...ctx,
52 | relativeApiClientPath: './' + basename(outputPath),
53 | });
54 | const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
55 | console.log("Generating tanstack client...", tanstackOutputPath);
56 | await ensureDir(dirname(tanstackOutputPath));
57 | await writeFile(tanstackOutputPath, tanstackContent);
58 | }
59 |
60 | console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
61 | }
62 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./box-factory.ts";
2 | export { generateFile, type OutputRuntime } from "./generator.ts";
3 | export * from "./tanstack-query.generator.ts";
4 | export * from "./map-openapi-endpoints.ts";
5 | export * from "./openapi-schema-to-ts.ts";
6 | export * from "./ref-resolver.ts";
7 | export * from "./ts-factory.ts";
8 | export * from "./types.ts";
9 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/is-reference-object.ts:
--------------------------------------------------------------------------------
1 | // taken from
2 | // https://github.dev/metadevpro/openapi3-ts/blob/a62ff445207af599f591532ef776e671c456cc37/src/model/OpenApi.ts#L261-L269
3 | // to avoid the runtime dependency on `openapi3-ts`
4 | // which itself depends on `yaml` import (which use CJS `require` and thus can't be imported in a ESM module)
5 |
6 | import type { ReferenceObject } from "openapi3-ts/oas31";
7 |
8 | /**
9 | * A type guard to check if the given value is a `ReferenceObject`.
10 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types
11 | *
12 | * @param obj The value to check.
13 | */
14 | export function isReferenceObject(obj: any): obj is ReferenceObject {
15 | return obj != null && Object.prototype.hasOwnProperty.call(obj, "$ref");
16 | }
17 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/map-openapi-endpoints.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIObject, ResponseObject } from "openapi3-ts/oas31";
2 | import { OperationObject, ParameterObject } from "openapi3-ts/oas31";
3 | import { capitalize, pick } from "pastable/server";
4 | import { Box } from "./box.ts";
5 | import { createBoxFactory } from "./box-factory.ts";
6 | import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
7 | import { createRefResolver } from "./ref-resolver.ts";
8 | import { tsFactory } from "./ts-factory.ts";
9 | import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types.ts";
10 | import { pathToVariableName } from "./string-utils.ts";
11 | import { match, P } from "ts-pattern";
12 |
13 | const factory = tsFactory;
14 |
15 | export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
16 | const refs = createRefResolver(doc, factory);
17 | const ctx: OpenapiSchemaConvertContext = { refs, factory };
18 | const endpointList = [] as Array;
19 |
20 | Object.entries(doc.paths ?? {}).forEach(([path, pathItemObj]) => {
21 | const pathItem = pick(pathItemObj, ["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
22 | Object.entries(pathItem).forEach(([method, operation]) => {
23 | if (operation.deprecated) return;
24 |
25 | const endpoint = {
26 | operation,
27 | method: method as Method,
28 | path,
29 | requestFormat: "json",
30 | response: openApiSchemaToTs({ schema: {}, ctx }),
31 | meta: {
32 | alias: getAlias({ path, method, operation } as Endpoint),
33 | areParametersRequired: false,
34 | hasParameters: false,
35 | },
36 | } as Endpoint;
37 |
38 | // Build a list of parameters by type + fill an object with all of them
39 | const lists = { query: [] as ParameterObject[], path: [] as ParameterObject[], header: [] as ParameterObject[] };
40 | const paramObjects = [...(pathItemObj.parameters ?? []), ...(operation.parameters ?? [])].reduce(
41 | (acc, paramOrRef) => {
42 | const param = refs.unwrap(paramOrRef);
43 | const schema = openApiSchemaToTs({ schema: refs.unwrap(param.schema ?? {}), ctx });
44 |
45 | if (param.required) endpoint.meta.areParametersRequired = true;
46 | endpoint.meta.hasParameters = true;
47 |
48 | if (param.in === "query") {
49 | lists.query.push(param);
50 | acc.query[param.name] = schema;
51 | }
52 | if (param.in === "path") {
53 | lists.path.push(param);
54 | acc.path[param.name] = schema;
55 | }
56 | if (param.in === "header") {
57 | lists.header.push(param);
58 | acc.header[param.name] = schema;
59 | }
60 |
61 | return acc;
62 | },
63 | { query: {} as Record, path: {} as Record, header: {} as Record },
64 | );
65 |
66 | // Filter out empty objects
67 | const params = Object.entries(paramObjects).reduce(
68 | (acc, [key, value]) => {
69 | if (Object.keys(value).length) {
70 | // @ts-expect-error
71 | acc[key] = value;
72 | }
73 | return acc;
74 | },
75 | {} as { query?: Record; path?: Record; header?: Record; body?: Box },
76 | );
77 |
78 | // Body
79 | if (operation.requestBody) {
80 | endpoint.meta.hasParameters = true;
81 | const requestBody = refs.unwrap(operation.requestBody ?? {});
82 | const content = requestBody.content;
83 | const matchingMediaType = Object.keys(content).find(isAllowedParamMediaTypes);
84 |
85 | if (matchingMediaType && content[matchingMediaType]) {
86 | params.body = openApiSchemaToTs({
87 | schema: content[matchingMediaType]?.schema ?? {} ?? {},
88 | ctx,
89 | });
90 | }
91 |
92 | endpoint.requestFormat = match(matchingMediaType)
93 | .with("application/octet-stream", () => "binary" as const)
94 | .with("multipart/form-data", () => "form-data" as const)
95 | .with("application/x-www-form-urlencoded", () => "form-url" as const)
96 | .with(P.string.includes("json"), () => "json" as const)
97 | .otherwise(() => "text" as const);
98 | }
99 |
100 | // Make parameters optional if all or some of them are not required
101 | if (params) {
102 | const t = createBoxFactory({}, ctx);
103 | const filtered_params = ["query", "path", "header"] as Array<
104 | keyof Pick
105 | >;
106 |
107 | for (const k of filtered_params) {
108 | if (params[k] && lists[k].length) {
109 | if (lists[k].every((param) => !param.required)) {
110 | params[k] = t.reference("Partial", [t.object(params[k]!)]) as any;
111 | } else {
112 | for (const p of lists[k]) {
113 | if (!p.required) {
114 | params[k]![p.name] = t.optional(params[k]![p.name] as any);
115 | }
116 | }
117 | }
118 | }
119 | }
120 |
121 | // No need to pass empty objects, it's confusing
122 | endpoint.parameters = Object.keys(params).length ? (params as any as EndpointParameters) : undefined;
123 | }
124 |
125 | // Match the first 2xx-3xx response found, or fallback to default one otherwise
126 | let responseObject: ResponseObject | undefined;
127 | Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
128 | const statusCode = Number(status);
129 | if (statusCode >= 200 && statusCode < 300) {
130 | responseObject = refs.unwrap(responseOrRef);
131 | }
132 | });
133 | if (!responseObject && operation.responses?.default) {
134 | responseObject = refs.unwrap(operation.responses.default);
135 | }
136 |
137 | const content = responseObject?.content;
138 | if (content) {
139 | const matchingMediaType = Object.keys(content).find(isResponseMediaType);
140 | if (matchingMediaType && content[matchingMediaType]) {
141 | endpoint.response = openApiSchemaToTs({
142 | schema: content[matchingMediaType]?.schema ?? {} ?? {},
143 | ctx,
144 | });
145 | }
146 | }
147 |
148 | endpointList.push(endpoint);
149 | });
150 | });
151 |
152 | return { doc, refs, endpointList, factory };
153 | };
154 |
155 | const allowedParamMediaTypes = [
156 | "application/octet-stream",
157 | "multipart/form-data",
158 | "application/x-www-form-urlencoded",
159 | "*/*",
160 | ] as const;
161 | const isAllowedParamMediaTypes = (
162 | mediaType: string,
163 | ): mediaType is (typeof allowedParamMediaTypes)[number] | `application/${string}json${string}` | `text/${string}` =>
164 | (mediaType.includes("application/") && mediaType.includes("json")) ||
165 | allowedParamMediaTypes.includes(mediaType as any) ||
166 | mediaType.includes("text/");
167 |
168 | const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
169 | const getAlias = ({ path, method, operation }: Endpoint) =>
170 | (method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__");
171 |
172 | type MutationMethod = "post" | "put" | "patch" | "delete";
173 | type Method = "get" | "head" | "options" | MutationMethod;
174 |
175 | export type EndpointParameters = {
176 | body?: Box;
177 | query?: Box | Record;
178 | header?: Box | Record;
179 | path?: Box | Record;
180 | };
181 |
182 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
183 |
184 | type DefaultEndpoint = {
185 | parameters?: EndpointParameters | undefined;
186 | response: AnyBox;
187 | };
188 |
189 | export type Endpoint = {
190 | operation: OperationObject;
191 | method: Method;
192 | path: string;
193 | parameters?: TConfig["parameters"];
194 | requestFormat: RequestFormat;
195 | meta: {
196 | alias: string;
197 | hasParameters: boolean;
198 | areParametersRequired: boolean;
199 | };
200 | response: TConfig["response"];
201 | };
202 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/node.export.ts:
--------------------------------------------------------------------------------
1 | export { prettify } from "./format.ts";
2 | export { generateClientFiles } from "./generate-client-files.ts";
3 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/openapi-schema-to-ts.ts:
--------------------------------------------------------------------------------
1 | import { isPrimitiveType } from "./asserts.ts";
2 | import { Box } from "./box.ts";
3 | import { createBoxFactory } from "./box-factory.ts";
4 | import { isReferenceObject } from "./is-reference-object.ts";
5 | import { AnyBoxDef, OpenapiSchemaConvertArgs, type LibSchemaObject } from "./types.ts";
6 | import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
7 |
8 | export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: OpenapiSchemaConvertArgs): Box => {
9 | const meta = {} as OpenapiSchemaConvertArgs["meta"];
10 |
11 | if (!schema) {
12 | throw new Error("Schema is required");
13 | }
14 |
15 | const t = createBoxFactory(schema as LibSchemaObject, ctx);
16 | const getTs = () => {
17 | if (isReferenceObject(schema)) {
18 | const refInfo = ctx.refs.getInfosByRef(schema.$ref);
19 |
20 | return t.reference(refInfo.normalized);
21 | }
22 |
23 | if (Array.isArray(schema.type)) {
24 | if (schema.type.length === 1) {
25 | return openApiSchemaToTs({ schema: { ...schema, type: schema.type[0]! }, ctx, meta });
26 | }
27 |
28 | return t.union(schema.type.map((prop) => openApiSchemaToTs({ schema: { ...schema, type: prop }, ctx, meta })));
29 | }
30 |
31 | if (schema.type === "null") {
32 | return t.literal("null");
33 | }
34 |
35 | if (schema.oneOf) {
36 | if (schema.oneOf.length === 1) {
37 | return openApiSchemaToTs({ schema: schema.oneOf[0]!, ctx, meta });
38 | }
39 |
40 | return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
41 | }
42 |
43 | // tl;dr: anyOf = oneOf
44 | // oneOf matches exactly one subschema, and anyOf can match one or more subschemas.
45 | // https://swagger.io/docs/specification/v3_0/data-models/oneof-anyof-allof-not/
46 | if (schema.anyOf) {
47 | if (schema.anyOf.length === 1) {
48 | return openApiSchemaToTs({ schema: schema.anyOf[0]!, ctx, meta });
49 | }
50 |
51 | return t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
52 | }
53 |
54 | if (schema.allOf) {
55 | if (schema.allOf.length === 1) {
56 | return openApiSchemaToTs({ schema: schema.allOf[0]!, ctx, meta });
57 | }
58 |
59 | const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }));
60 | return t.intersection(types);
61 | }
62 |
63 | const schemaType = schema.type ? (schema.type.toLowerCase() as NonNullable) : undefined;
64 | if (schemaType && isPrimitiveType(schemaType)) {
65 | if (schema.enum) {
66 | if (schema.enum.length === 1) {
67 | const value = schema.enum[0];
68 | if (value === null) {
69 | return t.literal("null");
70 | } else if (value === true) {
71 | return t.literal("true");
72 | } else if (value === false) {
73 | return t.literal("false");
74 | } else if (typeof value === "number") {
75 | return t.literal(`${value}`)
76 | } else {
77 | return t.literal(`"${value}"`);
78 | }
79 | }
80 |
81 | if (schemaType === "string") {
82 | return t.union(schema.enum.map((value) => t.literal(`"${value}"`)));
83 | }
84 |
85 | if (schema.enum.some((e) => typeof e === "string")) {
86 | return t.never();
87 | }
88 |
89 | return t.union(schema.enum.map((value) => t.literal(value === null ? "null" : value)));
90 | }
91 |
92 | if (schemaType === "string") return t.string();
93 | if (schemaType === "boolean") return t.boolean();
94 | if (schemaType === "number" || schemaType === "integer") return t.number();
95 | if (schemaType === "null") return t.literal("null");
96 | }
97 |
98 | if (schemaType === "array") {
99 | if (schema.items) {
100 | let arrayOfType = openApiSchemaToTs({ schema: schema.items, ctx, meta });
101 | if (typeof arrayOfType === "string") {
102 | arrayOfType = t.reference(arrayOfType);
103 | }
104 |
105 | return t.array(arrayOfType);
106 | }
107 |
108 | return t.array(t.any());
109 | }
110 |
111 | if (schemaType === "object" || schema.properties || schema.additionalProperties) {
112 | if (!schema.properties) {
113 | if (
114 | schema.additionalProperties &&
115 | !isReferenceObject(schema.additionalProperties) &&
116 | typeof schema.additionalProperties !== "boolean" &&
117 | schema.additionalProperties.type
118 | ) {
119 | const valueSchema = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta });
120 | return t.literal(`Record`);
121 | }
122 |
123 | return t.literal("Record");
124 | }
125 |
126 | let additionalProperties;
127 | if (schema.additionalProperties) {
128 | let additionalPropertiesType;
129 | if (
130 | (typeof schema.additionalProperties === "boolean" && schema.additionalProperties) ||
131 | (typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0)
132 | ) {
133 | additionalPropertiesType = t.any();
134 | } else if (typeof schema.additionalProperties === "object") {
135 | additionalPropertiesType = openApiSchemaToTs({
136 | schema: schema.additionalProperties,
137 | ctx,
138 | meta,
139 | });
140 | }
141 |
142 | additionalProperties = t.object({ [t.string().value]: additionalPropertiesType! });
143 | }
144 |
145 | const hasRequiredArray = schema.required && schema.required.length > 0;
146 | const isPartial = !schema.required?.length;
147 |
148 | const props = Object.fromEntries(
149 | Object.entries(schema.properties).map(([prop, propSchema]) => {
150 | let propType = openApiSchemaToTs({ schema: propSchema, ctx, meta });
151 | if (typeof propType === "string") {
152 | // TODO Partial ?
153 | propType = t.reference(propType);
154 | }
155 |
156 | const isRequired = Boolean(isPartial ? true : hasRequiredArray ? schema.required?.includes(prop) : false);
157 | const isOptional = !isPartial && !isRequired;
158 | return [`${wrapWithQuotesIfNeeded(prop)}`, isOptional ? t.optional(propType) : propType];
159 | }),
160 | );
161 |
162 | const objectType = additionalProperties
163 | ? t.intersection([t.object(props), additionalProperties])
164 | : t.object(props);
165 |
166 | return isPartial ? t.reference("Partial", [objectType]) : objectType;
167 | }
168 |
169 | if (!schemaType) return t.unknown();
170 |
171 | throw new Error(`Unsupported schema type: ${schemaType}`);
172 | };
173 |
174 | let output = getTs();
175 | if (!isReferenceObject(schema)) {
176 | // OpenAPI 3.1 does not have nullable, but OpenAPI 3.0 does
177 | if ((schema as LibSchemaObject).nullable) {
178 | output = t.union([output, t.literal("null")]);
179 | }
180 | }
181 |
182 | return output;
183 | };
184 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/ref-resolver.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIObject, ReferenceObject } from "openapi3-ts/oas31";
2 | import { get } from "pastable/server";
3 |
4 | import { Box } from "./box.ts";
5 | import { isReferenceObject } from "./is-reference-object.ts";
6 | import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
7 | import { normalizeString } from "./string-utils.ts";
8 | import { AnyBoxDef, GenericFactory, type LibSchemaObject } from "./types.ts";
9 | import { topologicalSort } from "./topological-sort.ts";
10 |
11 | const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
12 | const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
13 |
14 | export type RefInfo = {
15 | /**
16 | * The (potentially autocorrected) ref
17 | * @example "#/components/schemas/MySchema"
18 | */
19 | ref: string;
20 | /**
21 | * The name of the ref
22 | * @example "MySchema"
23 | * */
24 | name: string;
25 | normalized: string;
26 | kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
27 | };
28 |
29 | export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) => {
30 | // both used for debugging purpose
31 | const nameByRef = new Map();
32 | const refByName = new Map();
33 |
34 | const byRef = new Map();
35 | const byNormalized = new Map();
36 |
37 | const boxByRef = new Map>();
38 |
39 | const getSchemaByRef = (ref: string) => {
40 | // #components -> #/components
41 | const correctRef = autocorrectRef(ref);
42 | const split = correctRef.split("/");
43 |
44 | // "#/components/schemas/Something.jsonld" -> #/components/schemas
45 | const path = split.slice(1, -1).join("/")!;
46 | const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", ".");
47 | const map = get(doc, normalizedPath) ?? ({} as any);
48 |
49 | // "#/components/schemas/Something.jsonld" -> "Something.jsonld"
50 | const name = split[split.length - 1]!;
51 | const normalized = normalizeString(name);
52 |
53 | nameByRef.set(correctRef, normalized);
54 | refByName.set(normalized, correctRef);
55 |
56 | const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] as RefInfo["kind"] };
57 | byRef.set(infos.ref, infos);
58 | byNormalized.set(infos.normalized, infos);
59 |
60 | // doc.components.schemas["Something.jsonld"]
61 | const schema = map[name] as T;
62 | if (!schema) {
63 | throw new Error(`Unresolved ref "${name}" not found in "${path}"`);
64 | }
65 |
66 | return schema;
67 | };
68 |
69 | const getInfosByRef = (ref: string) => byRef.get(autocorrectRef(ref))!;
70 |
71 | const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key));
72 |
73 | schemaEntries.forEach(([key, component]) => {
74 | Object.keys(component).map((name) => {
75 | const ref = `#/components/${key}/${name}`;
76 | getSchemaByRef(ref);
77 | });
78 | });
79 |
80 | const directDependencies = new Map>();
81 |
82 | // need to be done after all refs are resolved
83 | schemaEntries.forEach(([key, component]) => {
84 | Object.keys(component).map((name) => {
85 | const ref = `#/components/${key}/${name}`;
86 | const schema = getSchemaByRef(ref);
87 | boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory, refs: { getInfosByRef } as any } }));
88 |
89 | if (!directDependencies.has(ref)) {
90 | directDependencies.set(ref, new Set());
91 | }
92 | setSchemaDependencies(schema, directDependencies.get(ref)!);
93 | });
94 | });
95 |
96 | const transitiveDependencies = getTransitiveDependencies(directDependencies);
97 |
98 | return {
99 | get: getSchemaByRef,
100 | unwrap: (component: T) => {
101 | return (isReferenceObject(component) ? getSchemaByRef(component.$ref) : component) as Exclude;
102 | },
103 | getInfosByRef: getInfosByRef,
104 | infos: byRef,
105 | /**
106 | * Get the schemas in the order they should be generated, depending on their dependencies
107 | * so that a schema is generated before the ones that depend on it
108 | */
109 | getOrderedSchemas: () => {
110 | const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => {
111 | const infos = getInfosByRef(ref);
112 | return [boxByRef.get(infos.ref)!, infos] as [schema: Box, infos: RefInfo];
113 | });
114 |
115 | return schemaOrderedByDependencies;
116 | },
117 | directDependencies,
118 | transitiveDependencies,
119 | };
120 | };
121 |
122 | export interface RefResolver extends ReturnType {}
123 |
124 | const setSchemaDependencies = (schema: LibSchemaObject, deps: Set) => {
125 | const visit = (schema: LibSchemaObject | ReferenceObject): void => {
126 | if (!schema) return;
127 |
128 | if (isReferenceObject(schema)) {
129 | deps.add(schema.$ref);
130 | return;
131 | }
132 |
133 | if (schema.allOf) {
134 | for (const allOf of schema.allOf) {
135 | visit(allOf);
136 | }
137 |
138 | return;
139 | }
140 |
141 | if (schema.oneOf) {
142 | for (const oneOf of schema.oneOf) {
143 | visit(oneOf);
144 | }
145 |
146 | return;
147 | }
148 |
149 | if (schema.anyOf) {
150 | for (const anyOf of schema.anyOf) {
151 | visit(anyOf);
152 | }
153 |
154 | return;
155 | }
156 |
157 | if (schema.type === "array") {
158 | if (!schema.items) return;
159 | return void visit(schema.items);
160 | }
161 |
162 | if (schema.type === "object" || schema.properties || schema.additionalProperties) {
163 | if (schema.properties) {
164 | for (const property in schema.properties) {
165 | visit(schema.properties[property]!);
166 | }
167 | }
168 |
169 | if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
170 | visit(schema.additionalProperties);
171 | }
172 | }
173 | };
174 |
175 | visit(schema);
176 | };
177 |
178 | const getTransitiveDependencies = (directDependencies: Map>) => {
179 | const transitiveDependencies = new Map>();
180 | const visitedsDeepRefs = new Set();
181 |
182 | directDependencies.forEach((deps, ref) => {
183 | if (!transitiveDependencies.has(ref)) {
184 | transitiveDependencies.set(ref, new Set());
185 | }
186 |
187 | const visit = (depRef: string) => {
188 | transitiveDependencies.get(ref)!.add(depRef);
189 |
190 | const deps = directDependencies.get(depRef);
191 | if (deps && ref !== depRef) {
192 | deps.forEach((transitive) => {
193 | const key = ref + "__" + transitive;
194 | if (visitedsDeepRefs.has(key)) return;
195 |
196 | visitedsDeepRefs.add(key);
197 | visit(transitive);
198 | });
199 | }
200 | };
201 |
202 | deps.forEach((dep) => visit(dep));
203 | });
204 |
205 | return transitiveDependencies;
206 | };
207 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/string-utils.ts:
--------------------------------------------------------------------------------
1 | import { capitalize, kebabToCamel } from "pastable/server";
2 |
3 | export const toSchemasRef = (name: string) => `#/components/schemas/${name}`;
4 |
5 | export function normalizeString(text: string) {
6 | const prefixed = prefixStringStartingWithNumberIfNeeded(text);
7 | return prefixed
8 | .normalize("NFKD") // The normalize() using NFKD method returns the Unicode Normalization Form of a given string.
9 | .trim() // Remove whitespace from both sides of a string (optional)
10 | .replace(/\s+/g, "_") // Replace spaces with _
11 | .replace(/-+/g, "_") // Replace - with _
12 | .replace(/[^\w\-]+/g, "_") // Remove all non-word chars
13 | .replace(/--+/g, "-"); // Replace multiple - with single -
14 | }
15 |
16 | const onlyWordRegex = /^\w+$/;
17 | export const wrapWithQuotesIfNeeded = (str: string) => {
18 | if (str[0] === '"' && str[str.length - 1] === '"') return str;
19 | if (onlyWordRegex.test(str)) {
20 | return str;
21 | }
22 |
23 | return `"${str}"`;
24 | };
25 |
26 | const prefixStringStartingWithNumberIfNeeded = (str: string) => {
27 | const firstAsNumber = Number(str[0]);
28 | if (typeof firstAsNumber === "number" && !Number.isNaN(firstAsNumber)) {
29 | return "_" + str;
30 | }
31 |
32 | return str;
33 | };
34 |
35 | const pathParamWithBracketsRegex = /({\w+})/g;
36 | const wordPrecededByNonWordCharacter = /[^\w\-]+/g;
37 |
38 | /** @example turns `/media-objects/{id}` into `MediaObjectsId` */
39 | export const pathToVariableName = (path: string) =>
40 | capitalize(kebabToCamel(path).replaceAll("/", "_")) // /media-objects/{id} -> MediaObjects{id}
41 | .replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))) // {id} -> Id
42 | .replace(wordPrecededByNonWordCharacter, "_"); // "/robots.txt" -> "/robots_txt"
43 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/tanstack-query.generator.ts:
--------------------------------------------------------------------------------
1 | import { capitalize } from "pastable/server";
2 | import { prettify } from "./format.ts";
3 | import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
4 |
5 | type GeneratorOptions = ReturnType;
6 | type GeneratorContext = Required;
7 |
8 | export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => {
9 | const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
10 |
11 | const file = `
12 | import { queryOptions } from "@tanstack/react-query"
13 | import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
14 |
15 | type EndpointQueryKey = [
16 | TOptions & {
17 | _id: string;
18 | _infinite?: boolean;
19 | }
20 | ];
21 |
22 | const createQueryKey = (id: string, options?: TOptions, infinite?: boolean): [
23 | EndpointQueryKey[0]
24 | ] => {
25 | const params: EndpointQueryKey[0] = { _id: id, } as EndpointQueryKey[0];
26 | if (infinite) {
27 | params._infinite = infinite;
28 | }
29 | if (options?.body) {
30 | params.body = options.body;
31 | }
32 | if (options?.header) {
33 | params.header = options.header;
34 | }
35 | if (options?.path) {
36 | params.path = options.path;
37 | }
38 | if (options?.query) {
39 | params.query = options.query;
40 | }
41 | return [
42 | params
43 | ];
44 | };
45 |
46 | //
47 | ${Array.from(endpointMethods)
48 | .map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"];`)
49 | .join("\n")}
50 | //
51 |
52 | //
53 | export type EndpointParameters = {
54 | body?: unknown;
55 | query?: Record;
56 | header?: Record;
57 | path?: Record;
58 | };
59 |
60 | type RequiredKeys = {
61 | [P in keyof T]-?: undefined extends T[P] ? never : P;
62 | }[keyof T];
63 |
64 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
65 |
66 | //
67 |
68 | //
69 | export class TanstackQueryApiClient {
70 | constructor(public client: ApiClient) { }
71 |
72 | ${Array.from(endpointMethods)
73 | .map(
74 | (method) => `
75 | //
76 | ${method}(
77 | path: Path,
78 | ...params: MaybeOptionalArg
79 | ) {
80 | const queryKey = createQueryKey(path, params[0]);
81 | const query = {
82 | /** type-only property if you need easy access to the endpoint params */
83 | "~endpoint": {} as TEndpoint,
84 | queryKey,
85 | queryOptions: queryOptions({
86 | queryFn: async ({ queryKey, signal, }) => {
87 | const res = await this.client.${method}(path, {
88 | ...params,
89 | ...queryKey[0],
90 | signal,
91 | });
92 | return res as TEndpoint["response"];
93 | },
94 | queryKey: queryKey
95 | }),
96 | mutationOptions: {
97 | mutationKey: queryKey,
98 | mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
99 | const res = await this.client.${method}(path, {
100 | ...params,
101 | ...queryKey[0],
102 | ...localOptions,
103 | });
104 | return res as TEndpoint["response"];
105 | }
106 | }
107 | };
108 |
109 | return query
110 | }
111 | //
112 | `,
113 | )
114 | .join("\n")}
115 |
116 | //
117 | /**
118 | * Generic mutation method with full type-safety for any endpoint that doesnt require parameters to be passed initially
119 | */
120 | mutation<
121 | TMethod extends keyof EndpointByMethod,
122 | TPath extends keyof EndpointByMethod[TMethod],
123 | TEndpoint extends EndpointByMethod[TMethod][TPath],
124 | TSelection,
125 | >(method: TMethod, path: TPath, selectFn?: (res: Omit & {
126 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
127 | json: () => Promise;
128 | }) => TSelection) {
129 | const mutationKey = [{ method, path }] as const;
130 | return {
131 | /** type-only property if you need easy access to the endpoint params */
132 | "~endpoint": {} as TEndpoint,
133 | mutationKey: mutationKey,
134 | mutationOptions: {
135 | mutationKey: mutationKey,
136 | mutationFn: async (params: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
137 | const response = await this.client.request(method, path, params);
138 | const res = selectFn ? selectFn(response) : response
139 | return res as unknown extends TSelection ? typeof response : Awaited
140 | },
141 | },
142 | };
143 | }
144 | //
145 | }
146 | `;
147 |
148 | return prettify(file);
149 | };
150 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/topological-sort.ts:
--------------------------------------------------------------------------------
1 | /** @see https://gist.github.com/RubyTuesdayDONO/5006455 */
2 | export function topologicalSort(graph: Map>) {
3 | const sorted: string[] = [], // sorted list of IDs ( returned value )
4 | visited: Record = {}; // hash: id of already visited node => true
5 |
6 | function visit(name: string, ancestors: string[]) {
7 | if (!Array.isArray(ancestors)) ancestors = [];
8 | ancestors.push(name);
9 | visited[name] = true;
10 |
11 | const deps = graph.get(name);
12 | if (deps) {
13 | deps.forEach((dep) => {
14 | if (ancestors.includes(dep)) {
15 | // if already in ancestors, a closed chain (recursive relation) exists
16 | return;
17 | // throw new Error(
18 | // 'Circular dependency "' + dep + '" is required by "' + name + '": ' + ancestors.join(" -> ")
19 | // );
20 | }
21 |
22 | // if already exists, do nothing
23 | if (visited[dep]) return;
24 | visit(dep, ancestors.slice(0)); // recursive call
25 | });
26 | }
27 |
28 | if (!sorted.includes(name)) sorted.push(name);
29 | }
30 |
31 | // 2. topological sort
32 | graph.forEach((_, name) => visit(name, []));
33 |
34 | return sorted;
35 | }
36 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/ts-factory.ts:
--------------------------------------------------------------------------------
1 | import { Box } from "./box.ts";
2 | import { createFactory, unwrap } from "./box-factory.ts";
3 | import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
4 |
5 | export const tsFactory = createFactory({
6 | union: (types) => `(${types.map(unwrap).join(" | ")})`,
7 | intersection: (types) => `(${types.map(unwrap).join(" & ")})`,
8 | array: (type) => `Array<${unwrap(type)}>`,
9 | optional: (type) => `${unwrap(type)} | undefined`,
10 | reference: (name, typeArgs) => `${name}${typeArgs ? `<${typeArgs.map(unwrap).join(", ")}>` : ""}`,
11 | literal: (value) => value.toString(),
12 | string: () => "string" as const,
13 | number: () => "number" as const,
14 | boolean: () => "boolean" as const,
15 | unknown: () => "unknown" as const,
16 | any: () => "any" as const,
17 | never: () => "never" as const,
18 | object: (props) => {
19 | const propsString = Object.entries(props)
20 | .map(
21 | ([prop, type]) =>
22 | `${wrapWithQuotesIfNeeded(prop)}${typeof type !== "string" && Box.isOptional(type) ? "?" : ""}: ${unwrap(
23 | type,
24 | )}`,
25 | )
26 | .join(", ");
27 |
28 | return `{ ${propsString} }`;
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/packages/typed-openapi/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { ReferenceObject, SchemaObject } from "openapi3-ts/oas31";
2 | import type { SchemaObject as SchemaObject3 } from "openapi3-ts/oas30";
3 |
4 | import type { RefResolver } from "./ref-resolver.ts";
5 | import { Box } from "./box.ts";
6 |
7 | export type LibSchemaObject = SchemaObject & SchemaObject3;
8 |
9 | export type BoxDefinition = {
10 | type: string;
11 | params: unknown;
12 | value: string;
13 | };
14 | export type BoxParams = string | BoxDefinition;
15 | export type WithSchema = {
16 | schema: LibSchemaObject | ReferenceObject | undefined;
17 | ctx: OpenapiSchemaConvertContext;
18 | };
19 |
20 | export type BoxUnion = WithSchema & {
21 | type: "union";
22 | params: {
23 | types: Array;
24 | };
25 | value: string;
26 | };
27 |
28 | export type BoxIntersection = WithSchema & {
29 | type: "intersection";
30 | params: {
31 | types: Array;
32 | };
33 | value: string;
34 | };
35 |
36 | export type BoxArray = WithSchema & {
37 | type: "array";
38 | params: {
39 | type: BoxParams;
40 | };
41 | value: string;
42 | };
43 |
44 | export type BoxOptional = WithSchema & {
45 | type: "optional";
46 | params: {
47 | type: BoxParams;
48 | };
49 | value: string;
50 | };
51 |
52 | export type BoxRef = WithSchema & {
53 | type: "ref";
54 | params: { name: string; generics?: BoxParams[] | undefined };
55 | value: string;
56 | };
57 |
58 | export type BoxLiteral = WithSchema & {
59 | type: "literal";
60 | params: {};
61 | value: string;
62 | };
63 |
64 | export type BoxKeyword = WithSchema & {
65 | type: "keyword";
66 | params: { name: string };
67 | value: string;
68 | };
69 |
70 | export type BoxObject = WithSchema & {
71 | type: "object";
72 | params: { props: Record };
73 | value: string;
74 | };
75 |
76 | export type AnyBoxDef =
77 | | BoxUnion
78 | | BoxIntersection
79 | | BoxArray
80 | | BoxOptional
81 | | BoxRef
82 | | BoxLiteral
83 | | BoxKeyword
84 | | BoxObject;
85 | export type AnyBox = Box;
86 |
87 | export type OpenapiSchemaConvertArgs = {
88 | schema: SchemaObject | ReferenceObject;
89 | ctx: OpenapiSchemaConvertContext;
90 | meta?: {} | undefined;
91 | };
92 |
93 | export type FactoryCreator = (
94 | schema: SchemaObject | ReferenceObject,
95 | ctx: OpenapiSchemaConvertContext,
96 | ) => GenericFactory;
97 | export type OpenapiSchemaConvertContext = {
98 | factory: FactoryCreator | GenericFactory;
99 | refs: RefResolver;
100 | onBox?: (box: Box) => Box;
101 | };
102 |
103 | export type StringOrBox = string | Box;
104 |
105 | export type BoxFactory = {
106 | union: (types: Array) => Box;
107 | intersection: (types: Array) => Box;
108 | array: (type: StringOrBox) => Box;
109 | object: (props: Record) => Box;
110 | optional: (type: StringOrBox) => Box;
111 | reference: (name: string, generics?: Array | undefined) => Box;
112 | literal: (value: StringOrBox) => Box;
113 | string: () => Box;
114 | number: () => Box;
115 | boolean: () => Box;
116 | unknown: () => Box;
117 | any: () => Box;
118 | never: () => Box;
119 | };
120 |
121 | export type GenericFactory = {
122 | callback?: OpenapiSchemaConvertContext["onBox"];
123 | union: (types: Array) => string;
124 | intersection: (types: Array) => string;
125 | array: (type: StringOrBox) => string;
126 | object: (props: Record) => string;
127 | optional: (type: StringOrBox) => string;
128 | reference: (name: string, generics?: Array | undefined) => string;
129 | literal: (value: StringOrBox) => string;
130 | string: () => string;
131 | number: () => string;
132 | boolean: () => string;
133 | unknown: () => string;
134 | any: () => string;
135 | never: () => string;
136 | };
137 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tests/generate-runtime.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import SwaggerParser from "@apidevtools/swagger-parser";
3 | import type { OpenAPIObject } from "openapi3-ts/oas31";
4 | import { mapOpenApiEndpoints } from "../src/map-openapi-endpoints.ts";
5 | import { allowedRuntimes, generateFile } from "../src/generator.ts";
6 | import { prettify } from "../src/format.ts";
7 |
8 | const samples = ["petstore", "docker.openapi", "long-operation-id"];
9 | // @ts-expect-error
10 | const runtimes = allowedRuntimes.toJsonSchema().enum;
11 |
12 | samples.forEach((sample) => {
13 | describe(`generate-rutime-${sample}`, async () => {
14 | const filePath = `${__dirname}/samples/${sample}.yaml`;
15 | const openApiDoc = (await SwaggerParser.parse(filePath)) as OpenAPIObject;
16 | const ctx = mapOpenApiEndpoints(openApiDoc);
17 |
18 | runtimes.forEach((runtime: string) => {
19 | if (runtime === "arktype" && sample === "docker.openapi") return;
20 |
21 | test(`generate ${runtime}`, async () => {
22 | const tsRouter = await prettify(generateFile({ ...ctx, runtime: runtime as any }));
23 | const runtimeName = runtime === "none" ? "client" : runtime;
24 | await expect(tsRouter).toMatchFileSnapshot(`./snapshots/${sample}.` + runtimeName + ".ts");
25 | });
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tests/ref-resolver.test.ts:
--------------------------------------------------------------------------------
1 | import SwaggerParser from "@apidevtools/swagger-parser";
2 | import type { OpenAPIObject } from "openapi3-ts/oas31";
3 | import { describe, test } from "vitest";
4 | import { createRefResolver } from "../src/ref-resolver.ts";
5 | import { tsFactory } from "../src/ts-factory.ts";
6 |
7 | describe("generator", () => {
8 | test("petstore", async ({ expect }) => {
9 | const openApiDoc = (await SwaggerParser.parse("./tests/samples/petstore.yaml")) as OpenAPIObject;
10 | const ref = createRefResolver(openApiDoc, tsFactory);
11 | expect(ref).toMatchInlineSnapshot(`
12 | {
13 | "directDependencies": Map {
14 | "#/components/schemas/Order" => Set {},
15 | "#/components/schemas/Customer" => Set {
16 | "#/components/schemas/Address",
17 | },
18 | "#/components/schemas/Address" => Set {},
19 | "#/components/schemas/Category" => Set {},
20 | "#/components/schemas/User" => Set {},
21 | "#/components/schemas/Tag" => Set {},
22 | "#/components/schemas/Pet" => Set {
23 | "#/components/schemas/Category",
24 | "#/components/schemas/Tag",
25 | },
26 | "#/components/schemas/ApiResponse" => Set {},
27 | "#/components/requestBodies/Pet" => Set {},
28 | "#/components/requestBodies/UserArray" => Set {},
29 | },
30 | "get": [Function],
31 | "getInfosByRef": [Function],
32 | "getOrderedSchemas": [Function],
33 | "infos": Map {
34 | "#/components/schemas/Order" => {
35 | "kind": "schemas",
36 | "name": "Order",
37 | "normalized": "Order",
38 | "ref": "#/components/schemas/Order",
39 | },
40 | "#/components/schemas/Customer" => {
41 | "kind": "schemas",
42 | "name": "Customer",
43 | "normalized": "Customer",
44 | "ref": "#/components/schemas/Customer",
45 | },
46 | "#/components/schemas/Address" => {
47 | "kind": "schemas",
48 | "name": "Address",
49 | "normalized": "Address",
50 | "ref": "#/components/schemas/Address",
51 | },
52 | "#/components/schemas/Category" => {
53 | "kind": "schemas",
54 | "name": "Category",
55 | "normalized": "Category",
56 | "ref": "#/components/schemas/Category",
57 | },
58 | "#/components/schemas/User" => {
59 | "kind": "schemas",
60 | "name": "User",
61 | "normalized": "User",
62 | "ref": "#/components/schemas/User",
63 | },
64 | "#/components/schemas/Tag" => {
65 | "kind": "schemas",
66 | "name": "Tag",
67 | "normalized": "Tag",
68 | "ref": "#/components/schemas/Tag",
69 | },
70 | "#/components/schemas/Pet" => {
71 | "kind": "schemas",
72 | "name": "Pet",
73 | "normalized": "Pet",
74 | "ref": "#/components/schemas/Pet",
75 | },
76 | "#/components/schemas/ApiResponse" => {
77 | "kind": "schemas",
78 | "name": "ApiResponse",
79 | "normalized": "ApiResponse",
80 | "ref": "#/components/schemas/ApiResponse",
81 | },
82 | "#/components/requestBodies/Pet" => {
83 | "kind": "requestBodies",
84 | "name": "Pet",
85 | "normalized": "Pet",
86 | "ref": "#/components/requestBodies/Pet",
87 | },
88 | "#/components/requestBodies/UserArray" => {
89 | "kind": "requestBodies",
90 | "name": "UserArray",
91 | "normalized": "UserArray",
92 | "ref": "#/components/requestBodies/UserArray",
93 | },
94 | },
95 | "transitiveDependencies": Map {
96 | "#/components/schemas/Order" => Set {},
97 | "#/components/schemas/Customer" => Set {
98 | "#/components/schemas/Address",
99 | },
100 | "#/components/schemas/Address" => Set {},
101 | "#/components/schemas/Category" => Set {},
102 | "#/components/schemas/User" => Set {},
103 | "#/components/schemas/Tag" => Set {},
104 | "#/components/schemas/Pet" => Set {
105 | "#/components/schemas/Category",
106 | "#/components/schemas/Tag",
107 | },
108 | "#/components/schemas/ApiResponse" => Set {},
109 | "#/components/requestBodies/Pet" => Set {},
110 | "#/components/requestBodies/UserArray" => Set {},
111 | },
112 | "unwrap": [Function],
113 | }
114 | `);
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tests/samples/long-operation-id.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.3
2 | info:
3 | title: Sample API
4 | description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
5 | version: 0.1.9
6 | paths:
7 | /users:
8 | get:
9 | operationId: get_users
10 | summary: Returns a list of users.
11 | responses:
12 | '200':
13 | description: A JSON array of user names
14 | content:
15 | application/json:
16 | schema:
17 | type: array
18 | items:
19 | type: string
20 | post:
21 | operationId: very_very_very_very_very_very_very_very_very_very_long
22 | summary: Creates a user.
23 | requestBody:
24 | required: true
25 | content:
26 | application/json:
27 | schema:
28 | type: object
29 | properties:
30 | username:
31 | type: string
32 | responses:
33 | '201':
34 | description: Created
35 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tests/samples/parameters.yaml:
--------------------------------------------------------------------------------
1 | # https://swagger.io/docs/specification/describing-parameters/#common-for-path
2 | openapi: 3.0.3
3 | info:
4 | title: Spec with both path-level and operation-level parameters
5 | paths:
6 | /users/{id}:
7 | parameters:
8 | - in: path
9 | name: id
10 | schema:
11 | type: integer
12 | required: true
13 | description: The user ID.
14 | # GET/users/{id}?metadata=true
15 | get:
16 | summary: Gets a user by ID
17 | # Note we only define the query parameter, because the {id} is defined at the path level.
18 | parameters:
19 | - in: query
20 | name: metadata
21 | schema:
22 | type: boolean
23 | required: false
24 | description: If true, the endpoint returns only the user metadata.
25 | responses:
26 | '200':
27 | description: OK
28 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tests/snapshots/long-operation-id.arktype.ts:
--------------------------------------------------------------------------------
1 | import { scope, type } from "arktype";
2 |
3 | export const types = scope({
4 | __ENDPOINTS_START__: type({}),
5 | get_Get_users: type({
6 | method: '"GET"',
7 | path: '"/users"',
8 | requestFormat: '"json"',
9 | parameters: "never",
10 | response: "string[]",
11 | }),
12 | post_Very_very_very_very_very_very_very_very_very_very_long: type({
13 | method: '"POST"',
14 | path: '"/users"',
15 | requestFormat: '"json"',
16 | parameters: type({
17 | body: type({
18 | "username?": "string",
19 | }),
20 | }),
21 | response: "unknown",
22 | }),
23 | __ENDPOINTS_END__: type({}),
24 | }).export();
25 |
26 | export type __ENDPOINTS_START__ = typeof __ENDPOINTS_START__.infer;
27 | export const __ENDPOINTS_START__ = types.__ENDPOINTS_START__;
28 | export type get_Get_users = typeof get_Get_users.infer;
29 | export const get_Get_users = types.get_Get_users;
30 | export type post_Very_very_very_very_very_very_very_very_very_very_long =
31 | typeof post_Very_very_very_very_very_very_very_very_very_very_long.infer;
32 | export const post_Very_very_very_very_very_very_very_very_very_very_long =
33 | types.post_Very_very_very_very_very_very_very_very_very_very_long;
34 | export type __ENDPOINTS_END__ = typeof __ENDPOINTS_END__.infer;
35 | export const __ENDPOINTS_END__ = types.__ENDPOINTS_END__;
36 |
37 | //
38 | export const EndpointByMethod = {
39 | get: {
40 | "/users": get_Get_users,
41 | },
42 | post: {
43 | "/users": post_Very_very_very_very_very_very_very_very_very_very_long,
44 | },
45 | };
46 | export type EndpointByMethod = typeof EndpointByMethod;
47 | //
48 |
49 | //
50 | export type GetEndpoints = EndpointByMethod["get"];
51 | export type PostEndpoints = EndpointByMethod["post"];
52 | //
53 |
54 | //
55 | export type EndpointParameters = {
56 | body?: unknown;
57 | query?: Record;
58 | header?: Record;
59 | path?: Record;
60 | };
61 |
62 | export type MutationMethod = "post" | "put" | "patch" | "delete";
63 | export type Method = "get" | "head" | "options" | MutationMethod;
64 |
65 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
66 |
67 | export type DefaultEndpoint = {
68 | parameters?: EndpointParameters | undefined;
69 | response: unknown;
70 | };
71 |
72 | export type Endpoint = {
73 | operationId: string;
74 | method: Method;
75 | path: string;
76 | requestFormat: RequestFormat;
77 | parameters?: TConfig["parameters"];
78 | meta: {
79 | alias: string;
80 | hasParameters: boolean;
81 | areParametersRequired: boolean;
82 | };
83 | response: TConfig["response"];
84 | };
85 |
86 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
87 |
88 | type RequiredKeys = {
89 | [P in keyof T]-?: undefined extends T[P] ? never : P;
90 | }[keyof T];
91 |
92 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
93 |
94 | //
95 |
96 | //
97 | export class ApiClient {
98 | baseUrl: string = "";
99 |
100 | constructor(public fetcher: Fetcher) {}
101 |
102 | setBaseUrl(baseUrl: string) {
103 | this.baseUrl = baseUrl;
104 | return this;
105 | }
106 |
107 | parseResponse = async (response: Response): Promise => {
108 | const contentType = response.headers.get("content-type");
109 | if (contentType?.includes("application/json")) {
110 | return response.json();
111 | }
112 | return response.text() as unknown as T;
113 | };
114 |
115 | //
116 | get(
117 | path: Path,
118 | ...params: MaybeOptionalArg
119 | ): Promise {
120 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
121 | this.parseResponse(response),
122 | ) as Promise;
123 | }
124 | //
125 |
126 | //
127 | post(
128 | path: Path,
129 | ...params: MaybeOptionalArg
130 | ): Promise {
131 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
132 | this.parseResponse(response),
133 | ) as Promise;
134 | }
135 | //
136 |
137 | //
138 | /**
139 | * Generic request method with full type-safety for any endpoint
140 | */
141 | request<
142 | TMethod extends keyof EndpointByMethod,
143 | TPath extends keyof EndpointByMethod[TMethod],
144 | TEndpoint extends EndpointByMethod[TMethod][TPath],
145 | >(
146 | method: TMethod,
147 | path: TPath,
148 | ...params: MaybeOptionalArg
149 | ): Promise<
150 | Omit & {
151 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
152 | json: () => Promise;
153 | }
154 | > {
155 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
156 | }
157 | //
158 | }
159 |
160 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
161 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
162 | }
163 |
164 | /**
165 | Example usage:
166 | const api = createApiClient((method, url, params) =>
167 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
168 | );
169 | api.get("/users").then((users) => console.log(users));
170 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
171 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
172 | */
173 |
174 | //
3 | //
4 | }
5 |
6 | export namespace Endpoints {
7 | //
8 |
9 | export type get_Get_users = {
10 | method: "GET";
11 | path: "/users";
12 | requestFormat: "json";
13 | parameters: never;
14 | response: Array;
15 | };
16 | export type post_Very_very_very_very_very_very_very_very_very_very_long = {
17 | method: "POST";
18 | path: "/users";
19 | requestFormat: "json";
20 | parameters: {
21 | body: Partial<{ username: string }>;
22 | };
23 | response: unknown;
24 | };
25 |
26 | //
27 | }
28 |
29 | //
30 | export type EndpointByMethod = {
31 | get: {
32 | "/users": Endpoints.get_Get_users;
33 | };
34 | post: {
35 | "/users": Endpoints.post_Very_very_very_very_very_very_very_very_very_very_long;
36 | };
37 | };
38 |
39 | //
40 |
41 | //
42 | export type GetEndpoints = EndpointByMethod["get"];
43 | export type PostEndpoints = EndpointByMethod["post"];
44 | //
45 |
46 | //
47 | export type EndpointParameters = {
48 | body?: unknown;
49 | query?: Record;
50 | header?: Record;
51 | path?: Record;
52 | };
53 |
54 | export type MutationMethod = "post" | "put" | "patch" | "delete";
55 | export type Method = "get" | "head" | "options" | MutationMethod;
56 |
57 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
58 |
59 | export type DefaultEndpoint = {
60 | parameters?: EndpointParameters | undefined;
61 | response: unknown;
62 | };
63 |
64 | export type Endpoint = {
65 | operationId: string;
66 | method: Method;
67 | path: string;
68 | requestFormat: RequestFormat;
69 | parameters?: TConfig["parameters"];
70 | meta: {
71 | alias: string;
72 | hasParameters: boolean;
73 | areParametersRequired: boolean;
74 | };
75 | response: TConfig["response"];
76 | };
77 |
78 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
79 |
80 | type RequiredKeys = {
81 | [P in keyof T]-?: undefined extends T[P] ? never : P;
82 | }[keyof T];
83 |
84 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
85 |
86 | //
87 |
88 | //
89 | export class ApiClient {
90 | baseUrl: string = "";
91 |
92 | constructor(public fetcher: Fetcher) {}
93 |
94 | setBaseUrl(baseUrl: string) {
95 | this.baseUrl = baseUrl;
96 | return this;
97 | }
98 |
99 | parseResponse = async (response: Response): Promise => {
100 | const contentType = response.headers.get("content-type");
101 | if (contentType?.includes("application/json")) {
102 | return response.json();
103 | }
104 | return response.text() as unknown as T;
105 | };
106 |
107 | //
108 | get(
109 | path: Path,
110 | ...params: MaybeOptionalArg
111 | ): Promise {
112 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
113 | this.parseResponse(response),
114 | ) as Promise;
115 | }
116 | //
117 |
118 | //
119 | post(
120 | path: Path,
121 | ...params: MaybeOptionalArg
122 | ): Promise {
123 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
124 | this.parseResponse(response),
125 | ) as Promise;
126 | }
127 | //
128 |
129 | //
130 | /**
131 | * Generic request method with full type-safety for any endpoint
132 | */
133 | request<
134 | TMethod extends keyof EndpointByMethod,
135 | TPath extends keyof EndpointByMethod[TMethod],
136 | TEndpoint extends EndpointByMethod[TMethod][TPath],
137 | >(
138 | method: TMethod,
139 | path: TPath,
140 | ...params: MaybeOptionalArg
141 | ): Promise<
142 | Omit & {
143 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
144 | json: () => Promise;
145 | }
146 | > {
147 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
148 | }
149 | //
150 | }
151 |
152 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
153 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
154 | }
155 |
156 | /**
157 | Example usage:
158 | const api = createApiClient((method, url, params) =>
159 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
160 | );
161 | api.get("/users").then((users) => console.log(users));
162 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
163 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
164 | */
165 |
166 | // ;
4 | export const __ENDPOINTS_START__ = t.type({});
5 |
6 | export type get_Get_users = t.TypeOf;
7 | export const get_Get_users = t.type({
8 | method: t.literal("GET"),
9 | path: t.literal("/users"),
10 | requestFormat: t.literal("json"),
11 | parameters: t.never,
12 | response: t.array(t.string),
13 | });
14 |
15 | export type post_Very_very_very_very_very_very_very_very_very_very_long = t.TypeOf<
16 | typeof post_Very_very_very_very_very_very_very_very_very_very_long
17 | >;
18 | export const post_Very_very_very_very_very_very_very_very_very_very_long = t.type({
19 | method: t.literal("POST"),
20 | path: t.literal("/users"),
21 | requestFormat: t.literal("json"),
22 | parameters: t.type({
23 | body: t.type({
24 | username: t.union([t.undefined, t.string]),
25 | }),
26 | }),
27 | response: t.unknown,
28 | });
29 |
30 | export type __ENDPOINTS_END__ = t.TypeOf;
31 | export const __ENDPOINTS_END__ = t.type({});
32 |
33 | //
34 | export const EndpointByMethod = {
35 | get: {
36 | "/users": get_Get_users,
37 | },
38 | post: {
39 | "/users": post_Very_very_very_very_very_very_very_very_very_very_long,
40 | },
41 | };
42 | export type EndpointByMethod = typeof EndpointByMethod;
43 | //
44 |
45 | //
46 | export type GetEndpoints = EndpointByMethod["get"];
47 | export type PostEndpoints = EndpointByMethod["post"];
48 | //
49 |
50 | //
51 | export type EndpointParameters = {
52 | body?: unknown;
53 | query?: Record;
54 | header?: Record;
55 | path?: Record;
56 | };
57 |
58 | export type MutationMethod = "post" | "put" | "patch" | "delete";
59 | export type Method = "get" | "head" | "options" | MutationMethod;
60 |
61 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
62 |
63 | export type DefaultEndpoint = {
64 | parameters?: EndpointParameters | undefined;
65 | response: unknown;
66 | };
67 |
68 | export type Endpoint = {
69 | operationId: string;
70 | method: Method;
71 | path: string;
72 | requestFormat: RequestFormat;
73 | parameters?: TConfig["parameters"];
74 | meta: {
75 | alias: string;
76 | hasParameters: boolean;
77 | areParametersRequired: boolean;
78 | };
79 | response: TConfig["response"];
80 | };
81 |
82 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
83 |
84 | type RequiredKeys = {
85 | [P in keyof T]-?: undefined extends T[P] ? never : P;
86 | }[keyof T];
87 |
88 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
89 |
90 | //
91 |
92 | //
93 | export class ApiClient {
94 | baseUrl: string = "";
95 |
96 | constructor(public fetcher: Fetcher) {}
97 |
98 | setBaseUrl(baseUrl: string) {
99 | this.baseUrl = baseUrl;
100 | return this;
101 | }
102 |
103 | parseResponse = async (response: Response): Promise => {
104 | const contentType = response.headers.get("content-type");
105 | if (contentType?.includes("application/json")) {
106 | return response.json();
107 | }
108 | return response.text() as unknown as T;
109 | };
110 |
111 | //
112 | get(
113 | path: Path,
114 | ...params: MaybeOptionalArg["parameters"]>
115 | ): Promise["response"]> {
116 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
117 | this.parseResponse(response),
118 | ) as Promise["response"]>;
119 | }
120 | //
121 |
122 | //
123 | post(
124 | path: Path,
125 | ...params: MaybeOptionalArg["parameters"]>
126 | ): Promise["response"]> {
127 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
128 | this.parseResponse(response),
129 | ) as Promise["response"]>;
130 | }
131 | //
132 |
133 | //
134 | /**
135 | * Generic request method with full type-safety for any endpoint
136 | */
137 | request<
138 | TMethod extends keyof EndpointByMethod,
139 | TPath extends keyof EndpointByMethod[TMethod],
140 | TEndpoint extends EndpointByMethod[TMethod][TPath],
141 | >(
142 | method: TMethod,
143 | path: TPath,
144 | ...params: MaybeOptionalArg["parameters"]>
145 | ): Promise<
146 | Omit & {
147 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
148 | json: () => Promise;
149 | }
150 | > {
151 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
152 | }
153 | //
154 | }
155 |
156 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
157 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
158 | }
159 |
160 | /**
161 | Example usage:
162 | const api = createApiClient((method, url, params) =>
163 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
164 | );
165 | api.get("/users").then((users) => console.log(users));
166 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
167 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
168 | */
169 |
170 | // ;
4 | const __ENDPOINTS_START__ = Type.Object({});
5 |
6 | export type get_Get_users = Static;
7 | export const get_Get_users = Type.Object({
8 | method: Type.Literal("GET"),
9 | path: Type.Literal("/users"),
10 | requestFormat: Type.Literal("json"),
11 | parameters: Type.Never(),
12 | response: Type.Array(Type.String()),
13 | });
14 |
15 | export type post_Very_very_very_very_very_very_very_very_very_very_long = Static<
16 | typeof post_Very_very_very_very_very_very_very_very_very_very_long
17 | >;
18 | export const post_Very_very_very_very_very_very_very_very_very_very_long = Type.Object({
19 | method: Type.Literal("POST"),
20 | path: Type.Literal("/users"),
21 | requestFormat: Type.Literal("json"),
22 | parameters: Type.Object({
23 | body: Type.Partial(
24 | Type.Object({
25 | username: Type.String(),
26 | }),
27 | ),
28 | }),
29 | response: Type.Unknown(),
30 | });
31 |
32 | type __ENDPOINTS_END__ = Static;
33 | const __ENDPOINTS_END__ = Type.Object({});
34 |
35 | //
36 | export const EndpointByMethod = {
37 | get: {
38 | "/users": get_Get_users,
39 | },
40 | post: {
41 | "/users": post_Very_very_very_very_very_very_very_very_very_very_long,
42 | },
43 | };
44 | export type EndpointByMethod = typeof EndpointByMethod;
45 | //
46 |
47 | //
48 | export type GetEndpoints = EndpointByMethod["get"];
49 | export type PostEndpoints = EndpointByMethod["post"];
50 | //
51 |
52 | //
53 | export type EndpointParameters = {
54 | body?: unknown;
55 | query?: Record;
56 | header?: Record;
57 | path?: Record;
58 | };
59 |
60 | export type MutationMethod = "post" | "put" | "patch" | "delete";
61 | export type Method = "get" | "head" | "options" | MutationMethod;
62 |
63 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
64 |
65 | export type DefaultEndpoint = {
66 | parameters?: EndpointParameters | undefined;
67 | response: unknown;
68 | };
69 |
70 | export type Endpoint = {
71 | operationId: string;
72 | method: Method;
73 | path: string;
74 | requestFormat: RequestFormat;
75 | parameters?: TConfig["parameters"];
76 | meta: {
77 | alias: string;
78 | hasParameters: boolean;
79 | areParametersRequired: boolean;
80 | };
81 | response: TConfig["response"];
82 | };
83 |
84 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
85 |
86 | type RequiredKeys = {
87 | [P in keyof T]-?: undefined extends T[P] ? never : P;
88 | }[keyof T];
89 |
90 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
91 |
92 | //
93 |
94 | //
95 | export class ApiClient {
96 | baseUrl: string = "";
97 |
98 | constructor(public fetcher: Fetcher) {}
99 |
100 | setBaseUrl(baseUrl: string) {
101 | this.baseUrl = baseUrl;
102 | return this;
103 | }
104 |
105 | parseResponse = async (response: Response): Promise => {
106 | const contentType = response.headers.get("content-type");
107 | if (contentType?.includes("application/json")) {
108 | return response.json();
109 | }
110 | return response.text() as unknown as T;
111 | };
112 |
113 | //
114 | get(
115 | path: Path,
116 | ...params: MaybeOptionalArg["parameters"]>
117 | ): Promise["response"]> {
118 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
119 | this.parseResponse(response),
120 | ) as Promise["response"]>;
121 | }
122 | //
123 |
124 | //
125 | post(
126 | path: Path,
127 | ...params: MaybeOptionalArg["parameters"]>
128 | ): Promise["response"]> {
129 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
130 | this.parseResponse(response),
131 | ) as Promise["response"]>;
132 | }
133 | //
134 |
135 | //
136 | /**
137 | * Generic request method with full type-safety for any endpoint
138 | */
139 | request<
140 | TMethod extends keyof EndpointByMethod,
141 | TPath extends keyof EndpointByMethod[TMethod],
142 | TEndpoint extends EndpointByMethod[TMethod][TPath],
143 | >(
144 | method: TMethod,
145 | path: TPath,
146 | ...params: MaybeOptionalArg["parameters"]>
147 | ): Promise<
148 | Omit & {
149 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
150 | json: () => Promise;
151 | }
152 | > {
153 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
154 | }
155 | //
156 | }
157 |
158 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
159 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
160 | }
161 |
162 | /**
163 | Example usage:
164 | const api = createApiClient((method, url, params) =>
165 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
166 | );
167 | api.get("/users").then((users) => console.log(users));
168 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
169 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
170 | */
171 |
172 | // ;
4 | export const __ENDPOINTS_START__ = v.object({});
5 |
6 | export type get_Get_users = v.InferOutput;
7 | export const get_Get_users = v.object({
8 | method: v.literal("GET"),
9 | path: v.literal("/users"),
10 | requestFormat: v.literal("json"),
11 | parameters: v.never(),
12 | response: v.array(v.string()),
13 | });
14 |
15 | export type post_Very_very_very_very_very_very_very_very_very_very_long = v.InferOutput<
16 | typeof post_Very_very_very_very_very_very_very_very_very_very_long
17 | >;
18 | export const post_Very_very_very_very_very_very_very_very_very_very_long = v.object({
19 | method: v.literal("POST"),
20 | path: v.literal("/users"),
21 | requestFormat: v.literal("json"),
22 | parameters: v.object({
23 | body: v.object({
24 | username: v.optional(v.string()),
25 | }),
26 | }),
27 | response: v.unknown(),
28 | });
29 |
30 | export type __ENDPOINTS_END__ = v.InferOutput;
31 | export const __ENDPOINTS_END__ = v.object({});
32 |
33 | //
34 | export const EndpointByMethod = {
35 | get: {
36 | "/users": get_Get_users,
37 | },
38 | post: {
39 | "/users": post_Very_very_very_very_very_very_very_very_very_very_long,
40 | },
41 | };
42 | export type EndpointByMethod = typeof EndpointByMethod;
43 | //
44 |
45 | //
46 | export type GetEndpoints = EndpointByMethod["get"];
47 | export type PostEndpoints = EndpointByMethod["post"];
48 | //
49 |
50 | //
51 | export type EndpointParameters = {
52 | body?: unknown;
53 | query?: Record;
54 | header?: Record;
55 | path?: Record;
56 | };
57 |
58 | export type MutationMethod = "post" | "put" | "patch" | "delete";
59 | export type Method = "get" | "head" | "options" | MutationMethod;
60 |
61 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
62 |
63 | export type DefaultEndpoint = {
64 | parameters?: EndpointParameters | undefined;
65 | response: unknown;
66 | };
67 |
68 | export type Endpoint = {
69 | operationId: string;
70 | method: Method;
71 | path: string;
72 | requestFormat: RequestFormat;
73 | parameters?: TConfig["parameters"];
74 | meta: {
75 | alias: string;
76 | hasParameters: boolean;
77 | areParametersRequired: boolean;
78 | };
79 | response: TConfig["response"];
80 | };
81 |
82 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
83 |
84 | type RequiredKeys = {
85 | [P in keyof T]-?: undefined extends T[P] ? never : P;
86 | }[keyof T];
87 |
88 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
89 |
90 | //
91 |
92 | //
93 | export class ApiClient {
94 | baseUrl: string = "";
95 |
96 | constructor(public fetcher: Fetcher) {}
97 |
98 | setBaseUrl(baseUrl: string) {
99 | this.baseUrl = baseUrl;
100 | return this;
101 | }
102 |
103 | parseResponse = async (response: Response): Promise => {
104 | const contentType = response.headers.get("content-type");
105 | if (contentType?.includes("application/json")) {
106 | return response.json();
107 | }
108 | return response.text() as unknown as T;
109 | };
110 |
111 | //
112 | get(
113 | path: Path,
114 | ...params: MaybeOptionalArg["parameters"]>
115 | ): Promise["response"]> {
116 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
117 | this.parseResponse(response),
118 | ) as Promise["response"]>;
119 | }
120 | //
121 |
122 | //
123 | post(
124 | path: Path,
125 | ...params: MaybeOptionalArg["parameters"]>
126 | ): Promise["response"]> {
127 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
128 | this.parseResponse(response),
129 | ) as Promise["response"]>;
130 | }
131 | //
132 |
133 | //
134 | /**
135 | * Generic request method with full type-safety for any endpoint
136 | */
137 | request<
138 | TMethod extends keyof EndpointByMethod,
139 | TPath extends keyof EndpointByMethod[TMethod],
140 | TEndpoint extends EndpointByMethod[TMethod][TPath],
141 | >(
142 | method: TMethod,
143 | path: TPath,
144 | ...params: MaybeOptionalArg["parameters"]>
145 | ): Promise<
146 | Omit & {
147 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
148 | json: () => Promise;
149 | }
150 | > {
151 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
152 | }
153 | //
154 | }
155 |
156 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
157 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
158 | }
159 |
160 | /**
161 | Example usage:
162 | const api = createApiClient((method, url, params) =>
163 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
164 | );
165 | api.get("/users").then((users) => console.log(users));
166 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
167 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
168 | */
169 |
170 | // value === "GET").required(),
6 | path: y.mixed((value): value is "/users" => value === "/users").required(),
7 | requestFormat: y.mixed((value): value is "json" => value === "json").required(),
8 | parameters: y.mixed((value): value is never => false).required(),
9 | response: y.array(y.string().required()),
10 | };
11 |
12 | export type post_Very_very_very_very_very_very_very_very_very_very_long =
13 | typeof post_Very_very_very_very_very_very_very_very_very_very_long;
14 | export const post_Very_very_very_very_very_very_very_very_very_very_long = {
15 | method: y.mixed((value): value is "POST" => value === "POST").required(),
16 | path: y.mixed((value): value is "/users" => value === "/users").required(),
17 | requestFormat: y.mixed((value): value is "json" => value === "json").required(),
18 | parameters: y.object({
19 | body: y.object({
20 | username: y.string().required().optional(),
21 | }),
22 | }),
23 | response: y.mixed((value): value is any => true).required() as y.MixedSchema,
24 | };
25 |
26 | //
27 | export const EndpointByMethod = {
28 | get: {
29 | "/users": get_Get_users,
30 | },
31 | post: {
32 | "/users": post_Very_very_very_very_very_very_very_very_very_very_long,
33 | },
34 | };
35 | export type EndpointByMethod = typeof EndpointByMethod;
36 | //
37 |
38 | //
39 | export type GetEndpoints = EndpointByMethod["get"];
40 | export type PostEndpoints = EndpointByMethod["post"];
41 | //
42 |
43 | //
44 | export type EndpointParameters = {
45 | body?: unknown;
46 | query?: Record;
47 | header?: Record;
48 | path?: Record;
49 | };
50 |
51 | export type MutationMethod = "post" | "put" | "patch" | "delete";
52 | export type Method = "get" | "head" | "options" | MutationMethod;
53 |
54 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
55 |
56 | export type DefaultEndpoint = {
57 | parameters?: EndpointParameters | undefined;
58 | response: unknown;
59 | };
60 |
61 | export type Endpoint = {
62 | operationId: string;
63 | method: Method;
64 | path: string;
65 | requestFormat: RequestFormat;
66 | parameters?: TConfig["parameters"];
67 | meta: {
68 | alias: string;
69 | hasParameters: boolean;
70 | areParametersRequired: boolean;
71 | };
72 | response: TConfig["response"];
73 | };
74 |
75 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
76 |
77 | type RequiredKeys = {
78 | [P in keyof T]-?: undefined extends T[P] ? never : P;
79 | }[keyof T];
80 |
81 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
82 |
83 | //
84 |
85 | //
86 | export class ApiClient {
87 | baseUrl: string = "";
88 |
89 | constructor(public fetcher: Fetcher) {}
90 |
91 | setBaseUrl(baseUrl: string) {
92 | this.baseUrl = baseUrl;
93 | return this;
94 | }
95 |
96 | parseResponse = async (response: Response): Promise => {
97 | const contentType = response.headers.get("content-type");
98 | if (contentType?.includes("application/json")) {
99 | return response.json();
100 | }
101 | return response.text() as unknown as T;
102 | };
103 |
104 | //
105 | get(
106 | path: Path,
107 | ...params: MaybeOptionalArg>
108 | ): Promise> {
109 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
110 | this.parseResponse(response),
111 | ) as Promise>;
112 | }
113 | //
114 |
115 | //
116 | post(
117 | path: Path,
118 | ...params: MaybeOptionalArg>
119 | ): Promise> {
120 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
121 | this.parseResponse(response),
122 | ) as Promise>;
123 | }
124 | //
125 |
126 | //
127 | /**
128 | * Generic request method with full type-safety for any endpoint
129 | */
130 | request<
131 | TMethod extends keyof EndpointByMethod,
132 | TPath extends keyof EndpointByMethod[TMethod],
133 | TEndpoint extends EndpointByMethod[TMethod][TPath],
134 | >(
135 | method: TMethod,
136 | path: TPath,
137 | ...params: MaybeOptionalArg>
138 | ): Promise<
139 | Omit & {
140 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
141 | json: () => Promise;
142 | }
143 | > {
144 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
145 | }
146 | //
147 | }
148 |
149 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
150 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
151 | }
152 |
153 | /**
154 | Example usage:
155 | const api = createApiClient((method, url, params) =>
156 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
157 | );
158 | api.get("/users").then((users) => console.log(users));
159 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
160 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
161 | */
162 |
163 | //
27 | export const EndpointByMethod = {
28 | get: {
29 | "/users": get_Get_users,
30 | },
31 | post: {
32 | "/users": post_Very_very_very_very_very_very_very_very_very_very_long,
33 | },
34 | };
35 | export type EndpointByMethod = typeof EndpointByMethod;
36 | //
37 |
38 | //
39 | export type GetEndpoints = EndpointByMethod["get"];
40 | export type PostEndpoints = EndpointByMethod["post"];
41 | //
42 |
43 | //
44 | export type EndpointParameters = {
45 | body?: unknown;
46 | query?: Record;
47 | header?: Record;
48 | path?: Record;
49 | };
50 |
51 | export type MutationMethod = "post" | "put" | "patch" | "delete";
52 | export type Method = "get" | "head" | "options" | MutationMethod;
53 |
54 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
55 |
56 | export type DefaultEndpoint = {
57 | parameters?: EndpointParameters | undefined;
58 | response: unknown;
59 | };
60 |
61 | export type Endpoint = {
62 | operationId: string;
63 | method: Method;
64 | path: string;
65 | requestFormat: RequestFormat;
66 | parameters?: TConfig["parameters"];
67 | meta: {
68 | alias: string;
69 | hasParameters: boolean;
70 | areParametersRequired: boolean;
71 | };
72 | response: TConfig["response"];
73 | };
74 |
75 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
76 |
77 | type RequiredKeys = {
78 | [P in keyof T]-?: undefined extends T[P] ? never : P;
79 | }[keyof T];
80 |
81 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
82 |
83 | //
84 |
85 | //
86 | export class ApiClient {
87 | baseUrl: string = "";
88 |
89 | constructor(public fetcher: Fetcher) {}
90 |
91 | setBaseUrl(baseUrl: string) {
92 | this.baseUrl = baseUrl;
93 | return this;
94 | }
95 |
96 | parseResponse = async (response: Response): Promise => {
97 | const contentType = response.headers.get("content-type");
98 | if (contentType?.includes("application/json")) {
99 | return response.json();
100 | }
101 | return response.text() as unknown as T;
102 | };
103 |
104 | //
105 | get(
106 | path: Path,
107 | ...params: MaybeOptionalArg>
108 | ): Promise> {
109 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
110 | this.parseResponse(response),
111 | ) as Promise>;
112 | }
113 | //
114 |
115 | //
116 | post(
117 | path: Path,
118 | ...params: MaybeOptionalArg>
119 | ): Promise> {
120 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
121 | this.parseResponse(response),
122 | ) as Promise>;
123 | }
124 | //
125 |
126 | //
127 | /**
128 | * Generic request method with full type-safety for any endpoint
129 | */
130 | request<
131 | TMethod extends keyof EndpointByMethod,
132 | TPath extends keyof EndpointByMethod[TMethod],
133 | TEndpoint extends EndpointByMethod[TMethod][TPath],
134 | >(
135 | method: TMethod,
136 | path: TPath,
137 | ...params: MaybeOptionalArg>
138 | ): Promise<
139 | Omit & {
140 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
141 | json: () => Promise;
142 | }
143 | > {
144 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
145 | }
146 | //
147 | }
148 |
149 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
150 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
151 | }
152 |
153 | /**
154 | Example usage:
155 | const api = createApiClient((method, url, params) =>
156 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
157 | );
158 | api.get("/users").then((users) => console.log(users));
159 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
160 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
161 | */
162 |
163 | //
3 | export type Order = Partial<{
4 | id: number;
5 | petId: number;
6 | quantity: number;
7 | shipDate: string;
8 | status: "placed" | "approved" | "delivered";
9 | complete: boolean;
10 | }>;
11 | export type Address = Partial<{ street: string; city: string; state: string; zip: string }>;
12 | export type Customer = Partial<{ id: number; username: string; address: Array }>;
13 | export type Category = Partial<{ id: number; name: string }>;
14 | export type User = Partial<{
15 | id: number;
16 | username: string;
17 | firstName: string;
18 | lastName: string;
19 | email: string;
20 | password: string;
21 | phone: string;
22 | userStatus: number;
23 | }>;
24 | export type Tag = Partial<{ id: number; name: string }>;
25 | export type Pet = {
26 | id?: number | undefined;
27 | name: string;
28 | category?: Category | undefined;
29 | photoUrls: Array;
30 | tags?: Array | undefined;
31 | status?: ("available" | "pending" | "sold") | undefined;
32 | };
33 | export type ApiResponse = Partial<{ code: number; type: string; message: string }>;
34 |
35 | //
36 | }
37 |
38 | export namespace Endpoints {
39 | //
40 |
41 | export type put_UpdatePet = {
42 | method: "PUT";
43 | path: "/pet";
44 | requestFormat: "json";
45 | parameters: {
46 | body: Schemas.Pet;
47 | };
48 | response: Schemas.Pet;
49 | };
50 | export type post_AddPet = {
51 | method: "POST";
52 | path: "/pet";
53 | requestFormat: "json";
54 | parameters: {
55 | body: Schemas.Pet;
56 | };
57 | response: Schemas.Pet;
58 | };
59 | export type get_FindPetsByStatus = {
60 | method: "GET";
61 | path: "/pet/findByStatus";
62 | requestFormat: "json";
63 | parameters: {
64 | query: Partial<{ status: "available" | "pending" | "sold" }>;
65 | };
66 | response: Array;
67 | };
68 | export type get_FindPetsByTags = {
69 | method: "GET";
70 | path: "/pet/findByTags";
71 | requestFormat: "json";
72 | parameters: {
73 | query: Partial<{ tags: Array }>;
74 | };
75 | response: Array;
76 | };
77 | export type get_GetPetById = {
78 | method: "GET";
79 | path: "/pet/{petId}";
80 | requestFormat: "json";
81 | parameters: {
82 | path: { petId: number };
83 | };
84 | response: Schemas.Pet;
85 | };
86 | export type post_UpdatePetWithForm = {
87 | method: "POST";
88 | path: "/pet/{petId}";
89 | requestFormat: "json";
90 | parameters: {
91 | query: Partial<{ name: string; status: string }>;
92 | path: { petId: number };
93 | };
94 | response: unknown;
95 | };
96 | export type delete_DeletePet = {
97 | method: "DELETE";
98 | path: "/pet/{petId}";
99 | requestFormat: "json";
100 | parameters: {
101 | path: { petId: number };
102 | header: Partial<{ api_key: string }>;
103 | };
104 | response: unknown;
105 | };
106 | export type post_UploadFile = {
107 | method: "POST";
108 | path: "/pet/{petId}/uploadImage";
109 | requestFormat: "binary";
110 | parameters: {
111 | query: Partial<{ additionalMetadata: string }>;
112 | path: { petId: number };
113 |
114 | body: string;
115 | };
116 | response: Schemas.ApiResponse;
117 | };
118 | export type get_GetInventory = {
119 | method: "GET";
120 | path: "/store/inventory";
121 | requestFormat: "json";
122 | parameters: never;
123 | response: Record;
124 | };
125 | export type post_PlaceOrder = {
126 | method: "POST";
127 | path: "/store/order";
128 | requestFormat: "json";
129 | parameters: {
130 | body: Schemas.Order;
131 | };
132 | response: Schemas.Order;
133 | };
134 | export type get_GetOrderById = {
135 | method: "GET";
136 | path: "/store/order/{orderId}";
137 | requestFormat: "json";
138 | parameters: {
139 | path: { orderId: number };
140 | };
141 | response: Schemas.Order;
142 | };
143 | export type delete_DeleteOrder = {
144 | method: "DELETE";
145 | path: "/store/order/{orderId}";
146 | requestFormat: "json";
147 | parameters: {
148 | path: { orderId: number };
149 | };
150 | response: unknown;
151 | };
152 | export type post_CreateUser = {
153 | method: "POST";
154 | path: "/user";
155 | requestFormat: "json";
156 | parameters: {
157 | body: Schemas.User;
158 | };
159 | response: Schemas.User;
160 | };
161 | export type post_CreateUsersWithListInput = {
162 | method: "POST";
163 | path: "/user/createWithList";
164 | requestFormat: "json";
165 | parameters: {
166 | body: Array;
167 | };
168 | response: Schemas.User;
169 | };
170 | export type get_LoginUser = {
171 | method: "GET";
172 | path: "/user/login";
173 | requestFormat: "json";
174 | parameters: {
175 | query: Partial<{ username: string; password: string }>;
176 | };
177 | response: string;
178 | };
179 | export type get_LogoutUser = {
180 | method: "GET";
181 | path: "/user/logout";
182 | requestFormat: "json";
183 | parameters: never;
184 | response: unknown;
185 | };
186 | export type get_GetUserByName = {
187 | method: "GET";
188 | path: "/user/{username}";
189 | requestFormat: "json";
190 | parameters: {
191 | path: { username: string };
192 | };
193 | response: Schemas.User;
194 | };
195 | export type put_UpdateUser = {
196 | method: "PUT";
197 | path: "/user/{username}";
198 | requestFormat: "json";
199 | parameters: {
200 | path: { username: string };
201 |
202 | body: Schemas.User;
203 | };
204 | response: unknown;
205 | };
206 | export type delete_DeleteUser = {
207 | method: "DELETE";
208 | path: "/user/{username}";
209 | requestFormat: "json";
210 | parameters: {
211 | path: { username: string };
212 | };
213 | response: unknown;
214 | };
215 |
216 | //
217 | }
218 |
219 | //
220 | export type EndpointByMethod = {
221 | put: {
222 | "/pet": Endpoints.put_UpdatePet;
223 | "/user/{username}": Endpoints.put_UpdateUser;
224 | };
225 | post: {
226 | "/pet": Endpoints.post_AddPet;
227 | "/pet/{petId}": Endpoints.post_UpdatePetWithForm;
228 | "/pet/{petId}/uploadImage": Endpoints.post_UploadFile;
229 | "/store/order": Endpoints.post_PlaceOrder;
230 | "/user": Endpoints.post_CreateUser;
231 | "/user/createWithList": Endpoints.post_CreateUsersWithListInput;
232 | };
233 | get: {
234 | "/pet/findByStatus": Endpoints.get_FindPetsByStatus;
235 | "/pet/findByTags": Endpoints.get_FindPetsByTags;
236 | "/pet/{petId}": Endpoints.get_GetPetById;
237 | "/store/inventory": Endpoints.get_GetInventory;
238 | "/store/order/{orderId}": Endpoints.get_GetOrderById;
239 | "/user/login": Endpoints.get_LoginUser;
240 | "/user/logout": Endpoints.get_LogoutUser;
241 | "/user/{username}": Endpoints.get_GetUserByName;
242 | };
243 | delete: {
244 | "/pet/{petId}": Endpoints.delete_DeletePet;
245 | "/store/order/{orderId}": Endpoints.delete_DeleteOrder;
246 | "/user/{username}": Endpoints.delete_DeleteUser;
247 | };
248 | };
249 |
250 | //
251 |
252 | //
253 | export type PutEndpoints = EndpointByMethod["put"];
254 | export type PostEndpoints = EndpointByMethod["post"];
255 | export type GetEndpoints = EndpointByMethod["get"];
256 | export type DeleteEndpoints = EndpointByMethod["delete"];
257 | //
258 |
259 | //
260 | export type EndpointParameters = {
261 | body?: unknown;
262 | query?: Record;
263 | header?: Record;
264 | path?: Record;
265 | };
266 |
267 | export type MutationMethod = "post" | "put" | "patch" | "delete";
268 | export type Method = "get" | "head" | "options" | MutationMethod;
269 |
270 | type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
271 |
272 | export type DefaultEndpoint = {
273 | parameters?: EndpointParameters | undefined;
274 | response: unknown;
275 | };
276 |
277 | export type Endpoint = {
278 | operationId: string;
279 | method: Method;
280 | path: string;
281 | requestFormat: RequestFormat;
282 | parameters?: TConfig["parameters"];
283 | meta: {
284 | alias: string;
285 | hasParameters: boolean;
286 | areParametersRequired: boolean;
287 | };
288 | response: TConfig["response"];
289 | };
290 |
291 | export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise;
292 |
293 | type RequiredKeys = {
294 | [P in keyof T]-?: undefined extends T[P] ? never : P;
295 | }[keyof T];
296 |
297 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
298 |
299 | //
300 |
301 | //
302 | export class ApiClient {
303 | baseUrl: string = "";
304 |
305 | constructor(public fetcher: Fetcher) {}
306 |
307 | setBaseUrl(baseUrl: string) {
308 | this.baseUrl = baseUrl;
309 | return this;
310 | }
311 |
312 | parseResponse = async (response: Response): Promise => {
313 | const contentType = response.headers.get("content-type");
314 | if (contentType?.includes("application/json")) {
315 | return response.json();
316 | }
317 | return response.text() as unknown as T;
318 | };
319 |
320 | //
321 | put(
322 | path: Path,
323 | ...params: MaybeOptionalArg
324 | ): Promise {
325 | return this.fetcher("put", this.baseUrl + path, params[0]).then((response) =>
326 | this.parseResponse(response),
327 | ) as Promise;
328 | }
329 | //
330 |
331 | //
332 | post(
333 | path: Path,
334 | ...params: MaybeOptionalArg
335 | ): Promise {
336 | return this.fetcher("post", this.baseUrl + path, params[0]).then((response) =>
337 | this.parseResponse(response),
338 | ) as Promise;
339 | }
340 | //
341 |
342 | //
343 | get(
344 | path: Path,
345 | ...params: MaybeOptionalArg
346 | ): Promise {
347 | return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
348 | this.parseResponse(response),
349 | ) as Promise;
350 | }
351 | //
352 |
353 | //
354 | delete(
355 | path: Path,
356 | ...params: MaybeOptionalArg
357 | ): Promise {
358 | return this.fetcher("delete", this.baseUrl + path, params[0]).then((response) =>
359 | this.parseResponse(response),
360 | ) as Promise;
361 | }
362 | //
363 |
364 | //
365 | /**
366 | * Generic request method with full type-safety for any endpoint
367 | */
368 | request<
369 | TMethod extends keyof EndpointByMethod,
370 | TPath extends keyof EndpointByMethod[TMethod],
371 | TEndpoint extends EndpointByMethod[TMethod][TPath],
372 | >(
373 | method: TMethod,
374 | path: TPath,
375 | ...params: MaybeOptionalArg
376 | ): Promise<
377 | Omit & {
378 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
379 | json: () => Promise;
380 | }
381 | > {
382 | return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
383 | }
384 | //
385 | }
386 |
387 | export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
388 | return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
389 | }
390 |
391 | /**
392 | Example usage:
393 | const api = createApiClient((method, url, params) =>
394 | fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
395 | );
396 | api.get("/users").then((users) => console.log(users));
397 | api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
398 | api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
399 | */
400 |
401 | // =12.20'}
65 | dev: false
66 |
67 | /valibot@0.8.0:
68 | resolution: {integrity: sha512-wQHXVkVFj6Z0R3297icGCp3UX8S66onuIg03ihJ8aVgMn8pMJvYSfmrKb+aIg7w/Aw08togrklaMSqDP2vKtIA==}
69 | dev: false
70 |
71 | /yup@1.2.0:
72 | resolution: {integrity: sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==}
73 | dependencies:
74 | property-expr: 2.0.5
75 | tiny-case: 1.0.3
76 | toposort: 2.0.2
77 | type-fest: 2.19.0
78 | dev: false
79 |
80 | /zod@3.21.4:
81 | resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
82 | dev: false
83 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tests/tanstack-query.generator.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test } from "vitest";
2 | import SwaggerParser from "@apidevtools/swagger-parser";
3 | import type { OpenAPIObject } from "openapi3-ts/oas31";
4 | import { mapOpenApiEndpoints } from "../src/map-openapi-endpoints.ts";
5 | import { generateTanstackQueryFile } from "../src/tanstack-query.generator.ts";
6 |
7 | describe("generator", () => {
8 | test("petstore", async ({ expect }) => {
9 | const openApiDoc = (await SwaggerParser.parse("./tests/samples/petstore.yaml")) as OpenAPIObject;
10 | expect(await generateTanstackQueryFile({
11 | ...mapOpenApiEndpoints(openApiDoc),
12 | relativeApiClientPath: "./api.client.ts"
13 | })).toMatchInlineSnapshot(`
14 | "import { queryOptions } from "@tanstack/react-query";
15 | import type { EndpointByMethod, ApiClient } from "./api.client.ts";
16 |
17 | type EndpointQueryKey = [
18 | TOptions & {
19 | _id: string;
20 | _infinite?: boolean;
21 | },
22 | ];
23 |
24 | const createQueryKey = (
25 | id: string,
26 | options?: TOptions,
27 | infinite?: boolean,
28 | ): [EndpointQueryKey[0]] => {
29 | const params: EndpointQueryKey[0] = { _id: id } as EndpointQueryKey[0];
30 | if (infinite) {
31 | params._infinite = infinite;
32 | }
33 | if (options?.body) {
34 | params.body = options.body;
35 | }
36 | if (options?.header) {
37 | params.header = options.header;
38 | }
39 | if (options?.path) {
40 | params.path = options.path;
41 | }
42 | if (options?.query) {
43 | params.query = options.query;
44 | }
45 | return [params];
46 | };
47 |
48 | //
49 | export type PutEndpoints = EndpointByMethod["put"];
50 | export type PostEndpoints = EndpointByMethod["post"];
51 | export type GetEndpoints = EndpointByMethod["get"];
52 | export type DeleteEndpoints = EndpointByMethod["delete"];
53 | //
54 |
55 | //
56 | export type EndpointParameters = {
57 | body?: unknown;
58 | query?: Record;
59 | header?: Record;
60 | path?: Record;
61 | };
62 |
63 | type RequiredKeys = {
64 | [P in keyof T]-?: undefined extends T[P] ? never : P;
65 | }[keyof T];
66 |
67 | type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
68 |
69 | //
70 |
71 | //
72 | export class TanstackQueryApiClient {
73 | constructor(public client: ApiClient) {}
74 |
75 | //
76 | put(
77 | path: Path,
78 | ...params: MaybeOptionalArg
79 | ) {
80 | const queryKey = createQueryKey(path, params[0]);
81 | const query = {
82 | /** type-only property if you need easy access to the endpoint params */
83 | "~endpoint": {} as TEndpoint,
84 | queryKey,
85 | queryOptions: queryOptions({
86 | queryFn: async ({ queryKey, signal }) => {
87 | const res = await this.client.put(path, {
88 | ...params,
89 | ...queryKey[0],
90 | signal,
91 | });
92 | return res as TEndpoint["response"];
93 | },
94 | queryKey: queryKey,
95 | }),
96 | mutationOptions: {
97 | mutationKey: queryKey,
98 | mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
99 | const res = await this.client.put(path, {
100 | ...params,
101 | ...queryKey[0],
102 | ...localOptions,
103 | });
104 | return res as TEndpoint["response"];
105 | },
106 | },
107 | };
108 |
109 | return query;
110 | }
111 | //
112 |
113 | //
114 | post(
115 | path: Path,
116 | ...params: MaybeOptionalArg
117 | ) {
118 | const queryKey = createQueryKey(path, params[0]);
119 | const query = {
120 | /** type-only property if you need easy access to the endpoint params */
121 | "~endpoint": {} as TEndpoint,
122 | queryKey,
123 | queryOptions: queryOptions({
124 | queryFn: async ({ queryKey, signal }) => {
125 | const res = await this.client.post(path, {
126 | ...params,
127 | ...queryKey[0],
128 | signal,
129 | });
130 | return res as TEndpoint["response"];
131 | },
132 | queryKey: queryKey,
133 | }),
134 | mutationOptions: {
135 | mutationKey: queryKey,
136 | mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
137 | const res = await this.client.post(path, {
138 | ...params,
139 | ...queryKey[0],
140 | ...localOptions,
141 | });
142 | return res as TEndpoint["response"];
143 | },
144 | },
145 | };
146 |
147 | return query;
148 | }
149 | //
150 |
151 | //
152 | get(
153 | path: Path,
154 | ...params: MaybeOptionalArg
155 | ) {
156 | const queryKey = createQueryKey(path, params[0]);
157 | const query = {
158 | /** type-only property if you need easy access to the endpoint params */
159 | "~endpoint": {} as TEndpoint,
160 | queryKey,
161 | queryOptions: queryOptions({
162 | queryFn: async ({ queryKey, signal }) => {
163 | const res = await this.client.get(path, {
164 | ...params,
165 | ...queryKey[0],
166 | signal,
167 | });
168 | return res as TEndpoint["response"];
169 | },
170 | queryKey: queryKey,
171 | }),
172 | mutationOptions: {
173 | mutationKey: queryKey,
174 | mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
175 | const res = await this.client.get(path, {
176 | ...params,
177 | ...queryKey[0],
178 | ...localOptions,
179 | });
180 | return res as TEndpoint["response"];
181 | },
182 | },
183 | };
184 |
185 | return query;
186 | }
187 | //
188 |
189 | //
190 | delete(
191 | path: Path,
192 | ...params: MaybeOptionalArg
193 | ) {
194 | const queryKey = createQueryKey(path, params[0]);
195 | const query = {
196 | /** type-only property if you need easy access to the endpoint params */
197 | "~endpoint": {} as TEndpoint,
198 | queryKey,
199 | queryOptions: queryOptions({
200 | queryFn: async ({ queryKey, signal }) => {
201 | const res = await this.client.delete(path, {
202 | ...params,
203 | ...queryKey[0],
204 | signal,
205 | });
206 | return res as TEndpoint["response"];
207 | },
208 | queryKey: queryKey,
209 | }),
210 | mutationOptions: {
211 | mutationKey: queryKey,
212 | mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
213 | const res = await this.client.delete(path, {
214 | ...params,
215 | ...queryKey[0],
216 | ...localOptions,
217 | });
218 | return res as TEndpoint["response"];
219 | },
220 | },
221 | };
222 |
223 | return query;
224 | }
225 | //
226 |
227 | //
228 | /**
229 | * Generic mutation method with full type-safety for any endpoint that doesnt require parameters to be passed initially
230 | */
231 | mutation<
232 | TMethod extends keyof EndpointByMethod,
233 | TPath extends keyof EndpointByMethod[TMethod],
234 | TEndpoint extends EndpointByMethod[TMethod][TPath],
235 | TSelection,
236 | >(
237 | method: TMethod,
238 | path: TPath,
239 | selectFn?: (
240 | res: Omit & {
241 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
242 | json: () => Promise;
243 | },
244 | ) => TSelection,
245 | ) {
246 | const mutationKey = [{ method, path }] as const;
247 | return {
248 | /** type-only property if you need easy access to the endpoint params */
249 | "~endpoint": {} as TEndpoint,
250 | mutationKey: mutationKey,
251 | mutationOptions: {
252 | mutationKey: mutationKey,
253 | mutationFn: async (params: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
254 | const response = await this.client.request(method, path, params);
255 | const res = selectFn ? selectFn(response) : response;
256 | return res as unknown extends TSelection ? typeof response : Awaited;
257 | },
258 | },
259 | };
260 | }
261 | //
262 | }
263 | "
264 | `);
265 | });
266 | });
267 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true
5 | },
6 | "include": [
7 | "src"
8 | ],
9 | "exclude": [
10 | "node_modules",
11 | "samples"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ESNext",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "lib": [
8 | "DOM",
9 | "ES2021"
10 | ],
11 | "resolveJsonModule": true,
12 | "allowImportingTsExtensions": true
13 | },
14 | "include": [
15 | "src",
16 | "tests"
17 | ],
18 | "exclude": [
19 | "node_modules",
20 | "samples"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/typed-openapi/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | clean: true,
5 | entryPoints: ["src/cli.ts", "src/index.ts", "src/node.export.ts"],
6 | outDir: "dist",
7 | dts: true,
8 | format: ["esm"],
9 | });
10 |
--------------------------------------------------------------------------------
/packages/typed-openapi/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from "vitest/config";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | test: {
8 | hideSkippedTests: true,
9 | snapshotFormat: {
10 | escapeString: false,
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/packages/web/fs.shim.ts:
--------------------------------------------------------------------------------
1 | export const statSync = (path: string) => {
2 | return {
3 | isFile: () => true,
4 | };
5 | };
6 |
7 | export const readdirSync = (path: string) => {
8 | return [];
9 | };
10 |
11 | export const fs = {
12 | readFileSync: (path: string) => {
13 | return "";
14 | },
15 | };
16 |
17 | export default fs;
18 |
--------------------------------------------------------------------------------
/packages/web/get-ts-declarations.ts:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from "node:fs/promises";
2 | import path from "node:path";
3 |
4 | import { safeJSONParse } from "pastable";
5 | import { rollup } from "rollup";
6 | import dts from "rollup-plugin-dts";
7 | import type { PackageJson } from "type-fest";
8 |
9 | import { OutputRuntime } from "typed-openapi";
10 |
11 | const getDeps = (pkg: PackageJson) =>
12 | Object.keys(pkg.dependencies ?? {}).concat(
13 | // Object.keys(pkg.devDependencies ?? {}),
14 | Object.keys(pkg.peerDependencies ?? {}),
15 | );
16 |
17 | const getPkg = async (name: string) =>
18 | safeJSONParse(await readFile(`./node_modules/${name}/package.json`, "utf8"));
19 |
20 | const getTypesDeclaration = async (name: string) => {
21 | // console.log(`Parsing "${name}" package.json...`);
22 | const pkg = await getPkg(name);
23 | const types = pkg.types ?? pkg.typings ?? "index.d.ts";
24 | const input = path.resolve("./node_modules/", name, types);
25 | if (!input) return;
26 |
27 | console.log(`Bundling "${name}"...`);
28 | const bundle = await rollup({
29 | input,
30 | plugins: [dts({ respectExternal: true })],
31 | external: (id) => getDeps(pkg).includes(id),
32 | });
33 | const result = await bundle.generate({});
34 |
35 | return result.output[0].code;
36 | };
37 |
38 | // const runtimes: OutputRuntime[] = ["zod", "arktype", "typebox", "valibot", "yup", "io-ts"];
39 | const runtimes: OutputRuntime[] = ["yup"];
40 |
41 | const getDeclarationFiles = async () => {
42 | const declarations = await Promise.all(
43 | runtimes.map(async (runtime) => ({
44 | name: runtime,
45 | code: await getTypesDeclaration(runtime === "typebox" ? "@sinclair/typebox" : runtime),
46 | })),
47 | );
48 |
49 | return declarations.filter((declaration) => Boolean(declaration.code));
50 | };
51 |
52 | console.log("Starting to bundle runtimes...");
53 | const declarations = await getDeclarationFiles();
54 | await Promise.all(
55 | declarations.map((result) => {
56 | console.log("Writing declaration file for", result.name);
57 | return writeFile(`./declarations/${result.name}.d.ts`, result.code!, "utf8");
58 | }),
59 | );
60 |
61 | console.log("Done!");
62 |
--------------------------------------------------------------------------------
/packages/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | typed-openapi
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/packages/web/module.shim.ts:
--------------------------------------------------------------------------------
1 | // Empty implementation for Rollup alias
2 | // eslint-disable-next-line @typescript-eslint/no-empty-function
3 | export const createRequire = () => {};
4 |
--------------------------------------------------------------------------------
/packages/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typed-openapi-web",
3 | "private": true,
4 | "type": "module",
5 | "version": "0.0.1",
6 | "scripts": {
7 | "prebuild": "panda && panda codegen && mkdir -p declarations && pnpm gen:ts-declarations",
8 | "gen:ts-declarations": "tsx ./get-ts-declarations.ts",
9 | "dev": "vite",
10 | "build": "pnpm prebuild && vite build",
11 | "preview": "vite preview",
12 | "test": "vitest"
13 | },
14 | "dependencies": {
15 | "@ark-ui/react": "0.10.0",
16 | "@fontsource/inter": "5.0.5",
17 | "@monaco-editor/react": "4.5.1",
18 | "@pandacss/dev": "0.9.0",
19 | "@xstate/react": "3.2.2",
20 | "lz-string": "1.5.0",
21 | "monaco-editor": "0.40.0",
22 | "pastable": "2.2.0",
23 | "prettier": "2.8.4",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "react-icons": "4.10.1",
27 | "react-resizable-panels": "0.0.53",
28 | "ts-patch": "3.0.2",
29 | "typed-openapi": "workspace:*",
30 | "xstate": "4.38.1",
31 | "yaml": "2.3.1"
32 | },
33 | "devDependencies": {
34 | "@esbuild-plugins/node-globals-polyfill": "0.2.3",
35 | "@pandacss/preset-base": "0.7.0",
36 | "@pandacss/preset-panda": "0.7.0",
37 | "@park-ui/presets": "0.2.0",
38 | "@sinclair/typebox": "0.30.4",
39 | "@types/node": "20.4.5",
40 | "@types/prettier": "2.7.3",
41 | "@types/react": "18.2.15",
42 | "@types/react-dom": "18.2.7",
43 | "@vitejs/plugin-react-swc": "3.3.2",
44 | "arktype": "1.0.18-alpha",
45 | "io-ts": "2.2.20",
46 | "os-browserify": "0.3.0",
47 | "path-browserify": "1.0.1",
48 | "rollup": "3.26.3",
49 | "rollup-plugin-dts": "5.3.0",
50 | "tsup": "7.1.0",
51 | "tsx": "4.19.4",
52 | "type-fest": "3.13.1",
53 | "typescript": "5.1.6",
54 | "util": "0.12.5",
55 | "valibot": "0.37.0",
56 | "vite": "4.4.4",
57 | "vite-plugin-react-click-to-component": "2.0.0",
58 | "vite-tsconfig-paths": "4.2.0",
59 | "vitest": "0.33.0",
60 | "yup": "1.2.0",
61 | "zod": "3.21.4"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/web/panda.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@pandacss/dev";
2 | import { preset as basePreset } from "@pandacss/preset-base";
3 | import { preset as pandaPreset } from "@pandacss/preset-panda";
4 | import { themePreset } from "./theme/preset";
5 |
6 | export default defineConfig({
7 | preflight: true,
8 | include: ["./src/**/*.{tsx,jsx}", "./pages/**/*.{jsx,tsx}"],
9 | exclude: [],
10 | jsxFramework: "react",
11 | presets: [pandaPreset as any, themePreset, "@park-ui/presets"],
12 | conditions: {
13 | // next-themes
14 | dark: '.dark &, [data-theme="dark"] &',
15 | light: ".light &",
16 | // react-resizable-panels
17 | resizeHandleActive: "[data-resize-handle-active] &",
18 | panelHorizontalActive: '[data-panel-group-direction="horizontal"] &',
19 | panelVerticalActive: '[data-panel-group-direction="vertical"] &',
20 | },
21 | utilities: {
22 | boxSize: {
23 | values: basePreset.utilities?.width?.values,
24 | transform: (value) => {
25 | return {
26 | width: value,
27 | height: value,
28 | };
29 | },
30 | },
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/packages/web/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: { "@pandacss/dev/postcss": {} },
3 | };
4 |
--------------------------------------------------------------------------------
/packages/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/astahmer/typed-openapi/851ae9d0d021963d06c2a10f0e546c73194fc1e0/packages/web/public/favicon.ico
--------------------------------------------------------------------------------
/packages/web/public/github-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/Playground.machine.ts:
--------------------------------------------------------------------------------
1 | import type { Monaco } from "@monaco-editor/react";
2 | import type { editor } from "monaco-editor";
3 | import { assign, createMachine } from "xstate";
4 | import { choose } from "xstate/lib/actions";
5 | import { safeJSONParse } from "pastable/utils";
6 | import { generateFile, mapOpenApiEndpoints, type OutputRuntime } from "typed-openapi";
7 | import { parse } from "yaml";
8 |
9 | // @ts-expect-error
10 | import { default as petstoreYaml } from "./petstore.yaml?raw";
11 | import { UrlSaver } from "./url-saver";
12 | import { fromPromise } from "xstate/lib/behaviors";
13 | import { prettify } from "./format";
14 |
15 | const urlSaver = new UrlSaver();
16 | const initialInputList = { "petstore.yaml": urlSaver.getValue("input") || petstoreYaml };
17 | const initialOutputList = { "petstore.client.ts": "" };
18 |
19 | type PlaygroundContext = {
20 | monaco: Monaco | null;
21 | inputEditor: editor.IStandaloneCodeEditor | null;
22 | outputEditor: editor.IStandaloneCodeEditor | null;
23 | inputList: Record;
24 | selectedInput: string;
25 | outputList: Record;
26 | selectedOutput: string;
27 | decorations: string[];
28 | //
29 | runtime: OutputRuntime;
30 | };
31 |
32 | type PlaygroundEvent =
33 | | {
34 | type: "Editor Loaded";
35 | editor: editor.IStandaloneCodeEditor;
36 | monaco: Monaco;
37 | kind: "input" | "output";
38 | }
39 | | { type: "Select input tab"; name: string }
40 | | { type: "Select output tab"; name: string }
41 | | { type: "Update runtime"; runtime: OutputRuntime }
42 | | { type: "Update input"; value: string };
43 |
44 | const initialContext: PlaygroundContext = {
45 | monaco: null,
46 | inputEditor: null,
47 | outputEditor: null,
48 | inputList: initialInputList,
49 | selectedInput: Object.keys(initialInputList)[0],
50 | outputList: initialOutputList,
51 | selectedOutput: Object.keys(initialOutputList)[0],
52 | decorations: [],
53 | //
54 | runtime: "none",
55 | };
56 |
57 | // @ts-ignore
58 | globalThis.__dirname = "/";
59 |
60 | export const playgroundMachine = createMachine(
61 | {
62 | predictableActionArguments: true,
63 | preserveActionOrder: true,
64 | id: "playground",
65 | tsTypes: {} as import("./Playground.machine.typegen").Typegen0,
66 | schema: {
67 | context: {} as PlaygroundContext,
68 | events: {} as PlaygroundEvent,
69 | },
70 | context: initialContext,
71 | initial: "loading",
72 | states: {
73 | loading: {
74 | on: {
75 | "Editor Loaded": [
76 | {
77 | cond: "willBeReady",
78 | target: "ready",
79 | actions: ["assignEditorRef", "updateOutput"],
80 | },
81 | { actions: "assignEditorRef" },
82 | ],
83 | },
84 | },
85 | ready: {
86 | initial: "Playing",
87 | entry: ["updateInput"],
88 | states: {
89 | Playing: {
90 | on: {
91 | "Select input tab": {
92 | actions: ["selectInputTab", "updateInput"],
93 | },
94 | "Select output tab": { actions: ["selectOutputTab"] },
95 | "Update input": { actions: ["updateInput"] },
96 | },
97 | },
98 | },
99 | },
100 | },
101 | on: {
102 | "Update runtime": { actions: ["updateRuntime", "updateOutput"] },
103 | },
104 | },
105 | {
106 | actions: {
107 | assignEditorRef: assign((ctx, event) => {
108 | if (event.kind === "input") {
109 | return { ...ctx, inputEditor: event.editor, monaco: event.monaco };
110 | }
111 |
112 | return { ...ctx, outputEditor: event.editor, monaco: event.monaco };
113 | }),
114 | selectInputTab: assign((ctx, event) => {
115 | return { ...ctx, selectedInput: event.name };
116 | }),
117 | selectOutputTab: assign((ctx, event) => {
118 | return { ...ctx, selectedOutput: event.name };
119 | }),
120 | updateSelectedInput: assign((ctx, event) => {
121 | if (event.type !== "Update input") return ctx;
122 |
123 | const { inputList, selectedInput } = ctx;
124 | if (inputList[selectedInput]) {
125 | inputList[selectedInput] = event.value;
126 | }
127 | return { ...ctx, inputList };
128 | }),
129 | updateUrl(context, event, meta) {
130 | urlSaver.setValue("input", context.inputList[context.selectedInput]);
131 | },
132 | updateInput: choose([{ actions: ["updateSelectedInput", "updateUrl", "updateOutput"] }]),
133 | updateRuntime: assign({ runtime: (_, event) => event.runtime }),
134 | updateOutput: assign((ctx, event) => {
135 | const now = new Date();
136 | console.log("Generating...");
137 |
138 | const input = (event.type === "Update input" ? event.value : ctx.inputList[ctx.selectedInput]) ?? "";
139 | const openApiDoc = input.startsWith("{") ? safeJSONParse(input) : safeYAMLParse(input);
140 | console.log({ input, openApiDoc });
141 | if (!openApiDoc) {
142 | // toasts.error("Error while parsing OpenAPI document");
143 | return ctx;
144 | }
145 |
146 | const context = mapOpenApiEndpoints(openApiDoc);
147 | console.log(`Found ${context.endpointList.length} endpoints`);
148 |
149 | const content = prettify(generateFile({ ...context, runtime: ctx.runtime }));
150 | const outputList = {
151 | ["petstore.client.ts"]: content,
152 | };
153 |
154 | console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
155 |
156 | return {
157 | ...ctx,
158 | outputList,
159 | };
160 | }),
161 | },
162 | guards: {
163 | willBeReady: (ctx) => {
164 | return Boolean(ctx.inputEditor || ctx.outputEditor);
165 | },
166 | },
167 | },
168 | );
169 |
170 | const safeYAMLParse = (value: string): string | null => {
171 | try {
172 | return parse(value);
173 | } catch {
174 | return null;
175 | }
176 | };
177 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/Playground.machine.typegen.ts:
--------------------------------------------------------------------------------
1 | // This file was automatically generated. Edits will be overwritten
2 |
3 | export interface Typegen0 {
4 | "@@xstate/typegen": true;
5 | internalEvents: {
6 | "xstate.init": { type: "xstate.init" };
7 | };
8 | invokeSrcNameMap: {};
9 | missingImplementations: {
10 | actions: never;
11 | delays: never;
12 | guards: never;
13 | services: never;
14 | };
15 | eventsCausingActions: {
16 | assignEditorRef: "Editor Loaded";
17 | selectInputTab: "Select input tab";
18 | selectOutputTab: "Select output tab";
19 | updateInput: "Editor Loaded" | "Select input tab" | "Update input";
20 | updateOutput: "Editor Loaded" | "Select input tab" | "Update input" | "Update runtime";
21 | updateRuntime: "Update runtime";
22 | updateSelectedInput: "Editor Loaded" | "Select input tab" | "Update input";
23 | updateUrl: "Editor Loaded" | "Select input tab" | "Update input";
24 | };
25 | eventsCausingDelays: {};
26 | eventsCausingGuards: {
27 | willBeReady: "Editor Loaded";
28 | };
29 | eventsCausingServices: {};
30 | matchesStates: "loading" | "ready" | "ready.Playing" | { ready?: "Playing" };
31 | tags: never;
32 | }
33 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/Playground.tsx:
--------------------------------------------------------------------------------
1 | import Editor from "@monaco-editor/react";
2 | import { useActor } from "@xstate/react";
3 | import { Panel, PanelGroup } from "react-resizable-panels";
4 | import { css } from "panda/css";
5 | import { Flex, styled } from "panda/jsx";
6 | import { usePlaygroundContext } from "./PlaygroundMachineProvider";
7 | import { ResizeHandle } from "./ResizeHandle";
8 | import { useTheme } from "../vite-themes/provider";
9 |
10 | // @ts-ignore
11 | import ZodDeclaration from "../../declarations/zod.d.ts?raw";
12 | // @ts-ignore
13 | import ArktypeDeclaration from "../../declarations/arktype.d.ts?raw";
14 | // @ts-ignore
15 | import ValibotDeclaration from "../../declarations/valibot.d.ts?raw";
16 | // @ts-ignore
17 | import TypeboxDeclaration from "../../declarations/typebox.d.ts?raw";
18 | // @ts-ignore
19 | import IoTsDeclaration from "../../declarations/io-ts.d.ts?raw";
20 | // @ts-ignore
21 | import YupDeclaration from "../../declarations/yup.d.ts?raw";
22 |
23 | export const Playground = () => {
24 | const service = usePlaygroundContext();
25 | const [state, send] = useActor(service);
26 | console.log(state.value, state.context);
27 |
28 | const theme = useTheme();
29 | const colorMode = theme.resolvedTheme;
30 |
31 | return (
32 |
33 |
34 |
35 |
36 | {Object.entries(state.context.inputList).map(([fileName]) => (
37 | send({ type: "Select input tab", name: fileName })}
41 | fontSize="sm"
42 | fontWeight="medium"
43 | borderRadius="0"
44 | p="2"
45 | color="cyan.500"
46 | opacity={0.8}
47 | transition="color opacity 150ms ease"
48 | bg="none"
49 | cursor="pointer"
50 | borderBottom="solid 1px transparent"
51 | data-active={state.context.selectedInput === fileName ? "" : undefined}
52 | _active={{
53 | color: "cyan.600",
54 | opacity: 1,
55 | borderBottom: "solid 1px token(colors.cyan.600, red)",
56 | }}
57 | _hover={{ color: "cyan.600" }}
58 | >
59 | {fileName}
60 |
61 | ))}
62 |
63 |
71 | {
77 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
78 | target: monaco.languages.typescript.ScriptTarget.Latest,
79 | allowNonTsExtensions: true,
80 | moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
81 | module: monaco.languages.typescript.ModuleKind.CommonJS,
82 | noEmit: true,
83 | esModuleInterop: true,
84 | jsx: monaco.languages.typescript.JsxEmit.Preserve,
85 | // reactNamespace: "React",
86 | allowJs: true,
87 | // typeRoots: ["node_modules/@types"],
88 | });
89 |
90 | const getDtsPath = (name: string) => `file:///node_modules/${name}/index.d.ts`;
91 |
92 | monaco.languages.typescript.typescriptDefaults.addExtraLib(ZodDeclaration, getDtsPath("zod"));
93 | monaco.languages.typescript.typescriptDefaults.addExtraLib(ArktypeDeclaration, getDtsPath("arktype"));
94 | monaco.languages.typescript.typescriptDefaults.addExtraLib(ValibotDeclaration, getDtsPath("valibot"));
95 | monaco.languages.typescript.typescriptDefaults.addExtraLib(
96 | TypeboxDeclaration,
97 | getDtsPath("@sinclair/typebox"),
98 | );
99 | monaco.languages.typescript.typescriptDefaults.addExtraLib(IoTsDeclaration, getDtsPath("io-ts"));
100 |
101 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
102 | noSemanticValidation: true,
103 | });
104 | }}
105 | onMount={(editor, monaco) => {
106 | console.log("editor mounted", editor, monaco);
107 | send({ type: "Editor Loaded", editor, monaco, kind: "input" });
108 | // editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
109 | // send({ type: "Save" });
110 | // });
111 | }}
112 | onChange={(content) => send({ type: "Update input", value: content ?? "" })}
113 | />
114 |
115 |
116 |
117 |
118 |
119 | {Object.entries(state.context.outputList).map(([fileName]) => (
120 | send({ type: "Select output tab", name: fileName })}
124 | fontSize="sm"
125 | fontWeight="medium"
126 | borderRadius="0"
127 | p="2"
128 | color="cyan.500"
129 | opacity={0.8}
130 | transition="color opacity 150ms ease"
131 | bg="none"
132 | cursor="pointer"
133 | borderBottom="solid 1px transparent"
134 | data-active={state.context.selectedOutput === fileName ? "" : undefined}
135 | _active={{
136 | color: "cyan.600",
137 | opacity: 1,
138 | borderBottom: "solid 1px token(colors.cyan.600, red)",
139 | }}
140 | _hover={{ color: "cyan.600" }}
141 | >
142 | {fileName}
143 |
144 | ))}
145 |
146 |
154 | {
161 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
162 | noSemanticValidation: true,
163 | });
164 | }}
165 | onMount={(editor, monaco) => {
166 | send({ type: "Editor Loaded", editor, monaco, kind: "output" });
167 | }}
168 | />
169 |
170 |
171 |
172 |
173 | );
174 | };
175 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/PlaygroundMachineProvider.ts:
--------------------------------------------------------------------------------
1 | import { createContextWithHook } from "pastable/react";
2 | import { InterpreterFrom } from "xstate";
3 | import { playgroundMachine } from "./Playground.machine";
4 |
5 | export const [PlaygroundMachineProvider, usePlaygroundContext] =
6 | createContextWithHook>("PlaygroundMachineContext");
7 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/PlaygroundWithMachine.tsx:
--------------------------------------------------------------------------------
1 | import { useInterpret } from "@xstate/react";
2 | import { ReactNode } from "react";
3 | import { InterpreterFrom } from "xstate";
4 | import { runIfFn } from "../run-if-fn";
5 | import { Playground } from "./Playground";
6 | import { playgroundMachine } from "./Playground.machine";
7 | import { PlaygroundMachineProvider } from "./PlaygroundMachineProvider";
8 |
9 | export const PlaygroundWithMachine = ({
10 | children,
11 | }: {
12 | children?: ReactNode | ((service: InterpreterFrom) => ReactNode);
13 | }) => {
14 | const service = useInterpret(playgroundMachine);
15 |
16 | return (
17 |
18 | {runIfFn(children, service)}
19 |
20 |
21 | );
22 | };
23 |
24 | export default PlaygroundWithMachine;
25 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/ResizeHandle.tsx:
--------------------------------------------------------------------------------
1 | import { PanelResizeHandle } from "react-resizable-panels";
2 | import { css } from "panda/css";
3 | import { styled } from "panda/jsx";
4 |
5 | // adapted from https://github.com/bvaughn/react-resizable-panels/blob/820f48f263407b6b78feecf975a6914c417107e6/packages/react-resizable-panels-website/src/components/ResizeHandle.tsx
6 | export function ResizeHandle({ className = "", id }: { className?: string; id?: string }) {
7 | return (
8 |
22 |
30 |
38 |
46 |
47 |
48 | );
49 | }
50 |
51 | export type IconType =
52 | | "chevron-down"
53 | | "close"
54 | | "css"
55 | | "files"
56 | | "horizontal-collapse"
57 | | "horizontal-expand"
58 | | "html"
59 | | "markdown"
60 | | "resize-horizontal"
61 | | "resize-vertical"
62 | | "search"
63 | | "typescript";
64 |
65 | function Icon({ className = "", type }: { className?: string; type: IconType }) {
66 | let path = "";
67 | switch (type) {
68 | case "chevron-down": {
69 | path = "M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z";
70 | break;
71 | }
72 |
73 | case "close": {
74 | path =
75 | "M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z";
76 | break;
77 | }
78 |
79 | case "css": {
80 | path =
81 | "M5,3L4.35,6.34H17.94L17.5,8.5H3.92L3.26,11.83H16.85L16.09,15.64L10.61,17.45L5.86,15.64L6.19,14H2.85L2.06,18L9.91,21L18.96,18L20.16,11.97L20.4,10.76L21.94,3H5Z";
82 | break;
83 | }
84 |
85 | case "files": {
86 | path =
87 | "M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z";
88 | break;
89 | }
90 |
91 | case "horizontal-collapse": {
92 | path = "M13,20V4H15.03V20H13M10,20V4H12.03V20H10M5,8L9.03,12L5,16V13H2V11H5V8M20,16L16,12L20,8V11H23V13H20V16Z";
93 | break;
94 | }
95 |
96 | case "horizontal-expand": {
97 | path = "M9,11H15V8L19,12L15,16V13H9V16L5,12L9,8V11M2,20V4H4V20H2M20,20V4H22V20H20Z";
98 | break;
99 | }
100 |
101 | case "html": {
102 | path =
103 | "M12,17.56L16.07,16.43L16.62,10.33H9.38L9.2,8.3H16.8L17,6.31H7L7.56,12.32H14.45L14.22,14.9L12,15.5L9.78,14.9L9.64,13.24H7.64L7.93,16.43L12,17.56M4.07,3H19.93L18.5,19.2L12,21L5.5,19.2L4.07,3Z";
104 | break;
105 | }
106 |
107 | case "markdown": {
108 | path =
109 | "M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6 3.44 6H20.56C21.35 6 22 6.63 22 7.41V16.59C22 17.37 21.35 18 20.56 18M6.81 15.19V11.53L8.73 13.88L10.65 11.53V15.19H12.58V8.81H10.65L8.73 11.16L6.81 8.81H4.89V15.19H6.81M19.69 12H17.77V8.81H15.85V12H13.92L16.81 15.28L19.69 12Z";
110 | break;
111 | }
112 |
113 | case "resize-horizontal": {
114 | path = "M18,16V13H15V22H13V2H15V11H18V8L22,12L18,16M2,12L6,16V13H9V22H11V2H9V11H6V8L2,12Z";
115 | break;
116 | }
117 |
118 | case "resize-vertical": {
119 | path = "M8,18H11V15H2V13H22V15H13V18H16L12,22L8,18M12,2L8,6H11V9H2V11H22V9H13V6H16L12,2Z";
120 | break;
121 | }
122 |
123 | case "search": {
124 | path =
125 | "M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z";
126 | break;
127 | }
128 |
129 | case "typescript": {
130 | path =
131 | "M3,3H21V21H3V3M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86M13,11.25H8V12.75H9.5V20H11.25V12.75H13V11.25Z";
132 | break;
133 | }
134 | }
135 |
136 | return (
137 |
140 | );
141 | }
142 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/format.ts:
--------------------------------------------------------------------------------
1 | import prettier, { type Options } from "prettier";
2 | import parserTypescript from "prettier/parser-typescript";
3 |
4 | /** @see https://github.dev/stephenh/ts-poet/blob/5ea0dbb3c9f1f4b0ee51a54abb2d758102eda4a2/src/Code.ts#L231 */
5 | function maybePretty(input: string, options?: Options | null): string {
6 | try {
7 | return prettier.format(input, {
8 | parser: "typescript",
9 | plugins: [parserTypescript],
10 | ...options,
11 | });
12 | } catch (err) {
13 | console.warn("Failed to format code");
14 | console.warn(err);
15 | return input; // assume it's invalid syntax and ignore
16 | }
17 | }
18 |
19 | export const prettify = (str: string, options?: Options | null) =>
20 | maybePretty(str, { printWidth: 120, trailingComma: "all", ...options });
21 |
--------------------------------------------------------------------------------
/packages/web/src/Playground/url-saver.ts:
--------------------------------------------------------------------------------
1 | import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string'
2 |
3 | // adapted from https://github.dev/dsherret/ts-ast-viewer/blob/c71e238123d972bae889b3829e23b44f39d8d5c2/site/src/utils/UrlSaver.ts#L1-L29
4 | function getDecompressedStringFromUrl(name: string) {
5 | if (typeof window === 'undefined') return
6 |
7 | const search = new URLSearchParams(window.location.search)
8 | const code = (search.get(name) ?? '').trim()
9 | return decompressFromEncodedURIComponent(code) ?? '' // will be null on error
10 | }
11 |
12 | function updateUrlWithCompressedString(name: string, value: string) {
13 | if (value.length === 0) {
14 | updateUrlWithParam(name, '')
15 | } else {
16 | const compressed = compressToEncodedURIComponent(value)
17 | const url = new URL(window.location.href)
18 | url.searchParams.set(name, compressed)
19 |
20 | // completely arbitrary limit of characters, but it appears to not work anymore around that
21 | if (url.toString().length >= 14_500) {
22 | throw new Error('The compressed string is too large to be stored in the URL.')
23 | } else {
24 | updateUrlWithParam(name, compressed)
25 | }
26 | }
27 | }
28 |
29 | function updateUrlWithParam(name: string, value: string | number) {
30 | if (typeof window === 'undefined') return
31 |
32 | const url = new URL(window.location.href)
33 | url.searchParams.set(name, String(value))
34 | window.history.replaceState(undefined, '', url)
35 | }
36 |
37 | const resetUrl = () => {
38 | if (typeof window === 'undefined') return
39 |
40 | window.history.replaceState(undefined, '', window.location.origin + window.location.pathname)
41 | }
42 |
43 | const deletingParamInUrl = (name: string) => {
44 | if (typeof window === 'undefined') return
45 |
46 | const url = new URL(window.location.href)
47 | url.searchParams.delete(name)
48 | window.history.replaceState(undefined, '', url)
49 | }
50 |
51 | export class UrlSaver {
52 | getValue(name: string) {
53 | return getDecompressedStringFromUrl(name)
54 | }
55 |
56 | setValue(name: string, value: string) {
57 | updateUrlWithCompressedString(name, value)
58 | }
59 |
60 | reset(name: string) {
61 | deletingParamInUrl(name)
62 | }
63 |
64 | resetAll() {
65 | resetUrl()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/web/src/components/button.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithoutRef } from "react";
2 | import { styled } from "panda/jsx";
3 | import { button, type ButtonVariantProps } from "panda/recipes";
4 |
5 | export type ButtonProps = ButtonVariantProps & ComponentPropsWithoutRef<"button">;
6 | export const Button = styled("button", button);
7 |
--------------------------------------------------------------------------------
/packages/web/src/components/color-mode-switch.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from "panda/css";
2 | import { ComponentPropsWithoutRef, useEffect, useState } from "react";
3 | import { useTheme } from "../vite-themes/provider";
4 | import { styled } from "panda/jsx";
5 | import { IconButton } from "./icon-button";
6 |
7 | export const ColorModeSwitch = () => {
8 | const [mounted, setMounted] = useState(false);
9 | const theme = useTheme();
10 |
11 | useEffect(() => {
12 | setMounted(true);
13 | }, []);
14 |
15 | const { setTheme, resolvedTheme } = theme;
16 |
17 | if (!mounted) {
18 | return null;
19 | }
20 |
21 | const isDark = resolvedTheme === "dark";
22 |
23 | const toggleTheme = () => setTheme(isDark ? "light" : "dark");
24 |
25 | const IconToUse = isDark ? Moon : Sun;
26 | const iconText = isDark ? "Dark" : "Light";
27 |
28 | return (
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | function Moon(props: ComponentPropsWithoutRef<"svg">) {
36 | return (
37 |
46 | );
47 | }
48 |
49 | function Sun(props: ComponentPropsWithoutRef<"svg">) {
50 | return (
51 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/web/src/components/github-icon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef } from "react";
2 |
3 | export const GithubIcon = (props: ComponentPropsWithoutRef<"svg">) => (
4 |
7 | );
8 |
--------------------------------------------------------------------------------
/packages/web/src/components/icon-button.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from "panda/css";
2 | import { SystemStyleObject } from "panda/types";
3 |
4 | interface IconButtonProps extends React.ButtonHTMLAttributes {
5 | children: React.ReactNode;
6 | css?: SystemStyleObject;
7 | }
8 |
9 | export function IconButton(props: IconButtonProps) {
10 | const { children, className, css: cssProp, ...rest } = props;
11 | return (
12 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/web/src/components/select-demo.tsx:
--------------------------------------------------------------------------------
1 | import { Portal } from "@ark-ui/react";
2 | import { FiChevronDown } from "react-icons/fi";
3 | import { Button } from "./button";
4 | import { Select, SelectContent, SelectOption, SelectPositioner, SelectTrigger, type SelectProps } from "./select";
5 | import { HStack } from "panda/jsx";
6 |
7 | export const SelectRuntime = (props: SelectProps) => {
8 | return (
9 |
50 | );
51 | };
52 |
53 | const SelectIcon = (props: { isOpen: boolean }) => {
54 | const iconStyles = {
55 | transform: props.isOpen ? "rotate(-180deg)" : undefined,
56 | transition: "transform 0.2s",
57 | transformOrigin: "center",
58 | fontSize: "18px",
59 | };
60 | return ;
61 | };
62 |
--------------------------------------------------------------------------------
/packages/web/src/components/select.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | SelectPositioner as ArkSelectPositioner,
3 | type SelectPositionerProps as ArkSelectPositionerProps,
4 | } from "@ark-ui/react/select";
5 | import { styled } from "panda/jsx";
6 | import { select } from "panda/recipes";
7 |
8 | export * from "@ark-ui/react/select";
9 |
10 | export type SelectPositionerProps = ArkSelectPositionerProps & React.ComponentProps<"span">;
11 | export const SelectPositioner = styled(ArkSelectPositioner, select);
12 |
--------------------------------------------------------------------------------
/packages/web/src/components/twitter-icon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef } from 'react'
2 |
3 | export const TwitterIcon = (props: ComponentPropsWithoutRef<'svg'>) => (
4 |
7 | )
8 |
--------------------------------------------------------------------------------
/packages/web/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./styles.css";
4 | import { Home } from "./pages/Home";
5 |
6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
7 |
8 |
9 | ,
10 | );
11 |
--------------------------------------------------------------------------------
/packages/web/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, HStack, Stack } from "panda/jsx";
2 | import { styled } from "panda/jsx";
3 | import PlaygroundWithMachine from "../Playground/PlaygroundWithMachine";
4 |
5 | import "../styles.css";
6 | import "@fontsource/inter"; // Defaults to weight 400
7 | import { ThemeProvider } from "../vite-themes/provider";
8 | import { ColorModeSwitch } from "../components/color-mode-switch";
9 | import { GithubIcon } from "../components/github-icon";
10 | import { IconButton } from "../components/icon-button";
11 | import { SelectRuntime } from "../components/select-demo";
12 | import { OutputRuntime } from "typed-openapi";
13 | import { TwitterIcon } from "../components/twitter-icon";
14 |
15 | export const Home = () => {
16 | return (
17 |
18 |
26 |
27 |
28 | {(service) => (
29 |
30 |
31 | typed-openapi
32 |
33 |
34 |
37 | service.send({ type: "Update runtime", runtime: (option?.value ?? "none") as OutputRuntime })
38 | }
39 | />
40 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
58 | openapi-zod-client
59 |
60 |
61 |
62 |
63 | )}
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/packages/web/src/run-if-fn.ts:
--------------------------------------------------------------------------------
1 | export type AnyFunction = (...args: Arg[]) => ReturnValue;
2 |
3 | const isFunction = (value: unknown): value is T => typeof value === "function";
4 |
5 | export const runIfFn = (
6 | valueOrFn: MaybeReturnValue | ((...fnArgs: FunctionArgs[]) => MaybeReturnValue),
7 | ...args: FunctionArgs[]
8 | ) =>
9 | isFunction>(valueOrFn)
10 | ? valueOrFn(...args)
11 | : (valueOrFn as unknown as MaybeReturnValue);
12 |
--------------------------------------------------------------------------------
/packages/web/src/styles.css:
--------------------------------------------------------------------------------
1 | @layer reset, base, tokens, recipes, utilities;
2 |
3 | :root {
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
5 | "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
6 | color: #333;
7 | font-size: 16px;
8 | min-width: 360px;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/web/src/vite-themes/provider.tsx:
--------------------------------------------------------------------------------
1 | /** Adapted from https://github.com/pacocoursey/next-themes/blob/a385b8d865bbb317ff73a5b6c1319ae566f7d6f1/src/index.tsx */
2 |
3 | import { createContext, Fragment, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
4 | import type { ThemeProviderProps, UseThemeProps } from "./vite-themes-types";
5 |
6 | const colorSchemes = ["light", "dark"];
7 | const MEDIA = "(prefers-color-scheme: dark)";
8 | const isServer = typeof window === "undefined";
9 | const ThemeContext = createContext(undefined);
10 | const defaultContext: UseThemeProps = { setTheme: (_) => {}, themes: [] };
11 |
12 | export const useTheme = () => useContext(ThemeContext) ?? defaultContext;
13 |
14 | export const ThemeProvider: React.FC = (props) => {
15 | const context = useContext(ThemeContext);
16 |
17 | // Ignore nested context providers, just passthrough children
18 | if (context) return {props.children as any};
19 | return ;
20 | };
21 |
22 | const defaultThemes = ["light", "dark"];
23 |
24 | const Theme: React.FC = ({
25 | forcedTheme,
26 | disableTransitionOnChange = false,
27 | enableSystem = true,
28 | enableColorScheme = true,
29 | storageKey = "theme",
30 | themes = defaultThemes,
31 | defaultTheme = enableSystem ? "system" : "light",
32 | attribute = "data-theme",
33 | value,
34 | children,
35 | nonce,
36 | }) => {
37 | const [theme, setThemeState] = useState(() => getTheme(storageKey, defaultTheme));
38 | const [resolvedTheme, setResolvedTheme] = useState(() => getTheme(storageKey));
39 | const attrs = !value ? themes : Object.values(value);
40 |
41 | const applyTheme = useCallback((theme?: string) => {
42 | let resolved = theme;
43 | if (!resolved) return;
44 |
45 | // If theme is system, resolve it before setting theme
46 | if (theme === "system" && enableSystem) {
47 | resolved = getSystemTheme();
48 | }
49 |
50 | const name = value ? value[resolved] : resolved;
51 | const enable = disableTransitionOnChange ? disableAnimation() : null;
52 | const d = document.documentElement;
53 |
54 | if (attribute === "class") {
55 | d.classList.remove(...attrs);
56 |
57 | if (name) d.classList.add(name);
58 | } else {
59 | if (name) {
60 | d.setAttribute(attribute, name);
61 | } else {
62 | d.removeAttribute(attribute);
63 | }
64 | }
65 |
66 | if (enableColorScheme) {
67 | const fallback = colorSchemes.includes(defaultTheme) ? defaultTheme : null;
68 | const colorScheme = colorSchemes.includes(resolved) ? resolved : fallback;
69 | // @ts-ignore
70 | d.style.colorScheme = colorScheme;
71 | }
72 |
73 | enable?.();
74 | }, []);
75 |
76 | const setTheme = useCallback(
77 | (theme: string) => {
78 | setThemeState(theme);
79 |
80 | // Save to storage
81 | try {
82 | localStorage.setItem(storageKey, theme);
83 | } catch (e) {
84 | // Unsupported
85 | }
86 | },
87 | [forcedTheme],
88 | );
89 |
90 | const handleMediaQuery = useCallback(
91 | (e: MediaQueryListEvent | MediaQueryList) => {
92 | const resolved = getSystemTheme(e);
93 | setResolvedTheme(resolved);
94 |
95 | if (theme === "system" && enableSystem && !forcedTheme) {
96 | applyTheme("system");
97 | }
98 | },
99 | [theme, forcedTheme],
100 | );
101 |
102 | // Always listen to System preference
103 | useEffect(() => {
104 | const media = window.matchMedia(MEDIA);
105 |
106 | // Intentionally use deprecated listener methods to support iOS & old browsers
107 | media.addListener(handleMediaQuery);
108 | handleMediaQuery(media);
109 |
110 | return () => media.removeListener(handleMediaQuery);
111 | }, [handleMediaQuery]);
112 |
113 | // localStorage event handling
114 | useEffect(() => {
115 | const handleStorage = (e: StorageEvent) => {
116 | if (e.key !== storageKey) {
117 | return;
118 | }
119 |
120 | // If default theme set, use it if localstorage === null (happens on local storage manual deletion)
121 | const theme = e.newValue || defaultTheme;
122 | setTheme(theme);
123 | };
124 |
125 | window.addEventListener("storage", handleStorage);
126 | return () => window.removeEventListener("storage", handleStorage);
127 | }, [setTheme]);
128 |
129 | // Whenever theme or forcedTheme changes, apply it
130 | useEffect(() => {
131 | applyTheme(forcedTheme ?? theme);
132 | }, [forcedTheme, theme]);
133 |
134 | const providerValue = useMemo(
135 | () => ({
136 | theme,
137 | setTheme,
138 | forcedTheme,
139 | resolvedTheme: theme === "system" ? resolvedTheme : theme,
140 | themes: enableSystem ? [...themes, "system"] : themes,
141 | systemTheme: (enableSystem ? resolvedTheme : undefined) as "light" | "dark" | undefined,
142 | }),
143 | [theme, setTheme, forcedTheme, resolvedTheme, enableSystem, themes],
144 | );
145 |
146 | return (
147 |
148 |
164 | {children as any}
165 |
166 | );
167 | };
168 |
169 | const ThemeScript = memo(
170 | ({
171 | forcedTheme,
172 | storageKey,
173 | attribute,
174 | enableSystem,
175 | enableColorScheme,
176 | defaultTheme,
177 | value,
178 | attrs,
179 | nonce,
180 | }: ThemeProviderProps & { attrs: string[]; defaultTheme: string }) => {
181 | const defaultSystem = defaultTheme === "system";
182 |
183 | // Code-golfing the amount of characters in the script
184 | const optimization = (() => {
185 | if (attribute === "class") {
186 | const removeClasses = `c.remove(${attrs.map((t: string) => `'${t}'`).join(",")})`;
187 |
188 | return `var d=document.documentElement,c=d.classList;${removeClasses};`;
189 | } else {
190 | return `var d=document.documentElement,n='${attribute}',s='setAttribute';`;
191 | }
192 | })();
193 |
194 | const fallbackColorScheme = (() => {
195 | if (!enableColorScheme) {
196 | return "";
197 | }
198 |
199 | const fallback = colorSchemes.includes(defaultTheme) ? defaultTheme : null;
200 |
201 | if (fallback) {
202 | return `if(e==='light'||e==='dark'||!e)d.style.colorScheme=e||'${defaultTheme}'`;
203 | } else {
204 | return `if(e==='light'||e==='dark')d.style.colorScheme=e`;
205 | }
206 | })();
207 |
208 | const updateDOM = (name: string, literal: boolean = false, setColorScheme = true) => {
209 | const resolvedName = value ? value[name] : name;
210 | const val = literal ? name + `|| ''` : `'${resolvedName}'`;
211 | let text = "";
212 |
213 | // MUCH faster to set colorScheme alongside HTML attribute/class
214 | // as it only incurs 1 style recalculation rather than 2
215 | // This can save over 250ms of work for pages with big DOM
216 | if (enableColorScheme && setColorScheme && !literal && colorSchemes.includes(name)) {
217 | text += `d.style.colorScheme = '${name}';`;
218 | }
219 |
220 | if (attribute === "class") {
221 | if (literal || resolvedName) {
222 | text += `c.add(${val})`;
223 | } else {
224 | text += `null`;
225 | }
226 | } else {
227 | if (resolvedName) {
228 | text += `d[s](n,${val})`;
229 | }
230 | }
231 |
232 | return text;
233 | };
234 |
235 | const scriptSrc = (() => {
236 | if (forcedTheme) {
237 | return `!function(){${optimization}${updateDOM(forcedTheme)}}()`;
238 | }
239 |
240 | if (enableSystem) {
241 | return `!function(){try{${optimization}var e=localStorage.getItem('${storageKey}');if('system'===e||(!e&&${defaultSystem})){var t='${MEDIA}',m=window.matchMedia(t);if(m.media!==t||m.matches){${updateDOM(
242 | "dark",
243 | )}}else{${updateDOM("light")}}}else if(e){${value ? `var x=${JSON.stringify(value)};` : ""}${updateDOM(
244 | value ? `x[e]` : "e",
245 | true,
246 | )}}${
247 | !defaultSystem ? `else{` + updateDOM(defaultTheme, false, false) + "}" : ""
248 | }${fallbackColorScheme}}catch(e){}}()`;
249 | }
250 |
251 | return `!function(){try{${optimization}var e=localStorage.getItem('${storageKey}');if(e){${
252 | value ? `var x=${JSON.stringify(value)};` : ""
253 | }${updateDOM(value ? `x[e]` : "e", true)}}else{${updateDOM(
254 | defaultTheme,
255 | false,
256 | false,
257 | )};}${fallbackColorScheme}}catch(t){}}();`;
258 | })();
259 |
260 | return ;
261 | },
262 | // Never re-render this component
263 | () => true,
264 | );
265 |
266 | // Helpers
267 | const getTheme = (key: string, fallback?: string) => {
268 | if (isServer) return undefined;
269 | let theme;
270 | try {
271 | theme = localStorage.getItem(key) || undefined;
272 | } catch (e) {
273 | // Unsupported
274 | }
275 | return theme || fallback;
276 | };
277 |
278 | const disableAnimation = () => {
279 | const css = document.createElement("style");
280 | css.appendChild(
281 | document.createTextNode(
282 | `*{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`,
283 | ),
284 | );
285 | document.head.appendChild(css);
286 |
287 | return () => {
288 | // Force restyle
289 | (() => window.getComputedStyle(document.body))();
290 |
291 | // Wait for next tick before removing
292 | setTimeout(() => {
293 | document.head.removeChild(css);
294 | }, 1);
295 | };
296 | };
297 |
298 | const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
299 | if (!e) e = window.matchMedia(MEDIA);
300 | const isDark = e.matches;
301 | const systemTheme = isDark ? "dark" : "light";
302 | return systemTheme;
303 | };
304 |
--------------------------------------------------------------------------------
/packages/web/src/vite-themes/vite-themes-types.ts:
--------------------------------------------------------------------------------
1 | /** Adapted from https://github.com/pacocoursey/next-themes/blob/a385b8d865bbb317ff73a5b6c1319ae566f7d6f1/src/types.ts */
2 |
3 | interface ValueObject {
4 | [themeName: string]: string;
5 | }
6 |
7 | export interface UseThemeProps {
8 | /** List of all available theme names */
9 | themes: string[];
10 | /** Forced theme name for the current page */
11 | forcedTheme?: string;
12 | /** Update the theme */
13 | setTheme: (theme: string) => void;
14 | /** Active theme name */
15 | theme?: string;
16 | /** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
17 | resolvedTheme?: string;
18 | /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
19 | systemTheme?: "dark" | "light";
20 | }
21 |
22 | export interface ThemeProviderProps {
23 | /** List of all available theme names */
24 | themes?: string[];
25 | /** Forced theme name for the current page */
26 | forcedTheme?: string;
27 | /** Whether to switch between dark and light themes based on prefers-color-scheme */
28 | enableSystem?: boolean;
29 | /** Disable all CSS transitions when switching themes */
30 | disableTransitionOnChange?: boolean;
31 | /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
32 | enableColorScheme?: boolean;
33 | /** Key used to store theme setting in localStorage */
34 | storageKey?: string;
35 | /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
36 | defaultTheme?: string;
37 | /** HTML attribute modified based on the active theme. Accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) */
38 | attribute?: string | "class";
39 | /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
40 | value?: ValueObject;
41 | /** Nonce string to pass to the inline script for CSP headers */
42 | nonce?: string;
43 |
44 | children?: React.ReactNode;
45 | }
46 |
--------------------------------------------------------------------------------
/packages/web/theme/preset.ts:
--------------------------------------------------------------------------------
1 | import { definePreset } from "@pandacss/dev";
2 |
3 | import { semanticTokens } from "./semantic-tokens";
4 | import { tokens } from "./tokens";
5 | import { textStyles } from "./text-styles";
6 |
7 | export const themePreset = definePreset({
8 | theme: {
9 | extend: {
10 | semanticTokens: semanticTokens,
11 | tokens: tokens,
12 | textStyles: textStyles
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/packages/web/theme/semantic-tokens.ts:
--------------------------------------------------------------------------------
1 | import { defineSemanticTokens } from '@pandacss/dev'
2 |
3 | export const semanticTokens = defineSemanticTokens({
4 | colors: {
5 | bg: {
6 | main: {
7 | value: {
8 | base: '{colors.yellow.300}',
9 | _dark: '{colors.gray.700}'
10 | }
11 | },
12 | muted: {
13 | value: {
14 | base: '{colors.gray.900}',
15 | _dark: '{colors.gray.400}'
16 | }
17 | },
18 | dark: {
19 | value: {
20 | base: '{colors.black}',
21 | _dark: '{colors.gray.400}'
22 | }
23 | },
24 | inverted: {
25 | value: { base: '{colors.white}', _dark: '{colors.black}' }
26 | },
27 | emphasized: {
28 | value: { base: '{colors.white}', _dark: '{colors.yellow.300}' }
29 | },
30 | 'emphasized.hover': {
31 | value: {
32 | base: '{colors.gray.100}',
33 | _dark: '{colors.gray.800}'
34 | }
35 | }
36 | },
37 | text: {
38 | main: {
39 | value: { base: '{colors.black}', _dark: '{colors.white}' }
40 | },
41 | headline: {
42 | value: { base: '{colors.black}', _dark: '{colors.yellow.300}' }
43 | },
44 | muted: {
45 | value: {
46 | base: '{colors.gray.800}',
47 | _dark: '{colors.gray.50}'
48 | }
49 | }
50 | }
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/packages/web/theme/text-styles.ts:
--------------------------------------------------------------------------------
1 | import { defineTextStyles } from '@pandacss/dev'
2 |
3 | export const textStyles = defineTextStyles({
4 | panda: {
5 | h1: {
6 | value: {
7 | fontSize: '14.5rem',
8 | lineHeight: '1',
9 | letterSpacing: 'tighter'
10 | }
11 | },
12 | h2: {
13 | value: {
14 | fontSize: { base: '2.5rem', lg: '3rem' },
15 | lineHeight: '1.2',
16 | letterSpacing: 'tight'
17 | }
18 | },
19 | h3: {
20 | value: {
21 | fontSize: { base: '1.875rem', lg: '2.25rem' },
22 | lineHeight: '1.2',
23 | letterSpacing: 'tight'
24 | }
25 | },
26 | h4: {
27 | value: {
28 | fontSize: '1.625rem',
29 | lineHeight: '1.2',
30 | letterSpacing: 'tight'
31 | }
32 | }
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/packages/web/theme/tokens.ts:
--------------------------------------------------------------------------------
1 | import { defineTokens } from "@pandacss/dev";
2 |
3 | export const tokens = defineTokens({
4 | colors: {
5 | // chakra
6 | whiteAlpha: {
7 | 50: { value: "rgba(255, 255, 255, 0.04)" },
8 | 100: { value: "rgba(255, 255, 255, 0.06)" },
9 | 200: { value: "rgba(255, 255, 255, 0.08)" },
10 | 300: { value: "rgba(255, 255, 255, 0.16)" },
11 | 400: { value: "rgba(255, 255, 255, 0.24)" },
12 | 500: { value: "rgba(255, 255, 255, 0.36)" },
13 | 600: { value: "rgba(255, 255, 255, 0.48)" },
14 | 700: { value: "rgba(255, 255, 255, 0.64)" },
15 | 800: { value: "rgba(255, 255, 255, 0.80)" },
16 | 900: { value: "rgba(255, 255, 255, 0.92)" },
17 | },
18 |
19 | blackAlpha: {
20 | 50: { value: "rgba(0, 0, 0, 0.04)" },
21 | 100: { value: "rgba(0, 0, 0, 0.06)" },
22 | 200: { value: "rgba(0, 0, 0, 0.08)" },
23 | 300: { value: "rgba(0, 0, 0, 0.16)" },
24 | 400: { value: "rgba(0, 0, 0, 0.24)" },
25 | 500: { value: "rgba(0, 0, 0, 0.36)" },
26 | 600: { value: "rgba(0, 0, 0, 0.48)" },
27 | 700: { value: "rgba(0, 0, 0, 0.64)" },
28 | 800: { value: "rgba(0, 0, 0, 0.80)" },
29 | 900: { value: "rgba(0, 0, 0, 0.92)" },
30 | },
31 | // custom
32 | primary: {
33 | "50": {value: "#EEF7F7"},
34 | "100": {value: "#CEE8E8"},
35 | "200": {value: "#AFDADA"},
36 | "300": {value: "#90CBCB"},
37 | "400": {value: "#70BCBC"},
38 | "500": {value: "#51AEAE"},
39 | "600": {value: "#418B8B"},
40 | "700": {value: "#316868"},
41 | "800": {value: "#204646"},
42 | "900": {value: "#102323"},
43 | },
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/packages/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "resolveJsonModule": true,
12 | "isolatedModules": false,
13 | "noEmit": true,
14 | "jsx": "react-jsx",
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "paths": {
22 | "panda/*": ["./styled-system/*"]
23 | }
24 | },
25 | "include": ["src", "styled-system", "./get-ts-declarations.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react-swc";
3 | import tsconfigPaths from "vite-tsconfig-paths";
4 | import { reactClickToComponent } from "vite-plugin-react-click-to-component";
5 |
6 | import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
7 |
8 | import path from "node:path";
9 | import * as url from "node:url";
10 |
11 | // @ts-ignore
12 | const dirname = url.fileURLToPath(new URL(".", import.meta.url));
13 |
14 | // https://vitejs.dev/config/
15 | export default defineConfig({
16 | plugins: [
17 | react(),
18 | tsconfigPaths(),
19 | reactClickToComponent(),
20 | {
21 | name: "replace-ts-patch",
22 | transform(code, id) {
23 | // fix ts-patch used in @sinclair/typebox-codegen
24 | if (!id.includes("sinclair")) return;
25 | const transformedCode = code.replace("tsp2.tsShim.sys.fileExists", "() => false");
26 | return {
27 | code: transformedCode,
28 | map: { mappings: "" },
29 | };
30 | },
31 | },
32 | ],
33 | optimizeDeps: {
34 | include: ["escalade"],
35 | esbuildOptions: {
36 | define: {
37 | global: "globalThis",
38 | },
39 | plugins: [NodeGlobalsPolyfillPlugin({ process: true })],
40 | },
41 | },
42 | build: {
43 | rollupOptions: {
44 | plugins: [
45 | {
46 | name: "replace-process-cwd",
47 | transform(code, _id) {
48 | const transformedCode = code.replace(/process\.cwd\(\)/g, '""');
49 | return {
50 | code: transformedCode,
51 | map: { mappings: "" },
52 | };
53 | },
54 | },
55 | ],
56 | },
57 | },
58 | resolve: {
59 | alias: {
60 | module: path.join(dirname, "./module.shim.ts"),
61 | path: "path-browserify",
62 | fs: path.join(dirname, "./fs.shim.ts"),
63 | process: "process/browser",
64 | os: "os-browserify",
65 | util: "util",
66 | },
67 | },
68 | });
69 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://github.com/tsconfig/bases/blob/141d65201924bae1c70a0a4e18d05f42fbf93a9a/bases/strictest.json
3 | "compilerOptions": {
4 | "strict": true,
5 | "allowUnusedLabels": false,
6 | "allowUnreachableCode": false,
7 | "exactOptionalPropertyTypes": true,
8 | "noFallthroughCasesInSwitch": true,
9 | "noImplicitOverride": true,
10 | "noImplicitReturns": true,
11 | "noPropertyAccessFromIndexSignature": true,
12 | "noUncheckedIndexedAccess": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 |
16 | "verbatimModuleSyntax": false,
17 | "checkJs": true,
18 |
19 | "esModuleInterop": true,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | //
23 | "isolatedModules": true, // https://preconstruct.tools/guides/building-typescript-packages
24 | "noEmit": true
25 | },
26 | "exclude": ["node_modules", ".changeset", ".github", "dist"],
27 | "types": ["node", "vitest/importMeta"],
28 | //
29 | "$schema": "https://json.schemastore.org/tsconfig",
30 | "display": "Strictest"
31 | }
32 |
--------------------------------------------------------------------------------