openapi-typescript
Advanced tools
Comparing version 6.5.0 to 6.5.1
# openapi-typescript | ||
## 6.5.1 | ||
### Patch Changes | ||
- [#1308](https://github.com/drwpow/openapi-typescript/pull/1308) [`ebb73b6`](https://github.com/drwpow/openapi-typescript/commit/ebb73b68c3a2f9a8c8193888735f9c0b7855722f) Thanks [@drwpow](https://github.com/drwpow)! - Fix bugs with remote $refs, add `cwd` option for JSON schema parsing | ||
## 6.5.0 | ||
@@ -4,0 +10,0 @@ |
@@ -25,2 +25,3 @@ import { URL } from "node:url"; | ||
alphabetize: options.alphabetize ?? false, | ||
cwd: options.cwd ?? new URL(`file://${process.cwd()}/`), | ||
defaultNonNullable: options.defaultNonNullable ?? false, | ||
@@ -40,5 +41,20 @@ discriminators: {}, | ||
}; | ||
const isInlineSchema = typeof schema !== "string" && schema instanceof URL === false; | ||
const allSchemas = {}; | ||
const schemaURL = typeof schema === "string" ? resolveSchema(schema) : schema; | ||
let rootURL = schemaURL; | ||
const isInlineSchema = typeof schema !== "string" && schema instanceof URL === false; | ||
if (isInlineSchema) { | ||
if (ctx.cwd) { | ||
if (ctx.cwd instanceof URL) { | ||
rootURL = ctx.cwd; | ||
} | ||
else if (typeof ctx.cwd === "string") { | ||
rootURL = new URL(ctx.cwd, `file://${process.cwd()}/`); | ||
} | ||
rootURL = new URL("root.yaml", rootURL); | ||
} | ||
else { | ||
rootURL = new URL(VIRTUAL_JSON_URL); | ||
} | ||
} | ||
await load(schemaURL, { | ||
@@ -48,3 +64,3 @@ ...ctx, | ||
schemas: allSchemas, | ||
rootURL: isInlineSchema ? new URL(VIRTUAL_JSON_URL) : schemaURL, | ||
rootURL, | ||
urlCache: new Set(), | ||
@@ -170,2 +186,6 @@ httpHeaders: options.httpHeaders, | ||
} | ||
if ("in" in schemaObject) { | ||
subschemaOutput += indent(`${escObjKey(name)}: ${transformParameterObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv); | ||
continue; | ||
} | ||
subschemaOutput += indent(`${escObjKey(name)}: ${transformSchemaObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv); | ||
@@ -178,3 +198,3 @@ } | ||
case "SchemaObject": { | ||
subschemaOutput = transformSchemaObject(subschema.schema, { path, ctx: { ...ctx, indentLv } }); | ||
subschemaOutput = `${transformSchemaObject(subschema.schema, { path, ctx: { ...ctx, indentLv } })};`; | ||
break; | ||
@@ -181,0 +201,0 @@ } |
@@ -7,3 +7,3 @@ import fs from "node:fs"; | ||
import { parseRef, error, makeTSIndex, walk, isRemoteURL, isFilepath } from "./utils.js"; | ||
const EXT_RE = /\.(yaml|yml|json)$/i; | ||
const EXT_RE = /\.(yaml|yml|json)#?\/?/i; | ||
export const VIRTUAL_JSON_URL = `file:///_json`; | ||
@@ -54,3 +54,3 @@ function parseYAML(schema) { | ||
catch (err) { | ||
error(`Cannot parse key: ${k} into JSON format. Continuing with the next HTTP header that is specified`); | ||
error(`Can’t parse key: ${k} into JSON format. Continuing with the next HTTP header that is specified`); | ||
} | ||
@@ -65,6 +65,8 @@ } | ||
const hint = options.hint ?? "OpenAPI3"; | ||
if (schema.href !== options.rootURL.href) | ||
if (schema.href !== options.rootURL.href) { | ||
schemaID = relativePath(options.rootURL, schema); | ||
if (options.urlCache.has(schemaID)) | ||
} | ||
if (options.urlCache.has(schemaID)) { | ||
return options.schemas; | ||
} | ||
options.urlCache.add(schemaID); | ||
@@ -102,3 +104,3 @@ const ext = path.extname(schema.pathname).toLowerCase(); | ||
const contents = fs.readFileSync(schema, "utf8"); | ||
if (ext === ".yaml" || ext === ".yml") | ||
if (ext === ".yaml" || ext === ".yml") { | ||
options.schemas[schemaID] = { | ||
@@ -108,3 +110,4 @@ hint, | ||
}; | ||
else if (ext === ".json") | ||
} | ||
else if (ext === ".json") { | ||
options.schemas[schemaID] = { | ||
@@ -114,2 +117,3 @@ hint, | ||
}; | ||
} | ||
} | ||
@@ -183,15 +187,4 @@ } | ||
const hint = isRemoteFullSchema ? "OpenAPI3" : getHint({ path: hintPath, external: !!ref.filename, startFrom: options.hint }); | ||
if (schema instanceof URL) { | ||
const nextURL = new URL(ref.filename, schema); | ||
const nextID = relativePath(schema, nextURL); | ||
if (options.urlCache.has(nextID)) | ||
return; | ||
refPromises.push(load(nextURL, { ...options, hint })); | ||
node.$ref = node.$ref.replace(ref.filename, nextID); | ||
return; | ||
} | ||
if (isRemoteURL(ref.filename) || isFilepath(ref.filename)) { | ||
const nextURL = new URL(ref.filename.startsWith("//") ? `https://${ref.filename}` : ref.filename); | ||
if (options.urlCache.has(nextURL.href)) | ||
return; | ||
refPromises.push(load(nextURL, { ...options, hint })); | ||
@@ -202,7 +195,9 @@ node.$ref = node.$ref.replace(ref.filename, nextURL.href); | ||
if (options.rootURL.href === VIRTUAL_JSON_URL) { | ||
error(`Can’t resolve "${ref.filename}" from dynamic JSON. Load this schema from a URL instead.`); | ||
error(`Can’t resolve "${ref.filename}" from dynamic JSON. Either load this schema from a filepath/URL, or set the \`cwd\` option: \`openapiTS(schema, { cwd: '/path/to/cwd' })\`.`); | ||
process.exit(1); | ||
} | ||
error(`Can’t resolve "${ref.filename}"`); | ||
process.exit(1); | ||
const nextURL = new URL(ref.filename, schema instanceof URL ? schema : options.rootURL); | ||
const nextID = relativePath(schema instanceof URL ? schema : options.rootURL, nextURL); | ||
refPromises.push(load(nextURL, { ...options, hint })); | ||
node.$ref = node.$ref.replace(ref.filename, nextID); | ||
}); | ||
@@ -269,2 +264,4 @@ await Promise.all(refPromises); | ||
return getHintFromResponseObject(path, external); | ||
case "SchemaMap": | ||
return "SchemaObject"; | ||
default: | ||
@@ -271,0 +268,0 @@ return startFrom; |
/// <reference types="node" resolution-mode="require"/> | ||
import type { URL } from "node:url"; | ||
import type { PathLike } from "node:fs"; | ||
import type { RequestInfo, RequestInit, Response } from "undici"; | ||
@@ -308,3 +308,3 @@ import type { TransformSchemaObjectOptions } from "./transform/schema-object.js"; | ||
emptyObjectsUnknown?: boolean; | ||
cwd?: URL; | ||
cwd?: PathLike; | ||
defaultNonNullable?: boolean; | ||
@@ -363,2 +363,3 @@ transform?: (schemaObject: SchemaObject, options: TransformSchemaObjectOptions) => string | undefined; | ||
alphabetize: boolean; | ||
cwd?: PathLike; | ||
emptyObjectsUnknown: boolean; | ||
@@ -365,0 +366,0 @@ defaultNonNullable: boolean; |
{ | ||
"name": "openapi-typescript", | ||
"description": "Generate TypeScript types from Swagger OpenAPI specs", | ||
"version": "6.5.0", | ||
"description": "Generate runtime-free TypeScript types from Swagger OpenAPI specs", | ||
"version": "6.5.1", | ||
"author": { | ||
@@ -53,6 +53,6 @@ "name": "Drew Powers", | ||
"@types/js-yaml": "^4.0.5", | ||
"@types/node": "^20.4.9", | ||
"@types/node": "^20.5.0", | ||
"degit": "^2.8.4", | ||
"del-cli": "^5.0.0", | ||
"esbuild": "^0.19.0", | ||
"esbuild": "^0.19.2", | ||
"execa": "^7.2.0", | ||
@@ -59,0 +59,0 @@ "vite": "^4.4.9", |
@@ -7,3 +7,3 @@ <img src="../../docs/public/assets/openapi-ts.svg" alt="openapi-typescript" width="200" height="40" /> | ||
**Features** | ||
## Features | ||
@@ -15,8 +15,20 @@ - ✅ Supports OpenAPI 3.0 and 3.1 (including advanced features like <a href="https://spec.openapis.org/oas/v3.1.0#discriminator-object" target="_blank" rel="noopener noreferrer">discriminators</a>) | ||
**Examples** | ||
## Examples | ||
👀 [See examples](./examples/) | ||
## Usage | ||
## Setup | ||
This library requires the latest version of <a href="https://nodejs.org/en" target="_blank" rel="noopener noreferrer">Node.js</a> installed (20.x or higher recommended). With that present, run the following in your project: | ||
```bash | ||
npm i -D openapi-typescript | ||
``` | ||
> ✨ **Tip** | ||
> | ||
> Enabling [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) in `tsconfig.json` can go along way to improve type safety ([read more](/advanced#enable-nouncheckedindexaccess-in-your-tsconfigjson)) | ||
## Basic usage | ||
First, generate a local type file by running `npx openapi-typescript`: | ||
@@ -23,0 +35,0 @@ |
@@ -1,2 +0,2 @@ | ||
import type { GlobalContext, OpenAPI3, OpenAPITSOptions, SchemaObject, Subschema } from "./types.js"; | ||
import type { GlobalContext, OpenAPI3, OpenAPITSOptions, ParameterObject, SchemaObject, Subschema } from "./types.js"; | ||
import type { Readable } from "node:stream"; | ||
@@ -43,2 +43,3 @@ import { URL } from "node:url"; | ||
alphabetize: options.alphabetize ?? false, | ||
cwd: options.cwd ?? new URL(`file://${process.cwd()}/`), | ||
defaultNonNullable: options.defaultNonNullable ?? false, | ||
@@ -59,8 +60,22 @@ discriminators: {}, | ||
// note: we may be loading many large schemas into memory at once; take care to reuse references without cloning | ||
const isInlineSchema = typeof schema !== "string" && schema instanceof URL === false; // eslint-disable-line @typescript-eslint/no-unnecessary-boolean-literal-compare | ||
// 1. load schema (and subschemas) | ||
const allSchemas: { [id: string]: Subschema } = {}; | ||
const schemaURL: URL = typeof schema === "string" ? resolveSchema(schema) : (schema as URL); | ||
let rootURL: URL = schemaURL; | ||
// 1a. if passed as in-memory JSON, handle `cwd` option | ||
const isInlineSchema = typeof schema !== "string" && schema instanceof URL === false; // eslint-disable-line @typescript-eslint/no-unnecessary-boolean-literal-compare | ||
if (isInlineSchema) { | ||
if (ctx.cwd) { | ||
if (ctx.cwd instanceof URL) { | ||
rootURL = ctx.cwd; | ||
} else if (typeof ctx.cwd === "string") { | ||
rootURL = new URL(ctx.cwd, `file://${process.cwd()}/`); | ||
} | ||
rootURL = new URL("root.yaml", rootURL); // give the root schema an arbitrary filename ("root.yaml") | ||
} else { | ||
rootURL = new URL(VIRTUAL_JSON_URL); // otherwise, set virtual filename (which prevents resolutions) | ||
} | ||
} | ||
await load(schemaURL, { | ||
@@ -70,3 +85,3 @@ ...ctx, | ||
schemas: allSchemas, | ||
rootURL: isInlineSchema ? new URL(VIRTUAL_JSON_URL) : schemaURL, // if an inline schema is passed, use virtual URL | ||
rootURL, | ||
urlCache: new Set(), | ||
@@ -191,3 +206,3 @@ httpHeaders: options.httpHeaders, | ||
// This might be a Path Item Object; only way to test is if top-level contains a method (not allowed on Schema Object) | ||
// Test for Path Item Object | ||
if (!("type" in schemaObject) && !("$ref" in schemaObject)) { | ||
@@ -201,2 +216,7 @@ for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) { | ||
} | ||
// Test for Parameter | ||
if ("in" in schemaObject) { | ||
subschemaOutput += indent(`${escObjKey(name)}: ${transformParameterObject(schemaObject as ParameterObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv); | ||
continue; | ||
} | ||
@@ -212,3 +232,3 @@ // Otherwise, this is a Schema Object | ||
case "SchemaObject": { | ||
subschemaOutput = transformSchemaObject(subschema.schema, { path, ctx: { ...ctx, indentLv } }); | ||
subschemaOutput = `${transformSchemaObject(subschema.schema, { path, ctx: { ...ctx, indentLv } })};`; | ||
break; | ||
@@ -215,0 +235,0 @@ } |
@@ -14,3 +14,3 @@ import type { ComponentsObject, Fetch, GlobalContext, OpenAPI3, OperationObject, ParameterObject, PathItemObject, ReferenceObject, RequestBodyObject, ResponseObject, SchemaObject, Subschema } from "./types.js"; | ||
const EXT_RE = /\.(yaml|yml|json)$/i; | ||
const EXT_RE = /\.(yaml|yml|json)#?\/?/i; | ||
export const VIRTUAL_JSON_URL = `file:///_json`; // fake URL reserved for dynamic JSON | ||
@@ -73,3 +73,3 @@ | ||
} catch (err) { | ||
error(`Cannot parse key: ${k} into JSON format. Continuing with the next HTTP header that is specified`); | ||
error(`Can’t parse key: ${k} into JSON format. Continuing with the next HTTP header that is specified`); | ||
} | ||
@@ -104,5 +104,9 @@ } | ||
// normalize ID | ||
if (schema.href !== options.rootURL.href) schemaID = relativePath(options.rootURL, schema); | ||
if (schema.href !== options.rootURL.href) { | ||
schemaID = relativePath(options.rootURL, schema); | ||
} | ||
if (options.urlCache.has(schemaID)) return options.schemas; // exit early if already indexed | ||
if (options.urlCache.has(schemaID)) { | ||
return options.schemas; // exit early if already indexed | ||
} | ||
options.urlCache.add(schemaID); | ||
@@ -144,3 +148,3 @@ | ||
const contents = fs.readFileSync(schema, "utf8"); | ||
if (ext === ".yaml" || ext === ".yml") | ||
if (ext === ".yaml" || ext === ".yml") { | ||
options.schemas[schemaID] = { | ||
@@ -150,3 +154,3 @@ hint, | ||
}; | ||
else if (ext === ".json") | ||
} else if (ext === ".json") { | ||
options.schemas[schemaID] = { | ||
@@ -156,2 +160,3 @@ hint, | ||
}; | ||
} | ||
} | ||
@@ -234,15 +239,4 @@ } | ||
// if root schema is remote and this is a relative reference, treat as remote | ||
if (schema instanceof URL) { | ||
const nextURL = new URL(ref.filename, schema); | ||
const nextID = relativePath(schema, nextURL); | ||
if (options.urlCache.has(nextID)) return; | ||
refPromises.push(load(nextURL, { ...options, hint })); | ||
node.$ref = node.$ref.replace(ref.filename, nextID); | ||
return; | ||
} | ||
// otherwise, if $ref is remote use that | ||
if (isRemoteURL(ref.filename) || isFilepath(ref.filename)) { | ||
const nextURL = new URL(ref.filename.startsWith("//") ? `https://${ref.filename}` : ref.filename); | ||
if (options.urlCache.has(nextURL.href)) return; | ||
refPromises.push(load(nextURL, { ...options, hint })); | ||
@@ -252,9 +246,13 @@ node.$ref = node.$ref.replace(ref.filename, nextURL.href); | ||
} | ||
// if this is dynamic JSON, we have no idea how to resolve external URLs, so throw here | ||
// if this is dynamic JSON (with no cwd), we have no idea how to resolve external URLs, so throw here | ||
if (options.rootURL.href === VIRTUAL_JSON_URL) { | ||
error(`Can’t resolve "${ref.filename}" from dynamic JSON. Load this schema from a URL instead.`); | ||
error(`Can’t resolve "${ref.filename}" from dynamic JSON. Either load this schema from a filepath/URL, or set the \`cwd\` option: \`openapiTS(schema, { cwd: '/path/to/cwd' })\`.`); | ||
process.exit(1); | ||
} | ||
error(`Can’t resolve "${ref.filename}"`); | ||
process.exit(1); | ||
const nextURL = new URL(ref.filename, schema instanceof URL ? schema : options.rootURL); | ||
const nextID = relativePath(schema instanceof URL ? schema : options.rootURL, nextURL); | ||
refPromises.push(load(nextURL, { ...options, hint })); | ||
node.$ref = node.$ref.replace(ref.filename, nextID); | ||
}); | ||
@@ -333,3 +331,8 @@ await Promise.all(refPromises); | ||
/** given a path array (an array of indices), what type of object is this? */ | ||
/** | ||
* Hinting | ||
* A remote `$ref` may point to anything—A full OpenAPI schema, partial OpenAPI schema, Schema Object, Parameter Object, etc. | ||
* The only way to parse its contents correctly is to trace the path from the root schema and infer the type it should be. | ||
* “Hinting” is the process of tracing its lineage back to the root schema to invoke the correct transformations on it. | ||
*/ | ||
export function getHint({ path, external, startFrom }: GetHintOptions): Subschema["hint"] | undefined { | ||
@@ -344,2 +347,4 @@ if (startFrom && startFrom !== "OpenAPI3") { | ||
return getHintFromResponseObject(path, external); | ||
case "SchemaMap": | ||
return "SchemaObject"; | ||
default: | ||
@@ -346,0 +351,0 @@ return startFrom; |
@@ -1,2 +0,2 @@ | ||
import type { URL } from "node:url"; | ||
import type { PathLike } from "node:fs"; | ||
import type { RequestInfo, RequestInit, Response } from "undici"; | ||
@@ -617,3 +617,3 @@ import type { TransformSchemaObjectOptions } from "./transform/schema-object.js"; | ||
/** Specify current working directory (cwd) to resolve remote schemas on disk (not needed for remote URL schemas) */ | ||
cwd?: URL; | ||
cwd?: PathLike; | ||
/** Should schema objects with a default value not be considered optional? */ | ||
@@ -689,2 +689,3 @@ defaultNonNullable?: boolean; | ||
alphabetize: boolean; | ||
cwd?: PathLike; | ||
emptyObjectsUnknown: boolean; | ||
@@ -691,0 +692,0 @@ defaultNonNullable: boolean; |
@@ -5,3 +5,4 @@ { | ||
"sourceRoot": ".", | ||
"outDir": "dist" | ||
"outDir": "dist", | ||
"types": ["vitest/globals"] | ||
}, | ||
@@ -8,0 +9,0 @@ "include": ["scripts", "src", "test", "*.ts"], |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
403618
6806
89