Skip to content

Commit d2aa96f

Browse files
committed
Load remote schemas
1 parent 8c2ada5 commit d2aa96f

Some content is hidden

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

58 files changed

+925
-237
lines changed

README.md

+12-9
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010

1111
🚀 Convert [OpenAPI 3.0][openapi3] and [2.0 (Swagger)][openapi2] schemas to TypeScript interfaces using Node.js.
1212

13-
💅 The output is prettified with [Prettier][prettier] (and can be customized!).
13+
**Features**
1414

15-
👉 Works for both local and remote resources (filesystem and HTTP).
15+
- Convert [Open API 3.x][openapi3] and [Swagger 2.x][openapi2] to TypeScript types
16+
- Load schemas either from local `.yaml` or `.json` files, or from a remote URL (simple authentication supported with the `--auth` flag)
17+
- Supports remote `$ref`s using [json-schema-ref-parser][json-schema-ref-parser]
18+
- Formats output using [Prettier][prettier]
19+
- Uses the latest TypeScript 4.0 syntax
1620

17-
View examples:
21+
**Examples**
1822

1923
- [Stripe, OpenAPI 2.0](./examples/stripe-openapi2.ts)
2024
- [Stripe, OpenAPI 3.0](./examples/stripe-openapi3.ts)
@@ -109,24 +113,22 @@ npm i --save-dev openapi-typescript
109113
const fs = require("fs");
110114
const openapiTS = require("openapi-typescript").default;
111115

112-
// option 1: load JS object, write to local file
116+
// option 1: load [object] as schema (JSON only)
113117
const schema = await fs.promises.readFile("spec.json", "utf8") // must be OpenAPI JSON
114118
const output = await openapiTS(JSON.parse(schema));
115119

116-
// option 2 (new in v3.3): load local path
120+
// option 2: load [string] as local file (YAML or JSON; released in v3.3)
117121
const localPath = path.join(__dirname, 'spec.yaml'); // may be YAML or JSON format
118122
const output = await openapiTS(localPath);
119123

120-
// option 3 (new in v3.3): load remote URL
124+
// option 3: load [string] as remote URL (YAML or JSON; released in v3.3)
121125
const output = await openapiTS('https://myurl.com/v1/openapi.yaml');
122126
```
123127

124-
The Node API may be useful if dealing with dynamically-created schemas, or you’re using within context of a larger application. It
128+
The Node API may be useful if dealing with dynamically-created schemas, or you’re using within context of a larger application. Pass in either a JSON-friendly object to load a schema from memory, or a string to load a schema from a local file or remote URL (it will load the file quickly using built-in Node methods). Note that a YAML string isn’t supported in the Node.js API; either use the CLI or convert to JSON using [js-yaml][js-yaml] first.
125129

126130
⚠️ As of `v3.3`, this is an async function.
127131

128-
It’s important to note that options 2 and 3 are triggered by passing in a `string` rather than an `object`.
129-
130132
#### Custom Formatter
131133

132134
If using the Node.js API, you can optionally pass a **formatter** to openapi-typescript. This is useful if you want to override the default types and substitute your own.
@@ -171,6 +173,7 @@ encouraged but not required.
171173

172174
[glob]: https://www.npmjs.com/package/glob
173175
[js-yaml]: https://www.npmjs.com/package/js-yaml
176+
[json-schema-ref-parser]: https://github.com/APIDevTools/json-schema-ref-parser
174177
[namespace]: https://www.typescriptlang.org/docs/handbook/namespaces.html
175178
[npm-run-all]: https://www.npmjs.com/package/npm-run-all
176179
[openapi-format]: https://swagger.io/specification/#data-types

bin/cli.js

+5-16
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,15 @@ function errorAndExit(errorMessage) {
6969
async function generateSchema(pathToSpec) {
7070
const output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT
7171

72-
// load spec
73-
if (!cli.flags.output) {
74-
output = "STDOUT"; // if --output not specified, fall back to stdout
75-
}
76-
if (output === "FILE") {
77-
console.info(bold(`✨ openapi-typescript ${require("../package.json").version}`)); // don’t log anything to console!
78-
}
79-
if (cli.flags.rawSchema && !cli.flags.version) {
80-
throw new Error(`--raw-schema requires --version flag`);
81-
}
82-
8372
// generate schema
8473
const result = await openapiTS(pathToSpec, {
85-
auth: cli.flags.auth,
8674
additionalProperties: cli.flags.additionalProperties,
87-
silent: output === "STDOUT",
88-
immutableTypes: cli.flags.immutableTypes,
75+
auth: cli.flags.auth,
8976
defaultNonNullable: cli.flags.defaultNonNullable,
77+
immutableTypes: cli.flags.immutableTypes,
9078
prettierConfig: cli.flags.prettierConfig,
9179
rawSchema: cli.flags.rawSchema,
80+
silent: output === OUTPUT_STDOUT,
9281
version: cli.flags.version,
9382
});
9483

@@ -108,14 +97,14 @@ async function generateSchema(pathToSpec) {
10897
console.log(green(`🚀 ${pathToSpec} -> ${bold(outputFile)} [${time}ms]`));
10998
} else {
11099
process.stdout.write(result);
111-
// (still) don’t log anything to console!
100+
// if stdout, (still) don’t log anything to console!
112101
}
113102

114103
return result;
115104
}
116105

117106
async function main() {
118-
const output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT
107+
let output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT
119108
const pathToSpec = cli.input[0];
120109

121110
if (output === OUTPUT_FILE) {

package-lock.json

+25-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,17 @@
5959
"kleur": "^4.1.4",
6060
"meow": "^9.0.0",
6161
"mime": "^2.5.2",
62+
"node-fetch": "^2.6.1",
6263
"prettier": "^2.3.0",
6364
"tiny-glob": "^0.2.9"
6465
},
6566
"devDependencies": {
6667
"@types/jest": "^26.0.23",
6768
"@types/js-yaml": "^4.0.1",
6869
"@types/mime": "^2.0.3",
69-
"@typescript-eslint/eslint-plugin": "^4.26.0",
70-
"@typescript-eslint/parser": "^4.26.0",
70+
"@types/node-fetch": "^2.5.10",
71+
"@typescript-eslint/eslint-plugin": "^4.25.0",
72+
"@typescript-eslint/parser": "^4.25.0",
7173
"codecov": "^3.8.2",
7274
"eslint": "^7.27.0",
7375
"eslint-config-prettier": "^8.3.0",

src/index.ts

+54-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import path from "path";
2+
import { bold, yellow } from "kleur";
23
import prettier from "prettier";
34
import parserTypescript from "prettier/parser-typescript";
4-
import load from "./load";
5+
import load, { resolveSchema } from "./load";
56
import { swaggerVersion } from "./utils";
67
import { transformAll } from "./transform/index";
78
import { GlobalContext, OpenAPI2, OpenAPI3, SchemaObject, SwaggerToTSOptions } from "./types";
@@ -16,28 +17,69 @@ export const WARNING_MESSAGE = `/**
1617
`;
1718

1819
export default async function openapiTS(
19-
schema: OpenAPI2 | OpenAPI3 | Record<string, SchemaObject>,
20-
options: SwaggerToTSOptions = {}
20+
schema: string | OpenAPI2 | OpenAPI3 | Record<string, SchemaObject>,
21+
options: SwaggerToTSOptions = {} as any
2122
): Promise<string> {
22-
// 1. set up context
2323
const ctx: GlobalContext = {
2424
additionalProperties: options.additionalProperties || false,
2525
auth: options.auth,
2626
defaultNonNullable: options.defaultNonNullable || false,
27-
formatter: typeof options.formatter === "function" ? options.formatter : undefined,
27+
formatter: options && typeof options.formatter === "function" ? options.formatter : undefined,
2828
immutableTypes: options.immutableTypes || false,
2929
rawSchema: options.rawSchema || false,
30-
version: options.version || swaggerVersion(schema as OpenAPI2 | OpenAPI3),
30+
version: options.version || 3,
3131
} as any;
3232

33-
// 2. generate output
33+
// note: we may be loading many large schemas into memory at once; take care to reuse references without cloning
34+
35+
// 1. load schema
36+
let rootSchema: Record<string, any> = {};
37+
let external: Record<string, Record<string, any>> = {};
38+
if (typeof schema === "string") {
39+
const schemaURL = resolveSchema(schema);
40+
if (options.silent === false) console.log(yellow(`🔭 Loading spec from ${bold(schemaURL.href)}…`));
41+
const schemas: Record<string, Record<string, any>> = {};
42+
await load(schemaURL, {
43+
...ctx,
44+
schemas,
45+
rootURL: schemaURL, // as it crawls schemas recursively, it needs to know which is the root to resolve everything relative to
46+
});
47+
for (const k of Object.keys(schemas)) {
48+
if (k === schemaURL.href) {
49+
rootSchema = schemas[k];
50+
} else {
51+
external[k] = schemas[k];
52+
}
53+
}
54+
} else {
55+
rootSchema = schema;
56+
}
57+
58+
// 2. generate raw output
3459
let output = WARNING_MESSAGE;
35-
const schemaObj = await load(schema, { auth: ctx.auth, silent: options?.silent || false });
36-
const rootTypes = transformAll(schemaObj, { ...ctx });
60+
61+
// 2a. root schema
62+
if (!options?.version && !ctx.rawSchema) ctx.version = swaggerVersion(rootSchema as any); // note: root version cascades down to all subschemas
63+
const rootTypes = transformAll(rootSchema, { ...ctx });
3764
for (const k of Object.keys(rootTypes)) {
38-
if (typeof rootTypes[k] !== "string") continue;
39-
output += `export interface ${k} {\n ${rootTypes[k]}\n}\n\n`;
65+
if (typeof rootTypes[k] === "string") {
66+
output += `export interface ${k} {\n ${rootTypes[k]}\n}\n\n`;
67+
}
68+
}
69+
70+
// 2b. external schemas (subschemas)
71+
output += `export interface external {\n`;
72+
const externalKeys = Object.keys(external);
73+
externalKeys.sort((a, b) => a.localeCompare(b, "en", { numeric: true })); // sort external keys because they may have resolved in a different order each time
74+
for (const subschemaURL of externalKeys) {
75+
output += ` "${subschemaURL}": {\n`;
76+
const subschemaTypes = transformAll(external[subschemaURL], { ...ctx, namespace: subschemaURL });
77+
for (const k of Object.keys(subschemaTypes)) {
78+
output += ` "${k}": {\n ${subschemaTypes[k]}\n }\n`;
79+
}
80+
output += ` }\n`;
4081
}
82+
output += `}\n\n`;
4183

4284
// 3. Prettify
4385
let prettierOptions: prettier.Options = {
@@ -46,7 +88,7 @@ export default async function openapiTS(
4688
};
4789
if (options && options.prettierConfig) {
4890
try {
49-
const userOptions = prettier.resolveConfig.sync(path.resolve(process.cwd(), options.prettierConfig));
91+
const userOptions = await prettier.resolveConfig(path.resolve(process.cwd(), options.prettierConfig));
5092
prettierOptions = {
5193
...(userOptions || {}),
5294
...prettierOptions,

0 commit comments

Comments
 (0)