swaggie 1.6.0 → 1.7.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -171
- package/dist/index.d.ts +7 -6
- package/dist/utils/documentLoader.js +10 -4
- package/dist/utils/index.js +1 -0
- package/dist/utils/refResolver.js +309 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -10,152 +10,115 @@
|
|
|
10
10
|

|
|
11
11
|

|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Swaggie generates TypeScript client code from an OpenAPI 3 specification. Instead of writing API-fetching code by hand, you point Swaggie at your API spec and it outputs a fully typed, ready-to-use client — helping you catch errors at compile time rather than at runtime.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
See the [Example section](#example) for a quick demo.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
> Inspired by [OpenApi Client](https://github.com/mikestead/openapi-client).
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
---
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
- Supports `fetch`, `axios`, `xior`, `SWR + axios`, `Angular 1`, `Angular 2+` templates. It's flexible.
|
|
23
|
-
- Possible to create your own template that works with your existing codebase
|
|
24
|
-
- It's a dev tool that generates code, so it's not a runtime dependency
|
|
25
|
-
- Support for `allOf`, `oneOf`, `anyOf` and `$ref` in schemas
|
|
26
|
-
- Support for different types of enums
|
|
27
|
-
- Support for different content types
|
|
28
|
-
- JSDoc comments for generated code
|
|
29
|
-
- Small library size and very small and tree-shakable output that is all placed in one file
|
|
30
|
-
- OpenAPI 3.1 is partially supported (mostly enums, more to come)
|
|
21
|
+
## Features
|
|
31
22
|
|
|
32
|
-
|
|
23
|
+
- Generates TypeScript code from OpenAPI 3.0, 3.1, and 3.2 specs
|
|
24
|
+
- Supports multiple HTTP client libraries out of the box: `fetch`, `axios`, `xior`, `SWR + axios`, `Angular 1`, `Angular 2+`, and `TanStack Query`
|
|
25
|
+
- Custom templates — bring your own to fit your existing codebase
|
|
26
|
+
- Supports `allOf`, `oneOf`, `anyOf`, `$ref`, nullable types, and various enum definitions
|
|
27
|
+
- Handles multiple content types: JSON, plain text, multipart/form-data, URL-encoded, and binary
|
|
28
|
+
- JSDoc comments on all generated functions
|
|
29
|
+
- Generates a single, small, tree-shakable output file
|
|
30
|
+
- Dev-only dependency — zero runtime overhead
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
---
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
## Installation
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
Install as a dev dependency in your project:
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## OpenAPI versions
|
|
43
|
-
|
|
44
|
-
Swaggie from version 1.0 supports OpenAPI 3.0 (and some features of 3.1). Swagger or OpenAPI 2.0 documents are not supported anymore, but you have few options how to deal with it:
|
|
45
|
-
|
|
46
|
-
- **(preferred)** From your backend server generate OpenAPI 3.0 spec instead of version 2 (samples are updated to use OpenAPI 3.0)
|
|
47
|
-
- Convert your OpenAPI 2.0 spec to 3.0 using [swagger2openapi](https://www.npmjs.com/package/swagger2openapi) tool (or something similar)
|
|
48
|
-
- If you can't do that for any reason, you can stick to `Swaggie v0.x`. But upgrade is suggested
|
|
49
|
-
|
|
50
|
-
Please note that OpenAPI 3.0 is a major spec upgrade and it's possible that there will be some breaking changes in the generated code.
|
|
51
|
-
I have tried my best to minimize the impact, but it was not possible to avoid it completely.
|
|
52
|
-
|
|
53
|
-
More info about breaking changes can be found in the [Releases](https://github.com/yhnavein/swaggie/releases).
|
|
54
|
-
|
|
55
|
-
### CLI
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
Usage: swaggie [options]
|
|
59
|
-
|
|
60
|
-
Options:
|
|
61
|
-
|
|
62
|
-
-V, --version output the version number
|
|
63
|
-
-c, --config <path> The path to the configuration JSON file. You can do all the set up there instead of parameters in the CLI
|
|
64
|
-
-s, --src <url|path> The url or path to the Open API spec file
|
|
65
|
-
-o, --out <filePath> The path to the file where the API would be generated. Use stdout if left empty
|
|
66
|
-
-b, --baseUrl <string> Base URL that will be used as a default value in the clients (default: "")
|
|
67
|
-
-t, --template <string> Template used for generating API client. Default: "axios"
|
|
68
|
-
--preferAny Use "any" type instead of "unknown" (default: false)
|
|
69
|
-
--skipDeprecated Skip deprecated operations. When enabled, deprecated operations will be skipped from the generated code (default: false)
|
|
70
|
-
--servicePrefix <string> Prefix for service names. Useful when you have multiple APIs and you want to avoid name collisions (default: "")
|
|
71
|
-
--allowDots <bool> Determines if dots should be used for serialization object properties
|
|
72
|
-
--arrayFormat <format> Determines how arrays should be serialized (choices: "indices", "repeat", "brackets")
|
|
73
|
-
-h, --help display help for command
|
|
38
|
+
```bash
|
|
39
|
+
npm install swaggie --save-dev
|
|
74
40
|
```
|
|
75
41
|
|
|
76
|
-
|
|
42
|
+
Or install globally to use from anywhere:
|
|
77
43
|
|
|
78
44
|
```bash
|
|
79
|
-
|
|
45
|
+
npm install swaggie -g
|
|
80
46
|
```
|
|
81
47
|
|
|
82
|
-
|
|
48
|
+
---
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
swaggie -s $URL -o ./client/petstore.ts && prettier ./client/petstore.ts --write`
|
|
86
|
-
```
|
|
50
|
+
## OpenAPI Version Support
|
|
87
51
|
|
|
88
|
-
|
|
52
|
+
Swaggie supports **OpenAPI 3.0 and newer**. OpenAPI 2.0 (Swagger) is not supported.
|
|
89
53
|
|
|
90
|
-
|
|
54
|
+
If your backend still produces a 2.0 spec, you have a few options:
|
|
91
55
|
|
|
92
|
-
|
|
56
|
+
- **(Recommended)** Update your backend to generate an OpenAPI 3.0 spec instead
|
|
57
|
+
- Convert your 2.0 spec to 3.0 using [swagger2openapi](https://www.npmjs.com/package/swagger2openapi)
|
|
58
|
+
- Stay on `Swaggie v0.x` if upgrading is not currently possible
|
|
93
59
|
|
|
94
|
-
|
|
60
|
+
---
|
|
95
61
|
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
"$schema": "https://raw.githubusercontent.com/yhnavein/swaggie/master/schema.json",
|
|
99
|
-
"out": "./src/client/petstore.ts",
|
|
100
|
-
"src": "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
101
|
-
"template": "axios",
|
|
102
|
-
"baseUrl": "/api",
|
|
103
|
-
"preferAny": true,
|
|
104
|
-
"servicePrefix": "",
|
|
105
|
-
"dateFormat": "Date", // "string" | "Date"
|
|
106
|
-
"nullableStrategy": "ignore", // "ignore" | "include" | "nullableAsOptional"
|
|
107
|
-
"queryParamsSerialization": {
|
|
108
|
-
"arrayFormat": "repeat", // "repeat" | "brackets" | "indices"
|
|
109
|
-
"allowDots": true
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
```
|
|
62
|
+
## Quick Start
|
|
113
63
|
|
|
114
|
-
###
|
|
64
|
+
### CLI
|
|
115
65
|
|
|
116
|
-
|
|
66
|
+
Run Swaggie from the command line:
|
|
117
67
|
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
xior Lightweight and modern alternative to axios. Uses [xior](https://github.com/suhaotian/xior#intro)
|
|
121
|
-
swr-axios SWR for GET requests with axios as backend
|
|
122
|
-
tsq-xior TanStack Query for GET requests with xior as backend
|
|
123
|
-
fetch Barebone fetch API. Recommended for React / Vue / similar frameworks
|
|
124
|
-
ng1 Angular 1 client (this is for the old one)
|
|
125
|
-
ng2 Angular 2+ client (uses HttpClient, InjectionTokens, etc)
|
|
68
|
+
```bash
|
|
69
|
+
swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore.ts
|
|
126
70
|
```
|
|
127
71
|
|
|
128
|
-
|
|
72
|
+
**Available options:**
|
|
129
73
|
|
|
130
|
-
```
|
|
131
|
-
|
|
74
|
+
```
|
|
75
|
+
-V, --version Output the version number
|
|
76
|
+
-c, --config <path> Path to a JSON configuration file
|
|
77
|
+
-s, --src <url|path> URL or file path to the OpenAPI spec
|
|
78
|
+
-o, --out <filePath> Output file path (omit to print to stdout)
|
|
79
|
+
-b, --baseUrl <string> Default base URL for the generated client (default: "")
|
|
80
|
+
-t, --template <string> Template to use for code generation (default: "axios")
|
|
81
|
+
--preferAny Use "any" instead of "unknown" for untyped values (default: false)
|
|
82
|
+
--skipDeprecated Exclude deprecated operations from the output (default: false)
|
|
83
|
+
--servicePrefix Prefix for service names — useful when generating multiple APIs
|
|
84
|
+
--allowDots Use dot notation to serialize nested object query params
|
|
85
|
+
--arrayFormat How arrays are serialized: "indices", "repeat", or "brackets"
|
|
86
|
+
-h, --help Show help
|
|
132
87
|
```
|
|
133
88
|
|
|
134
|
-
|
|
89
|
+
### Formatting the Output
|
|
135
90
|
|
|
136
|
-
|
|
91
|
+
Swaggie produces functional TypeScript, but the formatting is not always perfect. It is recommended to pipe the output through a formatter. For example, using Prettier:
|
|
137
92
|
|
|
138
|
-
|
|
93
|
+
```bash
|
|
94
|
+
swaggie -s $URL -o ./client/petstore.ts && prettier ./client/petstore.ts --write
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This can be added as an `npm` script in your project for easy re-generation.
|
|
139
98
|
|
|
140
|
-
|
|
99
|
+
---
|
|
141
100
|
|
|
142
|
-
|
|
101
|
+
## Configuration File
|
|
143
102
|
|
|
144
|
-
For
|
|
103
|
+
For anything beyond a one-off run, a configuration file is the cleaner approach. Create a JSON file with your settings and pass it via `-c`:
|
|
145
104
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
| `?a.b=1&c[]=2&c[]=3` | `true` | `brackets` |
|
|
150
|
-
| `?a.b=1&c[0]=2&c[1]=3` | `true` | `indices` |
|
|
151
|
-
| `?a[b]=1&c=2&c=3` | `false` | `repeat` |
|
|
152
|
-
| `?a[b]=1&c[]=2&c[]=3` | `false` | `brackets` |
|
|
153
|
-
| `?a[b]=1&c[0]=2&c[1]=3` | `false` | `indices` |
|
|
105
|
+
```bash
|
|
106
|
+
swaggie -c swaggie.config.json
|
|
107
|
+
```
|
|
154
108
|
|
|
155
|
-
|
|
109
|
+
**Example configuration:**
|
|
156
110
|
|
|
157
111
|
```json
|
|
158
112
|
{
|
|
113
|
+
"$schema": "https://raw.githubusercontent.com/yhnavein/swaggie/master/schema.json",
|
|
114
|
+
"src": "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
115
|
+
"out": "./src/client/petstore.ts",
|
|
116
|
+
"template": "axios",
|
|
117
|
+
"baseUrl": "/api",
|
|
118
|
+
"preferAny": true,
|
|
119
|
+
"servicePrefix": "",
|
|
120
|
+
"dateFormat": "Date",
|
|
121
|
+
"nullableStrategy": "ignore",
|
|
159
122
|
"queryParamsSerialization": {
|
|
160
123
|
"arrayFormat": "repeat",
|
|
161
124
|
"allowDots": true
|
|
@@ -163,57 +126,47 @@ Once you know what your backend expects, you can adjust the configuration file a
|
|
|
163
126
|
}
|
|
164
127
|
```
|
|
165
128
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
OpenAPI 3.0 allows marking fields as `nullable: true`. Swaggie provides three strategies for translating this into TypeScript, controlled by the `nullableStrategy` option:
|
|
169
|
-
|
|
170
|
-
| Value | Description |
|
|
171
|
-
| ---------------------- | ---------------------------------------------------------------------------------- |
|
|
172
|
-
| `"ignore"` (default) | `nullable: true` is ignored. Types are generated as if `nullable` was not set. |
|
|
173
|
-
| `"include"` | `nullable: true` appends `\| null` to the TypeScript type (e.g. `string \| null`). |
|
|
174
|
-
| `"nullableAsOptional"` | `nullable: true` makes the property optional (`?`) instead of adding `\| null`. |
|
|
175
|
-
|
|
176
|
-
**Examples** for a schema with `tenant: { type: 'string', nullable: true }` (required):
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
// nullableStrategy: "ignore" → tenant: string;
|
|
180
|
-
// nullableStrategy: "include" → tenant: string | null;
|
|
181
|
-
// nullableStrategy: "nullableAsOptional" → tenant?: string;
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Code Quality
|
|
129
|
+
The `$schema` field enables autocompletion and inline documentation in most editors.
|
|
185
130
|
|
|
186
|
-
|
|
187
|
-
> Because Swaggie relies on a templating engine, whitespaces are generally a mess, so they may change between versions.
|
|
131
|
+
---
|
|
188
132
|
|
|
189
|
-
|
|
133
|
+
## Templates
|
|
190
134
|
|
|
191
|
-
|
|
135
|
+
Swaggie ships with the following built-in templates:
|
|
192
136
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
137
|
+
| Template | Description |
|
|
138
|
+
| ----------- | ----------------------------------------------------------------------------------------- |
|
|
139
|
+
| `axios` | Default. Recommended for React, Vue, and similar frameworks |
|
|
140
|
+
| `xior` | Lightweight modern alternative to axios ([xior](https://github.com/suhaotian/xior#intro)) |
|
|
141
|
+
| `swr-axios` | SWR hooks for GET requests, backed by axios |
|
|
142
|
+
| `tsq-xior` | TanStack Query hooks for GET requests, backed by xior |
|
|
143
|
+
| `fetch` | Uses the native browser Fetch API |
|
|
144
|
+
| `ng1` | Angular 1 client |
|
|
145
|
+
| `ng2` | Angular 2+ client (uses HttpClient and InjectionTokens) |
|
|
196
146
|
|
|
197
|
-
|
|
147
|
+
To use a custom template, pass the path to your template directory:
|
|
198
148
|
|
|
199
149
|
```bash
|
|
200
|
-
|
|
150
|
+
swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore.ts --template ./my-swaggie-template/
|
|
201
151
|
```
|
|
202
152
|
|
|
203
|
-
|
|
153
|
+
---
|
|
204
154
|
|
|
205
|
-
|
|
155
|
+
## Example
|
|
206
156
|
|
|
207
|
-
Let's
|
|
157
|
+
Let's say you're building a TypeScript client for the [PetStore API](https://petstore3.swagger.io). Instead of writing fetch calls by hand, run:
|
|
208
158
|
|
|
209
159
|
```bash
|
|
210
160
|
swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./api/petstore.ts && prettier ./api/petstore.ts --write
|
|
211
161
|
```
|
|
212
162
|
|
|
163
|
+
Swaggie will generate something like this:
|
|
164
|
+
|
|
213
165
|
```typescript
|
|
214
166
|
// ./api/petstore.ts
|
|
215
167
|
|
|
216
168
|
import Axios, { AxiosPromise } from 'axios';
|
|
169
|
+
|
|
217
170
|
const axios = Axios.create({
|
|
218
171
|
baseURL: '/api',
|
|
219
172
|
paramsSerializer: (params) =>
|
|
@@ -242,27 +195,71 @@ export const petClient = {
|
|
|
242
195
|
};
|
|
243
196
|
```
|
|
244
197
|
|
|
245
|
-
|
|
198
|
+
You can then use it directly in your application code:
|
|
246
199
|
|
|
247
200
|
```typescript
|
|
248
201
|
// app.ts
|
|
249
202
|
|
|
250
|
-
import { petClient } from './api/
|
|
203
|
+
import { petClient } from './api/petstore';
|
|
251
204
|
|
|
252
205
|
petClient.getPetById(123).then((pet) => console.log('Pet: ', pet));
|
|
253
206
|
```
|
|
254
207
|
|
|
255
|
-
If
|
|
208
|
+
If the API removes an endpoint you rely on, re-running Swaggie will cause a **compile-time error** — not a runtime surprise for your users.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Advanced Configuration
|
|
213
|
+
|
|
214
|
+
### Query Parameter Serialization
|
|
215
|
+
|
|
216
|
+
Different backends expect query parameters in different formats. Swaggie lets you control this via the `queryParamsSerialization` config. The default values match what ASP.NET Core expects.
|
|
217
|
+
|
|
218
|
+
Here's how the object `{ "a": { "b": 1 }, "c": [2, 3] }` is serialized under each combination:
|
|
219
|
+
|
|
220
|
+
| Result | `allowDots` | `arrayFormat` |
|
|
221
|
+
| ----------------------- | ----------- | ------------- |
|
|
222
|
+
| `?a.b=1&c=2&c=3` | `true` | `repeat` |
|
|
223
|
+
| `?a.b=1&c[]=2&c[]=3` | `true` | `brackets` |
|
|
224
|
+
| `?a.b=1&c[0]=2&c[1]=3` | `true` | `indices` |
|
|
225
|
+
| `?a[b]=1&c=2&c=3` | `false` | `repeat` |
|
|
226
|
+
| `?a[b]=1&c[]=2&c[]=3` | `false` | `brackets` |
|
|
227
|
+
| `?a[b]=1&c[0]=2&c[1]=3` | `false` | `indices` |
|
|
228
|
+
|
|
229
|
+
Once you identify what your backend expects, update your config:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"queryParamsSerialization": {
|
|
234
|
+
"arrayFormat": "repeat",
|
|
235
|
+
"allowDots": true
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Nullable Strategy
|
|
256
241
|
|
|
257
|
-
|
|
242
|
+
OpenAPI 3.0 allows fields to be marked as `nullable: true`. Swaggie gives you three ways to handle this in the generated TypeScript, via the `nullableStrategy` option:
|
|
258
243
|
|
|
259
|
-
|
|
244
|
+
| Value | Behavior |
|
|
245
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
246
|
+
| `"ignore"` _(default)_ | `nullable` is ignored — the field is typed as if it were not nullable |
|
|
247
|
+
| `"include"` | Appends `\| null` to the type (e.g. `string \| null`) |
|
|
248
|
+
| `"nullableAsOptional"` | Makes the field optional (`?`) instead of adding `\| null` |
|
|
260
249
|
|
|
261
|
-
|
|
250
|
+
**Example** — given `tenant: { type: 'string', nullable: true }` (required):
|
|
262
251
|
|
|
263
|
-
|
|
252
|
+
```typescript
|
|
253
|
+
// nullableStrategy: "ignore" → tenant: string;
|
|
254
|
+
// nullableStrategy: "include" → tenant: string | null;
|
|
255
|
+
// nullableStrategy: "nullableAsOptional" → tenant?: string;
|
|
256
|
+
```
|
|
264
257
|
|
|
265
|
-
|
|
258
|
+
### Parameter Modifiers
|
|
259
|
+
|
|
260
|
+
Sometimes an API spec marks a parameter as required, but your client handles it in an interceptor and you don't want it cluttering every method signature. Parameter modifiers let you override this globally without touching the spec.
|
|
261
|
+
|
|
262
|
+
**Example:**
|
|
266
263
|
|
|
267
264
|
```json
|
|
268
265
|
{
|
|
@@ -276,23 +273,37 @@ Example (in the config file):
|
|
|
276
273
|
}
|
|
277
274
|
```
|
|
278
275
|
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
- `"ignore"` — the parameter is removed from all generated method signatures
|
|
277
|
+
- `"optional"` — the parameter becomes optional regardless of what the spec says
|
|
278
|
+
- `"required"` — the parameter is always required (generally better to just fix the spec)
|
|
279
|
+
|
|
280
|
+
### Code Quality
|
|
281
|
+
|
|
282
|
+
Swaggie's output is functional but not always perfectly formatted, since it uses a templating engine internally. It is strongly recommended to run the output through a formatter to ensure consistent style across regenerations.
|
|
283
|
+
|
|
284
|
+
**Prettier** (most popular):
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
prettier ./FILE_PATH.ts --write
|
|
288
|
+
```
|
|
281
289
|
|
|
282
|
-
|
|
290
|
+
**Biome** (fast alternative):
|
|
283
291
|
|
|
284
|
-
|
|
292
|
+
```bash
|
|
293
|
+
biome check ./FILE_PATH.ts --apply-unsafe
|
|
294
|
+
```
|
|
285
295
|
|
|
286
|
-
|
|
296
|
+
Either tool needs to be installed separately and configured for your project.
|
|
287
297
|
|
|
288
|
-
|
|
298
|
+
---
|
|
289
299
|
|
|
290
|
-
|
|
300
|
+
## Using Swaggie Programmatically
|
|
291
301
|
|
|
292
|
-
|
|
302
|
+
You can also call Swaggie directly from Node.js/bun/deno/etc:
|
|
293
303
|
|
|
294
304
|
```javascript
|
|
295
|
-
|
|
305
|
+
import swaggie from 'swaggie';
|
|
306
|
+
|
|
296
307
|
swaggie
|
|
297
308
|
.genCode({
|
|
298
309
|
src: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
|
@@ -309,22 +320,35 @@ function error(e) {
|
|
|
309
320
|
}
|
|
310
321
|
```
|
|
311
322
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Server Setup Samples
|
|
326
|
+
|
|
327
|
+
Swaggie only needs a JSON or YAML OpenAPI spec file — it does not require a running server. However, if you want to see how to configure your backend to expose an OpenAPI spec automatically, check out the sample configurations in the `samples/` folder:
|
|
328
|
+
|
|
329
|
+
- [ASP.NET Core + NSwag](./samples/dotnetcore/nswag/README.md)
|
|
330
|
+
- [ASP.NET Core + Swashbuckle](./samples/dotnetcore/swashbuckle/README.md)
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## What's Supported
|
|
335
|
+
|
|
336
|
+
| Supported | Not Supported |
|
|
337
|
+
| ------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
338
|
+
| OpenAPI 3.0, 3.1, 3.2 | Swagger / OpenAPI 2.0 |
|
|
339
|
+
| `allOf`, `oneOf`, `anyOf`, `$ref` | `not` keyword |
|
|
340
|
+
| Spec formats: JSON, YAML | Very complex query parameter structures |
|
|
341
|
+
| Extensions: `x-position`, `x-name`, `x-enumNames`, `x-enum-varnames` | Multiple response types (only the first is used) |
|
|
342
|
+
| Content types: JSON, plain text, multipart/form-data | Multiple request body types (only the first is used) |
|
|
343
|
+
| Content types: `application/x-www-form-urlencoded`, `application/octet-stream` | References to external spec files |
|
|
344
|
+
| Various enum definition styles | OpenAPI callbacks and webhooks |
|
|
345
|
+
| Nullable types, path inheritance, JSDoc descriptions | |
|
|
346
|
+
| Remote URLs and local file paths as spec source | |
|
|
347
|
+
| Grouping by tags, graceful handling of duplicate operation IDs | |
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Used By
|
|
328
352
|
|
|
329
353
|
<div style="display: flex; gap: 1rem;">
|
|
330
354
|
<a href="https://www.britishcouncil.org"><img alt="British Council" src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/BritishCouncil.png/320px-BritishCouncil.png" style="height: 50px;" /></a>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from 'openapi-types';
|
|
2
|
-
import type { ClientOptions, FullAppOptions } from '
|
|
2
|
+
import type { ClientOptions, FullAppOptions } from './types';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export type CodeGenResult = [string, AppOptions];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs the whole code generation process @returns `CodeGenResult`
|
|
8
|
+
**/
|
|
9
|
+
export declare function runCodeGenerator(options: Partial<FullAppOptions>): Promise<CodeGenResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var _nodefs = require('node:fs'); var _nodefs2 = _interopRequireDefault(_nodefs);
|
|
2
2
|
var _yaml = require('yaml');
|
|
3
3
|
|
|
4
|
+
var _refResolver = require('./refResolver');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Function that loads an OpenAPI document from a path or URL
|
|
@@ -32,10 +33,15 @@ async function loadFromUrl(url) {
|
|
|
32
33
|
return parseFileContents(contents, url);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function readLocalFile(filePath) {
|
|
36
|
-
|
|
37
|
-
_nodefs2.default.readFile(filePath, 'utf8', (err,
|
|
38
|
-
|
|
36
|
+
async function readLocalFile(filePath) {
|
|
37
|
+
const contents = await new Promise((res, rej) =>
|
|
38
|
+
_nodefs2.default.readFile(filePath, 'utf8', (err, loadedContents) =>
|
|
39
|
+
err ? rej(err) : res(loadedContents)
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
const spec = parseFileContents(contents , filePath) ;
|
|
43
|
+
|
|
44
|
+
return _refResolver.resolveExternalFileRefs.call(void 0, spec, filePath);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function parseFileContents(contents, path) {
|
package/dist/utils/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _createStarExport(obj) { Object.keys(obj) .filter((key) => key !== "default" && key !== "__esModule") .forEach((key) => { if (exports.hasOwnProperty(key)) { return; } Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); }); }var _utils = require('./utils'); _createStarExport(_utils);
|
|
2
2
|
var _documentLoader = require('./documentLoader'); _createStarExport(_documentLoader);
|
|
3
|
+
var _refResolver = require('./refResolver'); _createStarExport(_refResolver);
|
|
3
4
|
var _templateManager = require('./templateManager'); _createStarExport(_templateManager);
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var _promises = require('node:fs/promises'); var _promises2 = _interopRequireDefault(_promises);
|
|
2
|
+
var _nodepath = require('node:path'); var _nodepath2 = _interopRequireDefault(_nodepath);
|
|
3
|
+
var _yaml = require('yaml');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const SUPPORTED_COMPONENT_SECTIONS = new Set([
|
|
13
|
+
'schemas',
|
|
14
|
+
'parameters',
|
|
15
|
+
'requestBodies',
|
|
16
|
+
'responses',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves external file refs into local component refs.
|
|
33
|
+
* For now we only support local file refs (no http/https) and component targets.
|
|
34
|
+
*/
|
|
35
|
+
async function resolveExternalFileRefs(
|
|
36
|
+
spec,
|
|
37
|
+
rootSpecPath
|
|
38
|
+
) {
|
|
39
|
+
const resolvedRootPath = _nodepath2.default.resolve(rootSpecPath);
|
|
40
|
+
const context = {
|
|
41
|
+
rootSpec: spec,
|
|
42
|
+
rootSpecPath: resolvedRootPath,
|
|
43
|
+
docCache: new Map(),
|
|
44
|
+
importedRefs: new Map(),
|
|
45
|
+
resolvingRefs: new Set(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await rewriteRefsInNode(spec, resolvedRootPath, context);
|
|
49
|
+
|
|
50
|
+
return spec;
|
|
51
|
+
} exports.resolveExternalFileRefs = resolveExternalFileRefs;
|
|
52
|
+
|
|
53
|
+
async function rewriteRefsInNode(node, currentFilePath, context) {
|
|
54
|
+
if (!node) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(node)) {
|
|
59
|
+
for (const item of node) {
|
|
60
|
+
await rewriteRefsInNode(item, currentFilePath, context);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof node !== 'object') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const record = node ;
|
|
70
|
+
|
|
71
|
+
if (typeof record.$ref === 'string') {
|
|
72
|
+
const resolved = await resolveRef(record.$ref, currentFilePath, context);
|
|
73
|
+
if (resolved.type === 'ref') {
|
|
74
|
+
record.$ref = resolved.value;
|
|
75
|
+
} else if (Object.keys(record).length === 1) {
|
|
76
|
+
for (const key of Object.keys(record)) {
|
|
77
|
+
delete record[key];
|
|
78
|
+
}
|
|
79
|
+
Object.assign(record, resolved.value );
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const values = Object.values(record);
|
|
84
|
+
for (const value of values) {
|
|
85
|
+
await rewriteRefsInNode(value, currentFilePath, context);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveRef(
|
|
90
|
+
rawRef,
|
|
91
|
+
currentFilePath,
|
|
92
|
+
context
|
|
93
|
+
) {
|
|
94
|
+
if (rawRef.startsWith('#/')) {
|
|
95
|
+
if (_nodepath2.default.resolve(currentFilePath) === context.rootSpecPath) {
|
|
96
|
+
return { type: 'ref', value: rawRef };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return importRefFromFile(_nodepath2.default.resolve(currentFilePath), rawRef, rawRef, context);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (/^https?:\/\//i.test(rawRef)) {
|
|
103
|
+
throw new Error(`External HTTP refs are not supported: '${rawRef}'`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const [rawFilePath, fragment] = rawRef.split('#');
|
|
107
|
+
if (!rawFilePath) {
|
|
108
|
+
throw new Error(`Unsupported $ref format: '${rawRef}'`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fragment) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`External refs must include a JSON pointer fragment: '${rawRef}'`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!fragment.startsWith('/')) {
|
|
118
|
+
throw new Error(`Unsupported $ref pointer format: '${rawRef}'`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const resolvedPath = _nodepath2.default.resolve(_nodepath2.default.dirname(currentFilePath), rawFilePath);
|
|
122
|
+
const pointer = `#${fragment}`;
|
|
123
|
+
|
|
124
|
+
return importRefFromFile(resolvedPath, pointer, rawRef, context);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function importRefFromFile(
|
|
128
|
+
sourceFilePath,
|
|
129
|
+
pointer,
|
|
130
|
+
rawRef,
|
|
131
|
+
context
|
|
132
|
+
) {
|
|
133
|
+
const targetInfo = parseImportTarget(pointer);
|
|
134
|
+
const target = await getValueByPointer(sourceFilePath, pointer, context);
|
|
135
|
+
const importKey = `${sourceFilePath}${pointer}`;
|
|
136
|
+
|
|
137
|
+
if (!targetInfo) {
|
|
138
|
+
if (context.resolvingRefs.has(importKey)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Circular non-component external ref is not supported: '${rawRef}'`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const targetCopy = structuredClone(target);
|
|
145
|
+
context.resolvingRefs.add(importKey);
|
|
146
|
+
await rewriteRefsInNode(targetCopy, sourceFilePath, context);
|
|
147
|
+
context.resolvingRefs.delete(importKey);
|
|
148
|
+
|
|
149
|
+
return { type: 'inline', value: targetCopy };
|
|
150
|
+
}
|
|
151
|
+
const existingRef = context.importedRefs.get(importKey);
|
|
152
|
+
if (existingRef) {
|
|
153
|
+
return { type: 'ref', value: existingRef };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const section = targetInfo.section;
|
|
157
|
+
const name = targetInfo.name;
|
|
158
|
+
const targetCopy = structuredClone(target);
|
|
159
|
+
const alias = getOrCreateComponentAlias(section, name, sourceFilePath, context);
|
|
160
|
+
const localRef = `#/components/${section}/${alias}`;
|
|
161
|
+
|
|
162
|
+
context.importedRefs.set(importKey, localRef);
|
|
163
|
+
|
|
164
|
+
const sectionMap = ensureComponentSection(context.rootSpec, section);
|
|
165
|
+
sectionMap[alias] = targetCopy;
|
|
166
|
+
|
|
167
|
+
context.resolvingRefs.add(importKey);
|
|
168
|
+
await rewriteRefsInNode(targetCopy, sourceFilePath, context);
|
|
169
|
+
context.resolvingRefs.delete(importKey);
|
|
170
|
+
|
|
171
|
+
return { type: 'ref', value: localRef };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseImportTarget(pointer) {
|
|
175
|
+
const parts = pointer
|
|
176
|
+
.replace(/^#\//, '')
|
|
177
|
+
.split('/')
|
|
178
|
+
.map(unescapePointerSegment);
|
|
179
|
+
|
|
180
|
+
if (parts.length === 2) {
|
|
181
|
+
const [legacySection, name] = parts;
|
|
182
|
+
if (legacySection === 'parameters') {
|
|
183
|
+
return { section: 'parameters', name };
|
|
184
|
+
}
|
|
185
|
+
if (legacySection === 'responses') {
|
|
186
|
+
return { section: 'responses', name };
|
|
187
|
+
}
|
|
188
|
+
if (legacySection === 'requestBodies') {
|
|
189
|
+
return { section: 'requestBodies', name };
|
|
190
|
+
}
|
|
191
|
+
if (legacySection === 'definitions') {
|
|
192
|
+
return { section: 'schemas', name };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (parts.length !== 3 || parts[0] !== 'components') {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const section = parts[1] ;
|
|
201
|
+
const name = parts[2];
|
|
202
|
+
|
|
203
|
+
if (!SUPPORTED_COMPONENT_SECTIONS.has(section)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!name) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { section, name };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ensureComponentSection(
|
|
215
|
+
spec,
|
|
216
|
+
section
|
|
217
|
+
) {
|
|
218
|
+
if (!spec.components) {
|
|
219
|
+
spec.components = {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!spec.components[section]) {
|
|
223
|
+
spec.components[section] = {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return spec.components[section] ;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getOrCreateComponentAlias(
|
|
230
|
+
section,
|
|
231
|
+
originalName,
|
|
232
|
+
sourceFilePath,
|
|
233
|
+
context
|
|
234
|
+
) {
|
|
235
|
+
const sectionMap = ensureComponentSection(context.rootSpec, section);
|
|
236
|
+
|
|
237
|
+
if (!sectionMap[originalName]) {
|
|
238
|
+
return originalName;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const suffix = shortHash(sourceFilePath);
|
|
242
|
+
let candidate = `${originalName}__${suffix}`;
|
|
243
|
+
let inc = 2;
|
|
244
|
+
|
|
245
|
+
while (sectionMap[candidate]) {
|
|
246
|
+
candidate = `${originalName}__${suffix}_${inc}`;
|
|
247
|
+
inc += 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return candidate;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function getValueByPointer(filePath, pointer, context) {
|
|
254
|
+
const doc = await loadDocument(filePath, context);
|
|
255
|
+
const parts = pointer
|
|
256
|
+
.replace(/^#\//, '')
|
|
257
|
+
.split('/')
|
|
258
|
+
.map(unescapePointerSegment);
|
|
259
|
+
|
|
260
|
+
let current = doc;
|
|
261
|
+
for (const part of parts) {
|
|
262
|
+
if (!current || typeof current !== 'object' || !(part in current)) {
|
|
263
|
+
throw new Error(`Could not resolve ref pointer '${pointer}' in '${filePath}'`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
current = current[part];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return current;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function loadDocument(filePath, context) {
|
|
273
|
+
const absPath = _nodepath2.default.resolve(filePath);
|
|
274
|
+
const cached = context.docCache.get(absPath);
|
|
275
|
+
if (cached) {
|
|
276
|
+
return cached;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const contents = await _promises2.default.readFile(absPath, 'utf8');
|
|
280
|
+
const parsed = parseFileContents(contents, absPath);
|
|
281
|
+
context.docCache.set(absPath, parsed);
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseFileContents(contents, filePath) {
|
|
286
|
+
if (/.ya?ml$/i.test(filePath)) {
|
|
287
|
+
return _yaml.parse.call(void 0, contents);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (/.json$/i.test(filePath)) {
|
|
291
|
+
return JSON.parse(contents);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const firstChar = contents.trimStart()[0];
|
|
295
|
+
return firstChar === '{' ? JSON.parse(contents) : _yaml.parse.call(void 0, contents);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function unescapePointerSegment(segment) {
|
|
299
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function shortHash(input) {
|
|
303
|
+
let hash = 0;
|
|
304
|
+
for (let i = 0; i < input.length; i++) {
|
|
305
|
+
hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return hash.toString(36);
|
|
309
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swaggie",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Generate TypeScript
|
|
3
|
+
"version": "1.7.0-beta.1",
|
|
4
|
+
"description": "Generate a fully typed TypeScript API client from your OpenAPI 3 spec",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Piotr Dabrowski",
|
|
7
7
|
"url": "https://github.com/yhnavein"
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
"keywords": [
|
|
36
36
|
"swagger",
|
|
37
37
|
"openapi",
|
|
38
|
+
"oapi",
|
|
38
39
|
"openapi 3.0",
|
|
40
|
+
"openapi 3.1",
|
|
41
|
+
"openapi 3.2",
|
|
39
42
|
"rest",
|
|
40
43
|
"rest client",
|
|
41
44
|
"fetch",
|
|
@@ -53,10 +56,10 @@
|
|
|
53
56
|
"commander": "^14.0.3",
|
|
54
57
|
"eta": "^4.5.1",
|
|
55
58
|
"yaml": "^2.8.2",
|
|
56
|
-
"
|
|
59
|
+
"picocolors": "^1.1.1"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
|
-
"bun-types": "1.3.
|
|
62
|
+
"bun-types": "1.3.10",
|
|
60
63
|
"openapi-types": "^12.1.3",
|
|
61
64
|
"sucrase": "3.35.1",
|
|
62
65
|
"typescript": "5.9.3"
|