Skip to content

Commit

Permalink
Refactor using TypeScript AST and Redocly OpenAPI core
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Oct 2, 2023
1 parent 8e02a1b commit 6d1eb32
Show file tree
Hide file tree
Showing 90 changed files with 6,886 additions and 5,433 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-students-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

**Feature**: add `formatOptions` to allow formatting TS output
5 changes: 5 additions & 0 deletions .changeset/blue-ladybugs-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": major
---

⚠️ **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.
5 changes: 5 additions & 0 deletions .changeset/giant-scissors-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

**Feature**: add `enum` option to export top-level enums from schemas
5 changes: 5 additions & 0 deletions .changeset/happy-lamps-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": patch
---

🧹 Cleaned up and reorganized all tests
5 changes: 5 additions & 0 deletions .changeset/lazy-ads-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": major
---

⚠️ **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).
5 changes: 5 additions & 0 deletions .changeset/modern-bobcats-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

**Feature**: header responses add `[key: string]: unknown` index type to allow for additional untyped headers
5 changes: 5 additions & 0 deletions .changeset/nasty-candles-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": patch
---

Refactor internals to use TypeScript AST rather than string mashing
9 changes: 9 additions & 0 deletions .changeset/rude-jokes-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"openapi-typescript": major
---

⚠️ **Breaking**: Drop auth/fetching options in favor of Redocly CLI’s

- The `auth`, `httpHeaders`, `httpMethod`, and `fetch` options were all removed from the CLI and Node.js API
- 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)
- Worth noting your `redocly.yaml` config will be respected for any other related settings
5 changes: 5 additions & 0 deletions .changeset/shaggy-adults-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": major
---

⚠️ **Breaking** `defaultNonNullable` option now defaults to `true`. You’ll now need to manually set `false` to return to old behavior.
7 changes: 7 additions & 0 deletions .changeset/shaggy-experts-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"openapi-typescript": minor
---

**Feature**: bundle schemas with Redocly CLI

- Any options passed into your [redocly.yaml config](https://redocly.com/docs/cli/configuration/) are respected
5 changes: 5 additions & 0 deletions .changeset/thirty-turkeys-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": major
---

⚠️ **Breaking**: additionalProperties no longer have `| undefined` automatically appended
8 changes: 8 additions & 0 deletions .changeset/warm-masks-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"openapi-typescript": minor
---

**Feature**: automatically validate schemas with Redocly CLI ([docs](https://redocly.com/docs/cli/)). No more need for external tools to report errors! 🎉

- By default, it will only throw on actual schema errors (uses Redocly’s default settings)
- For stricter linting or custom rules, you can create a [redocly.yaml config](https://redocly.com/docs/cli/configuration/)
29 changes: 29 additions & 0 deletions .changeset/wise-coins-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"openapi-typescript": major
---

⚠️ **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:

```diff
+ import ts from "typescript";

+ const DATE = ts.factory.createIdentifier("Date");
+ const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());

const ast = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "date-time") {
- return schemaObject.nullable ? "Date | null" : "Date";
+ return schemaObject.nullable
+ ? ts.factory.createUnionTypeNode([DATE, NULL])
+ : DATE;
}
},
};
```

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).

For example syntax, search this codebae to see how the TypeScript AST is used.

Also see [AST Explorer](https://astexplorer.net/)’s `typescript` parser to inspect how TypeScript is interpreted as an AST.
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
bin
coverage
dist
examples
38 changes: 36 additions & 2 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,48 @@ module.exports = {
parserOptions: {
project: ["./tsconfig.json"],
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/strict", "plugin:vitest/recommended"],
plugins: ["@typescript-eslint", "no-only-tests", "prettier", "vitest"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/strict",
"plugin:vitest/recommended",
],
plugins: [
"@typescript-eslint",
"import",
"no-only-tests",
"prettier",
"vitest",
],
rules: {
"@typescript-eslint/consistent-indexed-object-style": "off", // sometimes naming keys is more user-friendly
"@typescript-eslint/no-dynamic-delete": "off", // delete is OK
"@typescript-eslint/no-non-null-assertion": "off", // this is better than "as"
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-unnecessary-condition": "off", // this gives bad advice
"arrow-body-style": ["error", "as-needed"],
"dot-notation": "error",
"import/newline-after-import": "error",
"import/order": [
"error",
{
alphabetize: {
order: "asc",
orderImportKind: "asc",
caseInsensitive: true,
},
groups: [
["builtin", "external"],
"internal",
"parent",
"index",
"sibling",
],
},
],
curly: "error",
"object-shorthand": "error", // don’t use foo["bar"]
"no-console": "error",
"no-global-assign": "error",
"no-unused-vars": "off",
},
overrides: [
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"printWidth": 240
"singleAttributePerLine": true
}
65 changes: 42 additions & 23 deletions docs/src/content/docs/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,40 @@ npm i --save-dev openapi-typescript
## Usage

```js
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).

```ts
import fs from "node:fs";
import openapiTS from "openapi-typescript";

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

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

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

> **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
From the result, you can traverse / manipulate / modify the AST as you see fit.

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):

```ts
import { astToString } from "openapi-typescript";

const contents = astToString(ast);
```

## Options

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

| Name | Type | Default | Description |
| :-------------- | :-------------: | :------ | :------------------------------------------------------------------------------------------------------------------- |
| `commentHeader` | `string` | | Override the default “This file was auto-generated …” file heading |
| `inject` | `string` | | Inject arbitrary TypeScript types into the start of the file |
| `transform` | `Function` | | Override the default Schema Object ➝ TypeScript transformer in certain scenarios |
| `postTransform` | `Function` | | Same as `transform` but runs _after_ the TypeScript transformation |
| `cwd` | `string \| URL` | | (optional) Provide the current working directory to resolve remote `$ref`s (only needed for in-memory JSON objects). |
Expand All @@ -50,7 +58,7 @@ The Node API supports all the [CLI flags](/cli#options) in `camelCase` format, p
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.

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

#### Example: `Date` types

Expand All @@ -65,11 +73,18 @@ properties:

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:

```js
const types = openapiTS(mySchema, {
transform(schemaObject, metadata): string {
if ("format" in schemaObject && schemaObject.format === "date-time") {
return schemaObject.nullable ? "Date | null" : "Date";
```ts
import ts from "typescript";

const DATE = ts.factory.createIdentifier("Date"); // `Date`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`

const ast = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "date-time") {
return schemaObject.nullable
? ts.factory.createUnionTypeNode([DATE, NULL])
: DATE;
}
},
});
Expand All @@ -93,18 +108,22 @@ Body_file_upload:
file:
type: string;
format: binary;
}
}
}
```

Use the same pattern to transform the types:

```ts
const types = openapiTS(mySchema, {
transform(schemaObject, metadata): string {
if ("format" in schemaObject && schemaObject.format === "binary") {
return schemaObject.nullable ? "Blob | null" : "Blob";
import ts from "typescript";

const BLOB = ts.factory.createIdentifier("Blob"); // `Blob`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`

const ast = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "binary") {
return schemaObject.nullable
? ts.factory.createUnionTypeNode([BLOB, NULL])
: BLOB;
}
},
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"del-cli": "^5.1.0",
"eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vitest": "^0.2.8",
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "next dev",
"prepare": "openapi-typescript lib/api/v1.json -o lib/api/v1.d.ts"
"--prepare": "openapi-typescript lib/api/v1.json -o lib/api/v1.d.ts"
},
"dependencies": {
"next": "13.4.19",
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/examples/react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "vite dev",
"prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
"--prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
},
"dependencies": {
"@tanstack/react-query": "^4.35.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/examples/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite dev",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
"--prepare": "openapi-typescript src/lib/api/v1.json -o src/lib/api/v1.d.ts"
},
"dependencies": {
"openapi-fetch": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"test": "pnpm run test:ts && npm run test:js",
"test:js": "vitest run",
"test:ts": "tsc --noEmit",
"prepare": "openapi-typescript test/v1.yaml -o test/v1.d.ts",
"--prepare": "openapi-typescript test/v1.yaml -o test/v1.d.ts",
"prepublish": "pnpm run prepare && pnpm run build",
"version": "pnpm run prepare && pnpm run build"
},
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-fetch/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"declaration": true,
"downlevelIteration": false,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
Expand Down
21 changes: 11 additions & 10 deletions packages/openapi-typescript/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ pnpm run dev

This will compile the code as you change automatically.

### Writing the PR
#### Tip: use ASTExplorer.net!

**Please fill out the template!** It’s a very lightweight template 🙂.
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:

### Use Test-driven Development!
1. Switch to the **typescript** parser in the top menu
2. Type out code in the left-hand panel
3. Inspect the right-hand panel to see what the desired AST is.

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.

#### Tip: Use Test-driven Development!

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:

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

### Generating types

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).
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).

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.

Expand Down Expand Up @@ -131,7 +137,6 @@ pnpm run update:examples

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:

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

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

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

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.

### I get an obscure error when testing against my schema

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.
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 comments on commit 6d1eb32

Please sign in to comment.