Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/quiet-compilers-coexist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"openapi-typescript": major
---

Support applications using TypeScript 7 by making the JavaScript compiler a runtime dependency instead of a peer dependency. The application compiler and generator compiler can now be installed independently, without aliases or custom loaders.

The generator's compiler is exported as `ts`. **Breaking:** Node API consumers that create, inspect, or print AST nodes must import `ts` from `openapi-typescript` instead of a separately installed `typescript`. This keeps factories, type guards, AST types, and the printer on the same compiler version. CLI usage and generated types are unchanged.

CommonJS declarations now match the existing runtime: `require("openapi-typescript")` returns named exports (including `ts`) and a `.default` generator function, not a directly callable function.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ jobs:
- uses: pnpm/action-setup@v5
with:
run_install: true
- run: pnpm --filter openapi-typescript test:typescript-7
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.0"
- run: pnpm --filter openapi-typescript test:typescript-7 --bun
- run: pnpm --filter openapi-typescript test:typescript-7 --bun --linker isolated
- run: pnpm test
test-e2e:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ This library requires the latest version of [Node.js](https://nodejs.org) instal
npm i -D openapi-typescript typescript
```

Your application can use TypeScript 7. The generator installs its own JavaScript TypeScript compiler dependency, independently of your application's `tsc`. No package aliases or custom loaders are needed. Generated types remain runtime-free.

And in your `tsconfig.json`, to load the types properly:

::: code-group
Expand Down
20 changes: 11 additions & 9 deletions docs/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ The Node API may be useful if dealing with dynamically-created schemas, or you
## Setup

```bash
npm i --save-dev openapi-typescript typescript
npm i --save-dev openapi-typescript
```

The generator installs its own JavaScript TypeScript compiler. Your application can use TypeScript 7 independently. For AST factories, type guards, printers, and AST types, import `ts` from `openapi-typescript` so your code uses the same compiler as the generator.

The JavaScript compiler remains an install-time dependency (about 23.6 MB unpacked for TypeScript 5.9.3). Package managers can share it with a compatible application compiler; TypeScript 7 applications need both versions installed. It is not included in the generated types.

**Migration:** replace `import ts from "typescript"` with `import { ts } from "openapi-typescript"` in code that creates or manipulates the generator's AST. This includes `transform`, `postTransform`, and `transformProperty` callbacks and `ts.Node`/`ts.TypeNode` annotations. Do not mix AST nodes from a different compiler version: their `SyntaxKind` values may differ. This change does not require changing the compiler used to typecheck your application.

::: tip Recommended

For the best experience, use Node ESM by adding `"type": "module"` to `package.json` ([docs](https://nodejs.org/api/esm.html#enabling))
Expand Down Expand Up @@ -115,8 +121,7 @@ By default, openapiTS will generate `updated_at?: string;` because it’s not su
::: code-group

```ts [src/my-project.ts]
import openapiTS from "openapi-typescript";
import ts from "typescript";
import openapiTS, { ts } from "openapi-typescript";

const DATE = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Date")); // `Date`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
Expand Down Expand Up @@ -167,8 +172,7 @@ Use the same pattern to transform the types:
::: code-group

```ts [src/my-project.ts]
import openapiTS from "openapi-typescript";
import ts from "typescript";
import openapiTS, { ts } from "openapi-typescript";

const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Blob")); // `Blob`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
Expand Down Expand Up @@ -220,8 +224,7 @@ Here we return an object with a schema property, which is the same as the above
::: code-group

```ts [src/my-project.ts]
import openapiTS from "openapi-typescript";
import ts from "typescript";
import openapiTS, { ts } from "openapi-typescript";

const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Blob")); // `Blob`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
Expand Down Expand Up @@ -297,8 +300,7 @@ components:

```ts [src/my-project.ts]
import fs from "node:fs";
import ts from "typescript";
import openapiTS, { astToString } from "openapi-typescript";
import openapiTS, { astToString, ts } from "openapi-typescript";

const ast = await openapiTS(mySchema, {
transformProperty(property, schemaObject, options) {
Expand Down
6 changes: 6 additions & 0 deletions packages/openapi-typescript/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export default defineBuildConfig({
declaration: "compatible",
clean: true,
sourcemap: true,
hooks: {
"rollup:dts:options"(_ctx, options) {
// Our CJS bundle exposes .default and named exports, not module.exports = default.
options.plugins = options.plugins.filter((plugin) => plugin.name !== "fix-dts-default-cjs-exports-plugin");
},
},
rollup: {
// Ship CommonJS-compatible bundle
emitCJS: true,
Expand Down
6 changes: 2 additions & 4 deletions packages/openapi-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,19 @@
"test:js": "vitest run",
"test:exports": "pnpm run build && attw --pack .",
"test:examples": "tsc -p tsconfig.examples.json --noEmit",
"test:typescript-7": "node ./scripts/test-typescript-7.mjs",
"update:examples": "pnpm run build && pnpm run download:schemas && vite-node ./scripts/update-examples.ts",
"prepublish": "pnpm run build",
"version": "pnpm run build"
},
"peerDependencies": {
"typescript": "^5.x"
},
"dependencies": {
"@redocly/openapi-core": "^1.34.6",
"ansi-colors": "^4.1.3",
"change-case": "^5.4.4",
"parse-json": "^8.3.0",
"scule": "^1.3.0",
"supports-color": "^10.2.2",
"typescript": "catalog:",
"yargs-parser": "^21.1.1"
},
"devDependencies": {
Expand All @@ -76,7 +75,6 @@
"degit": "2.8.4",
"execa": "catalog:",
"strip-ansi": "7.2.0",
"typescript": "catalog:",
"vite-node": "5.3.0"
}
}
95 changes: 95 additions & 0 deletions packages/openapi-typescript/scripts/test-typescript-7.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";

// Test an installed tarball: workspace links can hide peer/declaration resolution bugs.
// Compile with the application's compiler, not the package's own TS 5 lint command.
const { values } = parseArgs({
options: {
bun: { type: "boolean" },
linker: { type: "string", default: "hoisted" },
},
});
assert.ok(["hoisted", "isolated"].includes(values.linker));
const cwd = mkdtempSync(join(tmpdir(), "openapi-typescript-ts7-"));
const pnpm = process.env.npm_execpath;
assert.ok(pnpm, "Run with pnpm run test:typescript-7");
const run = (args, stdio = "inherit") =>
values.bun
? execFileSync("bun", args, { cwd, stdio })
: execFileSync(process.execPath, [pnpm, ...args], { cwd, stdio });
const install = [
"install",
"--ignore-scripts",
...(values.bun ? ["--linker", values.linker] : ["--strict-peer-dependencies"]),
];

try {
execFileSync(process.execPath, [pnpm, "pack", "--out", join(cwd, "openapi-typescript.tgz")], {
cwd: fileURLToPath(new URL("../", import.meta.url)),
stdio: "inherit",
});
cpSync(new URL("../test/fixtures/typescript-7/", import.meta.url), cwd, { recursive: true });
// Cover a fresh TS 7 install, then downgrade/upgrade without deleting the lockfile or node_modules.
for (const typescript of ["7.0.2", "5.9.3", "7.0.2"]) {
writeFileSync(
join(cwd, "package.json"),
JSON.stringify({
private: true,
type: "module",
dependencies: {
"openapi-typescript": "file:./openapi-typescript.tgz",
typescript,
"@types/node": "25.6.0",
// Redocly's public declarations reference these undeclared type dependencies.
"@types/js-yaml": "4.0.9",
"json-schema-to-ts": "3.1.1",
},
}),
);
// pnpm defaults to frozen installs in CI, but this fixture intentionally changes compiler versions.
run([...install, ...(values.bun ? [] : ["--no-frozen-lockfile"])]);
run([...install, "--frozen-lockfile"]);
const version = execFileSync(process.execPath, ["node_modules/typescript/bin/tsc", "--version"], { cwd });
assert.equal(version.toString().trim(), `Version ${typescript}`);
// Also check the application's PATH: installing the generator must not replace tsc.
const binVersion = run([values.bun ? "run" : "exec", "tsc", "--version"], "pipe");
assert.equal(binVersion.toString().trim(), `Version ${typescript}`);
run([
...(values.bun ? ["--bun", "run"] : ["exec"]),
"openapi-typescript",
"schema.json",
"--output",
"schema.d.ts",
]);
assert.match(readFileSync(join(cwd, "schema.d.ts"), "utf8"), /createdAt\?: string/);
execFileSync(
process.execPath,
[
"node_modules/typescript/bin/tsc",
"--strict",
"--skipLibCheck",
"false",
"--module",
"NodeNext",
"--target",
"ES2022",
"esm.mts",
"cjs.cts",
],
{ cwd, stdio: "inherit" },
);
execFileSync(process.execPath, ["esm.mjs"], { cwd, stdio: "inherit" });
execFileSync(process.execPath, ["cjs.cjs"], { cwd, stdio: "inherit" });
if (values.bun) {
execFileSync("bun", ["esm.mjs"], { cwd, stdio: "inherit" });
execFileSync("bun", ["cjs.cjs"], { cwd, stdio: "inherit" });
}
}
} finally {
rmSync(cwd, { recursive: true, force: true });
}
1 change: 1 addition & 0 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { debug, resolveRef, scanDiscriminators } from "./lib/utils.js";
import transformSchema from "./transform/index.js";
import type { GlobalContext, OpenAPI3, OpenAPITSOptions } from "./types.js";

export { default as ts } from "typescript";
export * from "./lib/ts.js";
export * from "./lib/utils.js";
export { default as transformComponentsObject } from "./transform/components-object.js";
Expand Down
20 changes: 20 additions & 0 deletions packages/openapi-typescript/test/fixtures/typescript-7/cjs.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert = require("node:assert/strict");
import fs = require("node:fs");
import openapiTS = require("openapi-typescript");

async function main() {
const { ts, astToString } = openapiTS;
const ast: openapiTS.ts.Node[] = await openapiTS.default(fs.readFileSync("schema.json", "utf8"), {
transform(schema) {
if (schema.format === "date-time") {
return ts.factory.createTypeReferenceNode("Date");
}
},
});
assert.ok(ast.some(ts.isInterfaceDeclaration));
assert.match(astToString(ast), /createdAt\?: Date/);
}

main().catch((error) => {
throw error;
});
45 changes: 45 additions & 0 deletions packages/openapi-typescript/test/fixtures/typescript-7/esm.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import openapiTS, { astToString, ts, type OpenAPITSOptions } from "openapi-typescript";
import type { components } from "./schema.js";

const require = createRequire(import.meta.url);
const generatorRequire = createRequire(require.resolve("openapi-typescript"));
assert.strictEqual(ts, generatorRequire("typescript"));
assert.strictEqual(ts, require("openapi-typescript").ts);
assert.match(ts.version, /^5\./);

const person: components["schemas"]["Person"] = { name: "Ada" };
assert.equal(person.name, "Ada");
// @ts-expect-error Generated types must still reject invalid data under TypeScript 7.
const invalid: components["schemas"]["Person"] = { name: 123 };

const options: OpenAPITSOptions = {
transform(schema): ts.TypeNode | undefined {
if (schema.format === "date-time") {
return ts.factory.createTypeReferenceNode("Date");
}
},
postTransform(node): ts.TypeNode {
if (node.kind === ts.SyntaxKind.StringKeyword) {
return ts.factory.createUnionTypeNode([node, ts.factory.createLiteralTypeNode(ts.factory.createNull())]);
}
return node;
},
transformProperty(property): ts.PropertySignature {
assert.ok(ts.isPropertySignature(property));
return ts.factory.updatePropertySignature(
property,
[ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)],
property.name,
property.questionToken,
property.type,
);
},
};
const ast: ts.Node[] = await openapiTS(new URL("./schema.json", import.meta.url), options);
assert.ok(ast.some(ts.isInterfaceDeclaration));
const printer: ts.PrinterOptions = { newLine: ts.NewLineKind.LineFeed };
const output = astToString(ast, { formatOptions: printer });
assert.match(output, /readonly name: string \| null/);
assert.match(output, /readonly createdAt\?: Date/);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"openapi": "3.1.0",
"info": { "title": "Compiler compatibility", "version": "1.0.0" },
"components": {
"schemas": {
"Person": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" }
}
}
}
}
}
3 changes: 1 addition & 2 deletions packages/openapi-typescript/test/node-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fileURLToPath } from "node:url";
import ts from "typescript";
import openapiTS, { astToString, COMMENT_HEADER } from "../src/index.js";
import openapiTS, { astToString, COMMENT_HEADER, ts } from "../src/index.js";
import type { OpenAPITSOptions } from "../src/types.js";
import type { TestCase } from "./test-helpers.js";

Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"types": ["vitest/globals"]
},
"include": ["scripts", "src", "test", "*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "test/fixtures/typescript-7"]
}
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading