Skip to content

Commit ea65d72

Browse files
authored
v7 rewrite (#1363)
* Refactor using TypeScript AST and Redocly OpenAPI core * Polish testing setup * Get CLI running * Improve comments * Update examples * Use Redocly APIs config * Fix tests * Bump version * Update openapi-fetch types * Make empty methods optional, add examples typechecking
1 parent 7798436 commit ea65d72

File tree

122 files changed

+349181
-304904
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+349181
-304904
lines changed

Diff for: .eslintignore

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
bin
21
coverage
32
dist
43
examples

Diff for: .eslintrc.cjs

+37-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,49 @@ module.exports = {
44
parserOptions: {
55
project: ["./tsconfig.json"],
66
},
7-
extends: ["eslint:recommended", "plugin:@typescript-eslint/strict", "plugin:vitest/recommended"],
8-
plugins: ["@typescript-eslint", "no-only-tests", "prettier", "vitest"],
7+
extends: [
8+
"eslint:recommended",
9+
"plugin:@typescript-eslint/strict",
10+
"plugin:vitest/recommended",
11+
],
12+
plugins: [
13+
"@typescript-eslint",
14+
"import",
15+
"no-only-tests",
16+
"prettier",
17+
"vitest",
18+
],
919
rules: {
1020
"@typescript-eslint/consistent-indexed-object-style": "off", // sometimes naming keys is more user-friendly
1121
"@typescript-eslint/no-dynamic-delete": "off", // delete is OK
1222
"@typescript-eslint/no-non-null-assertion": "off", // this is better than "as"
23+
"@typescript-eslint/no-shadow": "error",
1324
"@typescript-eslint/no-unnecessary-condition": "off", // this gives bad advice
25+
"arrow-body-style": ["error", "as-needed"],
26+
"dot-notation": "error",
27+
"import/newline-after-import": "error",
28+
"import/order": [
29+
"error",
30+
{
31+
alphabetize: {
32+
order: "asc",
33+
orderImportKind: "asc",
34+
caseInsensitive: true,
35+
},
36+
groups: [
37+
["builtin", "external"],
38+
"internal",
39+
"parent",
40+
"index",
41+
"sibling",
42+
],
43+
},
44+
],
45+
curly: "error",
46+
"object-shorthand": "error", // don’t use foo["bar"]
1447
"no-console": "error",
48+
"no-global-assign": "error",
49+
"no-undef": "off", // handled by TS
1550
"no-unused-vars": "off",
1651
},
1752
overrides: [

Diff for: .prettierrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"printWidth": 240
2+
"singleAttributePerLine": true
33
}

Diff for: docs/package.json

+13-13
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,26 @@
1010
"update-contributors": "node scripts/update-contributors.js"
1111
},
1212
"dependencies": {
13-
"@algolia/client-search": "^4.19.1",
13+
"@algolia/client-search": "^4.20.0",
1414
"@astrojs/preact": "^2.2.2",
15-
"@astrojs/react": "^2.2.2",
16-
"@docsearch/css": "^3.5.1",
17-
"@docsearch/react": "^3.5.1",
18-
"@types/react": "^18.2.20",
19-
"@types/react-dom": "^18.2.7",
20-
"astro": "^2.10.9",
21-
"preact": "^10.17.0",
15+
"@astrojs/react": "^3.0.2",
16+
"@docsearch/css": "^3.5.2",
17+
"@docsearch/react": "^3.5.2",
18+
"@types/react": "^18.2.24",
19+
"@types/react-dom": "^18.2.8",
20+
"astro": "^3.2.2",
21+
"preact": "^10.18.1",
2222
"react": "^18.2.0",
2323
"react-dom": "^18.2.0",
24-
"sass": "^1.65.1"
24+
"sass": "^1.68.0"
2525
},
2626
"devDependencies": {
2727
"@astrojs/sitemap": "^2.0.2",
28-
"@cobalt-ui/cli": "^1.4.1",
29-
"@cobalt-ui/plugin-sass": "^1.2.3",
30-
"@types/node": "^20.5.0",
28+
"@cobalt-ui/cli": "^1.6.0",
29+
"@cobalt-ui/plugin-sass": "^1.3.0",
30+
"@types/node": "^20.8.2",
3131
"html-escaper": "^3.0.3",
3232
"typescript": "^5.2.2",
33-
"vite-plugin-sass-dts": "^1.3.9"
33+
"vite-plugin-sass-dts": "^1.3.11"
3434
}
3535
}

