Skip to content

Commit 6d1eb32

Browse files
committed
Refactor using TypeScript AST and Redocly OpenAPI core
1 parent 8e02a1b commit 6d1eb32

File tree

90 files changed

+6886
-5433
lines changed

Some content is hidden

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

90 files changed

+6886
-5433
lines changed

.changeset/beige-students-wink.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
**Feature**: add `formatOptions` to allow formatting TS output

.changeset/blue-ladybugs-laugh.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": major
3+
---
4+
5+
⚠️ **Breaking**: Most optional objects are now always present in types, just typed as `:never`. This includes keys of the Components Object as well as HTTP methods.

.changeset/giant-scissors-repeat.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
**Feature**: add `enum` option to export top-level enums from schemas

.changeset/happy-lamps-bathe.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
🧹 Cleaned up and reorganized all tests

.changeset/lazy-ads-add.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": major
3+
---
4+
5+
⚠️ **Breaking**: No more `external` export in schemas anymore. Everything gets flattened into the `components` object instead (if referencing a schema object from a remote partial, note it may have had a minor name change to avoid conflict).

.changeset/modern-bobcats-think.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
**Feature**: header responses add `[key: string]: unknown` index type to allow for additional untyped headers

.changeset/nasty-candles-rescue.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
Refactor internals to use TypeScript AST rather than string mashing

.changeset/rude-jokes-grin.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"openapi-typescript": major
3+
---
4+
5+
⚠️ **Breaking**: Drop auth/fetching options in favor of Redocly CLI’s
6+
7+
- The `auth`, `httpHeaders`, `httpMethod`, and `fetch` options were all removed from the CLI and Node.js API
8+
- To migrate, you’ll need to create a [redocly.yaml config](https://redocly.com/docs/cli/configuration/) that specifies your auth options [in the http setting](https://redocly.com/docs/cli/configuration/#resolve-non-public-or-non-remote-urls)
9+
- Worth noting your `redocly.yaml` config will be respected for any other related settings

.changeset/shaggy-adults-obey.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": major
3+
---
4+
5+
⚠️ **Breaking** `defaultNonNullable` option now defaults to `true`. You’ll now need to manually set `false` to return to old behavior.

.changeset/shaggy-experts-confess.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
**Feature**: bundle schemas with Redocly CLI
6+
7+
- Any options passed into your [redocly.yaml config](https://redocly.com/docs/cli/configuration/) are respected

.changeset/thirty-turkeys-leave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": major
3+
---
4+
5+
⚠️ **Breaking**: additionalProperties no longer have `| undefined` automatically appended

.changeset/warm-masks-decide.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
**Feature**: automatically validate schemas with Redocly CLI ([docs](https://redocly.com/docs/cli/)). No more need for external tools to report errors! 🎉
6+
7+
- By default, it will only throw on actual schema errors (uses Redocly’s default settings)
8+
- For stricter linting or custom rules, you can create a [redocly.yaml config](https://redocly.com/docs/cli/configuration/)

.changeset/wise-coins-hug.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"openapi-typescript": major
3+
---
4+
5+
⚠️ **Breaking**: The Node.js API now returns the TypeScript AST for the main method as well as `transform()` and `postTransform()`. To migrate, you’ll have to use the `typescript` compiler API:
6+
7+
```diff
8+
+ import ts from "typescript";
9+
10+
+ const DATE = ts.factory.createIdentifier("Date");
11+
+ const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
12+
13+
const ast = await openapiTS(mySchema, {
14+
transform(schemaObject, metadata) {
15+
if (schemaObject.format === "date-time") {
16+
- return schemaObject.nullable ? "Date | null" : "Date";
17+
+ return schemaObject.nullable
18+
+ ? ts.factory.createUnionTypeNode([DATE, NULL])
19+
+ : DATE;
20+
}
21+
},
22+
};
23+
```
24+
25+
Though it’s more verbose, it’s also more powerful, as now you have access to additional properties of the generated code you didn’t before (such as injecting comments).
26+
27+
For example syntax, search this codebae to see how the TypeScript AST is used.
28+
29+
Also see [AST Explorer](https://astexplorer.net/)’s `typescript` parser to inspect how TypeScript is interpreted as an AST.

.eslintignore

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

.eslintrc.cjs

+36-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,48 @@ 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",
1549
"no-unused-vars": "off",
1650
},
1751
overrides: [

.prettierrc

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

docs/src/content/docs/node.md

+42-23
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,40 @@ npm i --save-dev openapi-typescript
1515
1616
## Usage
1717

18-
```js
18+
The Node API accepts either a parsed OpenAPI schema in a JS object, or a `string` or `URL` pointing to the location of a schema. It returns `Promise<ts.Node[]>` (an array of TypeScript AST nodes).
19+
20+
```ts
1921
import fs from "node:fs";
2022
import openapiTS from "openapi-typescript";
2123

22-
// example 1: load [object] as schema (JSON only)
24+
// example 1: load [object] as schema (provide a `cwd` to resolve relative $refs)
2325
const schema = await fs.promises.readFile("spec.json", "utf8"); // must be OpenAPI JSON
24-
const output = await openapiTS(JSON.parse(schema));
26+
const ast = await openapiTS(JSON.parse(schema), { cwd: process.cwd() });
2527

26-
// example 2: load [string] as local file (YAML or JSON; released in v4.0)
28+
// example 2: load [string] as local file
2729
const localPath = new URL("./spec.yaml", import.meta.url); // may be YAML or JSON format
28-
const output = await openapiTS(localPath);
30+
const ast = await openapiTS(localPath);
2931

30-
// example 3: load [string] as remote URL (YAML or JSON; released in v4.0)
31-
const output = await openapiTS("https://myurl.com/v1/openapi.yaml");
32+
// example 3: load [string] as remote URL
33+
const ast = await openapiTS("https://myurl.com/v1/openapi.yaml");
3234
```
3335

34-
> **Note**: a YAML string isn’t supported in the Node.js API (you’ll need to <a href="https://www.npmjs.com/package/js-yaml" target="_blank" rel="noopener noreferrer">convert it to JSON</a>). But loading YAML via URL is still supported in Node.js
36+
From the result, you can traverse / manipulate / modify the AST as you see fit.
37+
38+
To convert the TypeScript AST into a string, you can use `astToString()` helper which is a thin wrapper around [TypeScript’s printer](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#re-printing-sections-of-a-typescript-file):
39+
40+
```ts
41+
import { astToString } from "openapi-typescript";
42+
43+
const contents = astToString(ast);
44+
```
3545

3646
## Options
3747

3848
The Node API supports all the [CLI flags](/cli#options) in `camelCase` format, plus the following additional options:
3949

4050
| Name | Type | Default | Description |
4151
| :-------------- | :-------------: | :------ | :------------------------------------------------------------------------------------------------------------------- |
42-
| `commentHeader` | `string` | | Override the default “This file was auto-generated …” file heading |
43-
| `inject` | `string` | | Inject arbitrary TypeScript types into the start of the file |
4452
| `transform` | `Function` | | Override the default Schema Object ➝ TypeScript transformer in certain scenarios |
4553
| `postTransform` | `Function` | | Same as `transform` but runs _after_ the TypeScript transformation |
4654
| `cwd` | `string \| URL` | | (optional) Provide the current working directory to resolve remote `$ref`s (only needed for in-memory JSON objects). |
@@ -50,7 +58,7 @@ The Node API supports all the [CLI flags](/cli#options) in `camelCase` format, p
5058
Use the `transform()` and `postTransform()` options to override the default Schema Object transformer with your own. This is useful for providing nonstandard modifications for specific parts of your schema.
5159

5260
- `transform()` runs **before** the conversion to TypeScript (you’re working with the original OpenAPI nodes)
53-
- `postTransform()` runs **after** the conversion to TypeScript (you’re working with TypeScript types)
61+
- `postTransform()` runs **after** the conversion to TypeScript (you’re working with TypeScript AST)
5462

5563
#### Example: `Date` types
5664

@@ -65,11 +73,18 @@ properties:
6573
6674
By default, openapiTS will generate `updated_at?: string;` because it’s not sure which format you want by `"date-time"` (formats are nonstandard and can be whatever you’d like). But we can enhance this by providing our own custom formatter, like so:
6775

68-
```js
69-
const types = openapiTS(mySchema, {
70-
transform(schemaObject, metadata): string {
71-
if ("format" in schemaObject && schemaObject.format === "date-time") {
72-
return schemaObject.nullable ? "Date | null" : "Date";
76+
```ts
77+
import ts from "typescript";
78+
79+
const DATE = ts.factory.createIdentifier("Date"); // `Date`
80+
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
81+
82+
const ast = await openapiTS(mySchema, {
83+
transform(schemaObject, metadata) {
84+
if (schemaObject.format === "date-time") {
85+
return schemaObject.nullable
86+
? ts.factory.createUnionTypeNode([DATE, NULL])
87+
: DATE;
7388
}
7489
},
7590
});
@@ -93,18 +108,22 @@ Body_file_upload:
93108
file:
94109
type: string;
95110
format: binary;
96-
}
97-
}
98-
}
99111
```
100112
101113
Use the same pattern to transform the types:
102114
103115
```ts
104-
const types = openapiTS(mySchema, {
105-
transform(schemaObject, metadata): string {
106-
if ("format" in schemaObject && schemaObject.format === "binary") {
107-
return schemaObject.nullable ? "Blob | null" : "Blob";
116+
import ts from "typescript";
117+
118+
const BLOB = ts.factory.createIdentifier("Blob"); // `Blob`
119+
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
120+
121+
const ast = await openapiTS(mySchema, {
122+
transform(schemaObject, metadata) {
123+
if (schemaObject.format === "binary") {
124+
return schemaObject.nullable
125+
? ts.factory.createUnionTypeNode([BLOB, NULL])
126+
: BLOB;
108127
}
109128
},
110129
});

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"del-cli": "^5.1.0",
3030
"eslint": "^8.50.0",
3131
"eslint-config-prettier": "^9.0.0",
32+
"eslint-plugin-import": "^2.28.1",
3233
"eslint-plugin-no-only-tests": "^3.1.0",
3334
"eslint-plugin-prettier": "^5.0.0",
3435
"eslint-plugin-vitest": "^0.2.8",

packages/openapi-fetch/examples/nextjs/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"scripts": {
66
"dev": "next dev",
7-
"prepare": "openapi-typescript lib/api/v1.json -o lib/api/v1.d.ts"
7+
"--prepare": "openapi-typescript lib/api/v1.json -o lib/api/v1.d.ts"
88
},
99
"dependencies": {
1010
"next": "13.4.19",

packages/openapi-fetch/examples/react-query/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"scripts": {
66
"dev": "vite dev",
7-
"prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
7+
"--prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
88
},
99
"dependencies": {
1010
"@tanstack/react-query": "^4.35.0",

packages/openapi-fetch/examples/sveltekit/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev": "vite dev",
77
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
88
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
9-
"prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
9+
"--prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
1010
},
1111
"dependencies": {
1212
"openapi-fetch": "workspace:^",

packages/openapi-fetch/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"test": "pnpm run test:ts && npm run test:js",
5959
"test:js": "vitest run",
6060
"test:ts": "tsc --noEmit",
61-
"prepare": "openapi-typescript test/v1.yaml -o test/v1.d.ts",
61+
"--prepare": "openapi-typescript test/v1.yaml -o test/v1.d.ts",
6262
"prepublish": "pnpm run prepare && pnpm run build",
6363
"version": "pnpm run prepare && pnpm run build"
6464
},

packages/openapi-fetch/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"declaration": true,
55
"downlevelIteration": false,
66
"esModuleInterop": true,
7+
"lib": ["ESNext", "DOM"],
78
"module": "NodeNext",
89
"moduleResolution": "NodeNext",
910
"outDir": "dist",

packages/openapi-typescript/CONTRIBUTING.md

+11-10
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,17 @@ pnpm run dev
3838

3939
This will compile the code as you change automatically.
4040

41-
### Writing the PR
41+
#### Tip: use ASTExplorer.net!
4242

43-
**Please fill out the template!** It’s a very lightweight template 🙂.
43+
Working with the TypeScript AST can be daunting. Luckly, there’s [astexplorer.net](https://astexplorer.net) which makes it much more accessible. Rather than trying to build an AST from scratch (which is near impossible), instead:
4444

45-
### Use Test-driven Development!
45+
1. Switch to the **typescript** parser in the top menu
46+
2. Type out code in the left-hand panel
47+
3. Inspect the right-hand panel to see what the desired AST is.
48+
49+
From there, you can refer to existing examples in the codebase. There may even be helper utilities in `src/lib/ts.ts` to make life easier.
50+
51+
#### Tip: Use Test-driven Development!
4652

4753
Contributing to this library is hard-bordering-on-impossible without a [test-driven development (TDD)](https://en.wikipedia.org/wiki/Test-driven_development) strategy. If you’re new to this, the basic workflow is:
4854

@@ -60,7 +66,7 @@ To add a schema as a snapshot test, modify the [/scripts/download-schemas.ts](/s
6066

6167
### Generating types
6268

63-
It may be surprising to hear, but _generating TypeScript types from OpenAPI is opinionated!_ Even though TypeScript and OpenAPI are very close relatives, both being JavaScript/JSON-based, they are nonetheless 2 different languages and thus there is always some room for interpretation. Likewise, some parts of the OpenAPI specification can be ambiguous on how they’re used, and what the expected type outcomes may be (though this is generally for more advanced usecasees, such as specific implementations of `anyOf` as well as [discriminator](https://spec.openapis.org/oas/latest.html#discriminatorObject) and complex polymorphism).
69+
It may be surprising to hear, but generating TypeScript types from OpenAPI is opinionated. Even though TypeScript and OpenAPI are close relativesboth JavaScript/JSON-basedthey are nonetheless 2 different languages and thus there is room for interpretation. Further, some parts of the OpenAPI specification can be ambiguous on how they’re used, and what the expected type outcomes may be (though this is generally for more advanced usecasees, such as specific implementations of `anyOf` as well as [discriminator](https://spec.openapis.org/oas/latest.html#discriminatorObject) and complex polymorphism).
6470

6571
All that said, this library should strive to generate _the most predictable_ TypeScript output for a given schema. And to achieve that, it always helps to open an [issue](https://github.com/drwpow/openapi-typescript/issues) or [discussion](https://github.com/drwpow/openapi-typescript/discussions) to gather feedback.
6672

@@ -131,7 +137,6 @@ pnpm run update:examples
131137

132138
This library has both unit tests (tests that test a tiny part of a schema) and snapshot tests (tests that run over an entire, complete schema). When opening a PR, the former are more valuable than the latter, and are always required. However, updating snapshot tests can help with the following:
133139

134-
- Fixing bugs that deal with multiple schemas with remote `$ref`s
135140
- Fixing Node.js or OS-related bugs
136141
- Adding a CLI option that changes the entire output
137142

@@ -141,8 +146,4 @@ For most PRs, **snapshot tests can be avoided.** But for scenarios similar to th
141146

142147
### When I run tests, it’s not picking up my changes
143148

144-
Be sure to run `pnpm run build` to build the project. Most tests actually test the **compiled JS**, not the source TypeScript. It’s recommended to run `pnpm run dev` as you work so changes are always up-to-date.
145-
146-
### I get an obscure error when testing against my schema
147-
148-
Be sure your schema passes [Redocly lint](https://redocly.com/docs/cli/commands/lint/). Remember this library requires already-validated OpenAPI schemas, so even subtle errors will throw.
149+
Some tests import the **built package** and not the source file. Be sure to run `pnpm run build` to build the project. You can also run `pnpm run dev` as you work so changes are always up-to-date.

0 commit comments

Comments
 (0)