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 CHANGED
@@ -10,152 +10,115 @@
10
10
  ![npm downloads](https://img.shields.io/npm/dw/swaggie.svg)
11
11
  ![npm install size](https://packagephobia.now.sh/badge?p=swaggie)
12
12
 
13
- Generate Typescript code from an OpenAPI 3.0 document, so that accessing REST API resources from the client code is less error-prone, static-typed and just easier to use long-term.
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
- You can take a look at the [Examples section](#example) down below.
15
+ See the [Example section](#example) for a quick demo.
16
16
 
17
- Project is based and inspired by [OpenApi Client](https://github.com/mikestead/openapi-client).
17
+ > Inspired by [OpenApi Client](https://github.com/mikestead/openapi-client).
18
18
 
19
- ### Features
19
+ ---
20
20
 
21
- - Generate TypeScript code from OpenAPI 3.0 spec
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
- ## Install
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
- In your project
32
+ ---
35
33
 
36
- npm install swaggie --save-dev
34
+ ## Installation
37
35
 
38
- Or globally to run CLI from anywhere
36
+ Install as a dev dependency in your project:
39
37
 
40
- npm install swaggie -g
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
- Sample CLI usage using Swagger's Pet Store:
42
+ Or install globally to use from anywhere:
77
43
 
78
44
  ```bash
79
- swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore.ts
45
+ npm install swaggie -g
80
46
  ```
81
47
 
82
- `swaggie` outputs TypeScript that is somehow formatted, but it's far from perfect. You can adjust the generated code by prettifying output using your preferred beautify tool using your repo's styling guidelines. For example involving `prettier` looks like this:
48
+ ---
83
49
 
84
- ```bash
85
- swaggie -s $URL -o ./client/petstore.ts && prettier ./client/petstore.ts --write`
86
- ```
50
+ ## OpenAPI Version Support
87
51
 
88
- And this can be easily automated (in the `npm` scripts for example)
52
+ Swaggie supports **OpenAPI 3.0 and newer**. OpenAPI 2.0 (Swagger) is not supported.
89
53
 
90
- ### Configuration File
54
+ If your backend still produces a 2.0 spec, you have a few options:
91
55
 
92
- Instead of providing all required flags from the command line you can alternatively create a new JSON file where you can fill up all settings.
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
- Sample configuration looks like this:
60
+ ---
95
61
 
96
- ```json
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
- ### Templates
64
+ ### CLI
115
65
 
116
- The following templates are bundled with Swaggie:
66
+ Run Swaggie from the command line:
117
67
 
118
- ```
119
- axios Default template. Recommended for React / Vue / similar frameworks. Uses axios
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
- If you want to use your own template, you can use the path to your template for the `-t` parameter:
72
+ **Available options:**
129
73
 
130
- ```bash
131
- swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore --template ./my-swaggie-template/
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
- ## Usage Integrating into your project
89
+ ### Formatting the Output
135
90
 
136
- Let's assume that you have a [PetStore API](http://petstore.swagger.io/) as your REST API and you are developing a client app written in TypeScript that will consume this API.
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
- Instead of writing any code by hand for fetching particular resources, we will let Swaggie do it for us.
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
- ### Query Parameters Serialization
99
+ ---
141
100
 
142
- When it comes to use of query parameters then you might need to adjust the way these parameters will be serialized, as backend server you are using expects them to be in a specific format. Thankfully in Swaggie you can specify how they should be handled. If you won't provide any configuration, then Swaggie will use the defaults values expected in the ASP.NET Core world.
101
+ ## Configuration File
143
102
 
144
- For your convenience there are few config examples to achieve different serialization formats for an object `{ "a": { "b": 1 }, "c": [2, 3] }`:
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
- | Expected Format | allowDots | arrayFormat |
147
- | ----------------------- | --------- | ----------- |
148
- | `?a.b=1&c=2&c=3` | `true` | `repeat` |
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
- Once you know what your backend expects, you can adjust the configuration file accordingly: (below are default values)
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
- ### Nullable Strategy
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
- > Please note that it's **recommended** to pipe Swaggie command to some prettifier like `prettier`, `biome` or `dprint` to make the generated code look not only nice, but also persistent.
187
- > Because Swaggie relies on a templating engine, whitespaces are generally a mess, so they may change between versions.
131
+ ---
188
132
 
189
- **Suggested prettiers**
133
+ ## Templates
190
134
 
191
- [prettier](https://prettier.io/) - the most popular one
135
+ Swaggie ships with the following built-in templates:
192
136
 
193
- ```bash
194
- prettier ./FILE_PATH.ts --write
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
- [biome](https://biomejs.dev) - the super fast one
147
+ To use a custom template, pass the path to your template directory:
198
148
 
199
149
  ```bash
200
- biome check ./FILE_PATH.ts --apply-unsafe
150
+ swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore.ts --template ./my-swaggie-template/
201
151
  ```
202
152
 
203
- You are not limited to any of these, but in our examples we will use Prettier. Please remember that these tools needs to be installed first and they need a config file in your project.
153
+ ---
204
154
 
205
- ### Example
155
+ ## Example
206
156
 
207
- Let's run `swaggie` against PetStore API and see what will happen:
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
- When we have that we can write some domain code and use this auto-generated classes:
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/petClient';
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 Petstore owners decide to remove method we use, then after running `swaggie` again it will no longer be present in the `petClient` class. This will result in the build error, which is very much appreciated at this stage.
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
- Without this approach, the error would be spotted by our end-user and he/she would not appreciate it at all!
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
- ## Other features
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
- ### Parameter adjustment
250
+ **Example** given `tenant: { type: 'string', nullable: true }` (required):
262
251
 
263
- In some cases you might want to adjust the parameters globally but without touching the OpenAPI spec. For example, the spec may explicitly define some of parameters as `required`, but you will handle them in the interceptor. In such case, you don't want to define them is every single method. This is where this feature comes in handy.
252
+ ```typescript
253
+ // nullableStrategy: "ignore" → tenant: string;
254
+ // nullableStrategy: "include" → tenant: string | null;
255
+ // nullableStrategy: "nullableAsOptional" → tenant?: string;
256
+ ```
264
257
 
265
- Example (in the config file):
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
- This will ensure that `clientId` parameters will never appear in any of the generated methods, and `orgId` will be optional (regardless of what the spec says).
280
- You can also use `required` value to enforce the parameter to be required, _(but in this case, realistically it would be better to just fix the spec)_.
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
- ## Server config
290
+ **Biome** (fast alternative):
283
291
 
284
- You might wonder how to set up server to fully utilize Swaggie's features. For that I've added a `samples/` folder with sample configurations.
292
+ ```bash
293
+ biome check ./FILE_PATH.ts --apply-unsafe
294
+ ```
285
295
 
286
- [ASP.NET Core + Nswag](./samples/dotnetcore/nswag/README.md)
296
+ Either tool needs to be installed separately and configured for your project.
287
297
 
288
- [ASP.NET Core + Swashbuckle](./samples/dotnetcore/swashbuckle/README.md)
298
+ ---
289
299
 
290
- Server is not necessary to use Swaggie. Swaggie cares only about the JSON/yaml file with the Open API spec, but for your development purpose you might want to have a server that can serve this file automatically from the actual endpoints.
300
+ ## Using Swaggie Programmatically
291
301
 
292
- ## Using Swaggie programmatically
302
+ You can also call Swaggie directly from Node.js/bun/deno/etc:
293
303
 
294
304
  ```javascript
295
- const swaggie = require('swaggie');
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
- ## Notes
313
-
314
- | Supported | Not supported |
315
- | ------------------------------------------------------------------------------ | ----------------------------------------------- |
316
- | OpenAPI 3, OpenAPI 3.1, OpenAPI 3.2 | Swagger, Open API 2.0 |
317
- | `allOf`, `oneOf`, `anyOf`, `$ref` to schemas | `not` |
318
- | Spec formats: `JSON`, `YAML` | VERY complex query params |
319
- | Extensions: `x-position`, `x-name`, `x-enumNames`, `x-enum-varnames` | Multiple response types (only one will be used) |
320
- | Content types: `JSON`, `text`, `multipart/form-data` | Multiple request types (only one will be used) |
321
- | Content types: `application/x-www-form-urlencoded`, `application/octet-stream` | References to external spec files |
322
- | Different types of enum definitions | OpenAPI callbacks |
323
- | Paths inheritance, comments (descriptions), nullable, `["<TYPE>", null]` | OpenAPI webhooks |
324
- | Getting documents from remote locations or as path reference (local file) | |
325
- | Grouping endpoints by tags + handle gracefully duplicate operation ids | |
326
-
327
- ## Used by
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 '../src/types';
2
+ import type { ClientOptions, FullAppOptions } from './types';
3
3
 
4
- /** Runs whole code generation process. @returns generated code */
5
- export declare function runCodeGenerator(options: FullAppOptions): Promise<string>;
6
- /** Validates if the spec is correct and if is supported */
7
- export declare function verifySpec(spec: OpenAPIV3.Document): Promise<OpenAPIV3.Document>;
8
- export declare function applyConfigFile(options: FullAppOptions): Promise<ClientOptions>;
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
- return new Promise((res, rej) =>
37
- _nodefs2.default.readFile(filePath, 'utf8', (err, contents) => (err ? rej(err) : res(contents)))
38
- ).then((contents) => parseFileContents(contents, filePath));
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) {
@@ -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.6.0",
4
- "description": "Generate TypeScript REST client code from an OpenAPI spec",
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
- "nanocolors": "^0.2.0"
59
+ "picocolors": "^1.1.1"
57
60
  },
58
61
  "devDependencies": {
59
- "bun-types": "1.3.9",
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"