Diff for: docs/src/content/docs/about.md

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ description: Additional info about this project
2424

2525
1. Support converting any valid OpenAPI schema to TypeScript types, no matter how complicated.
2626
1. Generated types should be statically-analyzable and runtime-free (with minor exceptions like <a href="https://www.typescriptlang.org/docs/handbook/enums.html" target="_blank" rel="noopener noreferrer">enums</a>).
27-
1. Don’t validate schemas; there are existing libraries for that like <a href="https://redocly.com/docs/cli/commands/lint/" target="_blank" rel="noopener noreferrer">Redocly</a>.
2827
1. Generated types should match your original schema as closely as possible, preserving original capitalization, etc.
2928
1. Typegen only needs Node.js to run (no Java, Python, etc.) and works in any environment.
3029
1. Support fetching OpenAPI schemas from files as well as local and remote servers.

Diff for: docs/src/content/docs/advanced.md

+38-9
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ const BASE_URL = "https://myapi.com/v1";
8888
// End Settings
8989

9090
// type helpers — ignore these; these just make TS lookups better
91-
type FilterKeys<Obj, Matchers> = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj];
91+
type FilterKeys<Obj, Matchers> = {
92+
[K in keyof Obj]: K extends Matchers ? Obj[K] : never;
93+
}[keyof Obj];
9294
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
9395
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
9496
type MediaType = `${string}/${string}`;
@@ -107,26 +109,39 @@ type MockedResponse<T, Status extends keyof T = keyof T> = FilterKeys<
107109
*/
108110
export function mockResponses(responses: {
109111
[Path in keyof Partial<paths>]: {
110-
[Method in keyof Partial<paths[Path]>]: MockedResponse<PathResponses<paths[Path][Method]>>;
112+
[Method in keyof Partial<paths[Path]>]: MockedResponse<
113+
PathResponses<paths[Path][Method]>
114+
>;
111115
};
112116
}) {
113117
fetchMock.mockResponse((req) => {
114-
const mockedPath = findPath(req.url.replace(BASE_URL, ""), Object.keys(responses))!;
118+
const mockedPath = findPath(
119+
req.url.replace(BASE_URL, ""),
120+
Object.keys(responses),
121+
)!;
115122
// note: we get lazy with the types here, because the inference is bad anyway and this has a `void` return signature. The important bit is the parameter signature.
116-
if (!mockedPath || (!responses as any)[mockedPath]) throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior)
123+
if (!mockedPath || (!responses as any)[mockedPath])
124+
throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior)
117125
const method = req.method.toLowerCase();
118-
if (!(responses as any)[mockedPath][method]) throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked
126+
if (!(responses as any)[mockedPath][method])
127+
throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked
119128
if (!(responses as any)[mockedPath][method]) {
120129
throw new Error(`${req.method} called but not mocked on ${mockedPath}`);
121130
}
122131
const { status, body } = (responses as any)[mockedPath][method];
123132
return { status, body: JSON.stringify(body) };
124-
})
133+
});
125134
}
126135

127136
// helper function that matches a realistic URL (/users/123) to an OpenAPI path (/users/{user_id}
128-
export function findPath(actual: string, testPaths: string[]): string | undefined {
129-
const url = new URL(actual, actual.startsWith("http") ? undefined : "http://testapi.com");
137+
export function findPath(
138+
actual: string,
139+
testPaths: string[],
140+
): string | undefined {
141+
const url = new URL(
142+
actual,
143+
actual.startsWith("http") ? undefined : "http://testapi.com",
144+
);
130145
const actualParts = url.pathname.split("/");
131146
for (const p of testPaths) {
132147
let matched = true;
@@ -149,7 +164,9 @@ export function findPath(actual: string, testPaths: string[]): string | undefine
149164
```ts
150165
export function mockResponses(responses: {
151166
[Path in keyof Partial<paths>]: {
152-
[Method in keyof Partial<paths[Path]>]: MockedResponse<PathResponses<paths[Path][Method]>>;
167+
[Method in keyof Partial<paths[Path]>]: MockedResponse<
168+
PathResponses<paths[Path][Method]>
169+
>;
153170
};
154171
});
155172
```
@@ -158,6 +175,18 @@ export function mockResponses(responses: {
158175

159176
Now, whenever your schema updates, **all your mock data will be typechecked correctly** 🎉. This is a huge step in ensuring resilient, accurate tests.
160177

178+
## Debugging
179+
180+
To enable debugging, set `DEBUG=openapi-ts:*` as an env var like so:
181+
182+
```sh
183+
$ DEBUG=openapi-ts:* npx openapi-typescript schema.yaml -o my-types.ts
184+
```
185+
186+
To only see certain types of debug messages, you can set `DEBUG=openapi-ts:[scope]` instead. Valid scopes are `redoc`, `lint`, `bundle`, and `ts`.
187+
188+
Note that debug messages will be suppressed if the output is `stdout`.
189+
161190
## Tips
162191

163192
In no particular order, here are a few best practices to make life easier when working with OpenAPI-derived types.

Diff for: docs/src/content/docs/cli.md

+73-36
Original file line numberDiff line numberDiff line change
@@ -3,60 +3,97 @@ title: CLI
33
description: openapi-typescript CLI usage
44
---
55

6-
The CLI is the most common way to use openapi-typescript. The CLI can parse JSON and YAML (via <a href="https://www.npmjs.com/package/js-yaml" target="_blank" rel="noopener noreferrer">js-yaml</a>). It can parse local and remote schemas (and even supports basic auth).
6+
The CLI is the most common way to use openapi-typescript. The CLI can parse JSON and YAML, and even validates your schemas using the [Redocly CLI](https://redocly.com/docs/cli/commands/lint/). It can parse local and remote schemas (and even supports basic auth).
77

8-
## Reading schemas
8+
## Transforming an OpenAPI schema to TypeScript
9+
10+
### Single schema
11+
12+
The simplest way to transform schemas is by specifying an input schema (JSON or YAML), followed by `--output` (`-o`) where you’d like the output to be saved:
913

1014
```bash
1115
npx openapi-typescript schema.yaml -o schema.ts
1216

13-
# 🚀 schema.yaml -> schema.ts [7ms]
17+
# 🚀 schema.yaml -> schema.ts [50ms]
1418
```
1519

16-
### Globbing local schemas
17-
1820
```bash
19-
npx openapi-typescript "specs/**/*.yaml" -o schemas/
21+
npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.yaml -o petstore.d.ts
2022

21-
# 🚀 specs/one.yaml -> schemas/specs/one.ts [7ms]
22-
# 🚀 specs/two.yaml -> schemas/specs/two.ts [7ms]
23-
# 🚀 specs/three.yaml -> schemas/specs/three.ts [7ms]
23+
# 🚀 https://petstore3.swagger.io/api/v3/openapi.yaml -> petstore.d.ts [250ms]
2424
```
2525

26-
_Thanks, [@sharmarajdaksh](https://github.com/sharmarajdaksh)!_
26+
### Multiple schemas
2727

28-
### Remote schemas
28+
To transform multiple schemas, create a `redocly.yaml` file in the root of your project with [APIs defined](https://redocly.com/docs/cli/configuration/). Under `apis`, give each schema a unique name and optionally a version (the name doesn’t matter, so long as it’s unique). Set the `root` value to your schema’s entry point—this will act as the main input. For the output, set it with `openapi-ts.output`:
29+
30+
```yaml
31+
apis:
32+
core@v2:
33+
root: ./openapi/openapi.yaml
34+
openapi-ts:
35+
output: ./openapi/openapi.ts
36+
external@v1:
37+
root: ./openapi/external.yaml
38+
openapi-ts:
39+
output: ./openapi/openapi.ts
40+
```
41+
42+
Whenver you have a `redocly.yaml` file in your project with `apis`, you can omit the input/output parameters in the CLI:
2943

3044
```bash
31-
npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.yaml -o petstore.d.ts
45+
npx openapi-typescript
46+
```
3247

33-
# 🚀 https://petstore3.swagger.io/api/v3/openapi.yaml -> petstore.d.ts [250ms]
48+
> ⚠️ In previous versions globbing was supported, but that has been **deprecated** in v7 in favor of `redocly.yaml`. You’ll be able to control per-schema output locations better, as well as getting unique per-schema settings.
49+
50+
## Redoc config
51+
52+
A `redocly.yaml` file isn’t required to use openapi-typescript. By default it extends the `"minimal"` built-in config. But it is recommended if you want to have custom validation rules (or build types for [multiple schemas](#multiple-schemas)). The CLI will try to automatically find a `redocly.yaml` in the root of your project, but you can also provide its location with the `--redoc` flag:
53+
54+
```bash
55+
npx openapi-typescript --redoc ./path/to/redocly.yaml
56+
```
57+
58+
You can read more about the Redoc’s configuration options [in their docs](https://redocly.com/docs/cli/configuration/).
59+
60+
## Auth
61+
62+
Authentication for non-public schemas is handled in your [Redocly config](https://redocly.com/docs/cli/configuration/#resolve-non-public-or-non-remote-urls). You can add headers and basic authentication like so:
63+
64+
```yaml
65+
resolve:
66+
http:
67+
headers:
68+
- matches: https://api.example.com/v2/**
69+
name: X-API-KEY
70+
envVariable: SECRET_KEY
71+
- matches: https://example.com/*/test.yaml
72+
name: Authorization
73+
envVariable: SECRET_AUTH
3474
```
3575

36-
_Thanks, [@psmyrdek](https://github.com/psmyrdek)!_
76+
Refer to the [Redocly docs](https://redocly.com/docs/cli/configuration/#resolve-non-public-or-non-remote-urls) for additional options.
3777

3878
## Options
3979

40-
| Option | Alias | Default | Description |
41-
| :------------------------ | :---- | :------: | :--------------------------------------------------------------------------------------------------------------------------- |
42-
| `--help` | | | Display inline help message and exit |
43-
| `--version` | | | Display this library’s version and exit |
44-
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
45-
| `--auth [token]` | | | Provide an auth token to be passed along in the request (only if accessing a private schema) |
46-
| `--header` | `-x` | | Provide an array of or singular headers as an alternative to a JSON object. Each header must follow the `key: value` pattern |
47-
| `--headers-object="{…}"` | `-h` | | Provide a JSON object as string of HTTP headers for remote schema request. This will take priority over `--header` |
48-
| `--http-method` | `-m` | `GET` | Provide the HTTP Verb/Method for fetching a schema from a remote URL |
49-
| `--immutable-types` | | `false` | Generates immutable types (readonly properties and readonly array) |
50-
| `--additional-properties` | | `false` | Allow arbitrary properties for all schema objects without `additionalProperties: false` |
51-
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
52-
| `--default-non-nullable` | | `false` | Treat schema objects with default values as non-nullable |
53-
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
54-
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |
55-
| `--support-array-length` | | `false` | Generate tuples using array `minItems` / `maxItems` |
56-
| `--alphabetize` | | `false` | Sort types alphabetically |
57-
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |
58-
59-
### `--path-params-as-types`
80+
| Option | Alias | Default | Description |
81+
| :------------------------ | :---- | :------: | :------------------------------------------------------------------------------------------------------------------ |
82+
| `--help` | | | Display inline help message and exit |
83+
| `--version` | | | Display this library’s version and exit |
84+
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
85+
| `--redoc [location]` | | | Path to a `redocly.yaml` file (see [Multiple schemas](#multiple-schemas)) |
86+
| `--immutable` | | `false` | Generates immutable types (readonly properties and readonly array) |
87+
| `--additional-properties` | | `false` | Allow arbitrary properties for all schema objects without `additionalProperties: false` |
88+
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
89+
| `--default-non-nullable` | | `false` | Treat schema objects with default values as non-nullable |
90+
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
91+
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |
92+
| `--array-length` | | `false` | Generate tuples using array `minItems` / `maxItems` |
93+
| `--alphabetize` | | `false` | Sort types alphabetically |
94+
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |
95+
96+
### pathParamsAsTypes flag
6097

6198
By default, your URLs are preserved exactly as-written in your schema:
6299

@@ -88,7 +125,7 @@ Though this is a contrived example, you could use this feature to automatically
88125

89126
_Thanks, [@Powell-v2](https://github.com/Powell-v2)!_
90127

91-
### `--support-array-length`
128+
### arrayLength flag
92129

93130
This option is useful for generating tuples if an array type specifies `minItems` or `maxItems`.
94131

@@ -105,7 +142,7 @@ components:
105142
maxItems: 2
106143
```
107144
108-
Enabling `--support-array-length` would change the typing like so:
145+
Enabling `--array-length` would change the typing like so:
109146

110147
```diff
111148
export interface components {

Diff for: docs/src/content/docs/introduction.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ npm i -D openapi-typescript
3434
3535
## Basic usage
3636

37-
First, generate a local type file by running `npx openapi-typescript`:
37+
First, generate a local type file by running `npx openapi-typescript`, first specifying your input schema (JSON or YAML), and where you’d like the `--output` (`-o`) to be saved:
3838

3939
```bash
4040
# Local schema
@@ -46,9 +46,7 @@ npx openapi-typescript https://myapi.dev/api/v1/openapi.yaml -o ./path/to/my/sch
4646
# 🚀 https://myapi.dev/api/v1/openapi.yaml -> ./path/to/my/schema.d.ts [250ms]
4747
```
4848

49-
> ⚠️ Be sure to <a href="https://redocly.com/docs/cli/commands/lint/" target="_blank" rel="noopener noreferrer">validate your schemas</a>! openapi-typescript will err on invalid schemas.
50-
51-
Then, import schemas from the generated file like so:
49+
Then in your TypeScript project, import types where needed:
5250

5351
```ts
5452
import { paths, components } from "./path/to/my/schema"; // <- generated by openapi-typescript
@@ -60,8 +58,10 @@ type MyType = components["schemas"]["MyType"];
6058
type EndpointParams = paths["/my/endpoint"]["parameters"];
6159

6260
// Response obj
63-
type SuccessResponse = paths["/my/endpoint"]["get"]["responses"][200]["content"]["application/json"]["schema"];
64-
type ErrorResponse = paths["/my/endpoint"]["get"]["responses"][500]["content"]["application/json"]["schema"];
61+
type SuccessResponse =
62+
paths["/my/endpoint"]["get"]["responses"][200]["content"]["application/json"]["schema"];
63+
type ErrorResponse =
64+
paths["/my/endpoint"]["get"]["responses"][500]["content"]["application/json"]["schema"];
6565
```
6666

6767
From here, you can use these types for any of the following (but not limited to):

0 commit comments

Comments
 (0)