Conversation
Replace the TypeScript compiler API (ts.factory, createPrinter, createSourceFile) with raw string templates, so the generator no longer depends on TypeScript at runtime. TypeScript 7 ships no classic compiler API, which currently breaks the package twice over: the ^5.x peer range makes installation fail with ERESOLVE, and forcing past it crashes with "Cannot read properties of undefined (reading 'createKeywordTypeNode')". Generation now emits source text directly, reproducing the printer's formatting byte for byte: indentation, comment layout and trailing whitespace, parenthesisation of nested unions/intersections, multi-line tuples, and createStringLiteral's escaping of non-ASCII code units. typescript is dropped from peerDependencies and is never resolved, so the package works under TypeScript 5, 6 and 7 alike. Also fixes two cases that produced invalid TypeScript: a multi-line x-enum-descriptions entry leaked a bare token into the enum body, and pathParamsAsTypes emitted an unterminated template literal for a URL containing a backtick. BREAKING CHANGE: openapiTS() resolves to a string instead of ts.Node[], and the transform/postTransform/transformProperty hooks exchange strings and plain objects instead of AST nodes.
👷 Deploy request for openapi-ts pending review.Visit the deploys page to approve it
|
🦋 Changeset detectedLatest commit: 26f4233 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Open question: the
|
CI note:
|
|
Follow-up: the stuck
Once #2869 lands, rebasing this PR on |
Replace the generator's TypeScript compiler API with source-string builders so applications can install TypeScript 7 without a peer dependency conflict or a runtime failure at ts.factory. Generation also works without TypeScript installed. Keep TypeScript as a workspace development dependency for builds and semantic tests, rather than requiring a second compiler in consumers. Adapt upstream PR openapi-ts#2868 (26f4233) onto this fork's TypeScript 6 and required-only allOf improvements. This includes fixes and regression coverage beyond the upstream string-template implementation. Generator implementation - Migrate the schema transforms, document assembly, and supporting utilities from AST nodes to TypeScript source fragments. Centralize declarations, properties, index signatures, tuples, arrays, enums, unions, intersections, literals, references, and comments in the existing ts helper module. - Preserve established indentation and literal escaping where applicable. Keep expression fragments distinct from already-indented declaration and property lines through the builders' documented conventions. - Retain deferred assembly of the operations interface so operations can be discovered incrementally without changing its position among footer types. - Use context-sensitive parentheses for arrays and composed types, including readonly/keyof/typeof operands, functions, constructors, and conditionals. Skip quoted strings, nested templates, and comments when inspecting types. Recognize LF, CR, CRLF, U+2028, and U+2029 line-comment boundaries. - Deduplicate generated helpers by their canonical declaration strings. An enum value or comment mentioning a helper name must not suppress the actual WithRequired, ReadonlyArray, or FlattenedDeepRequired declaration. - Write fixed helper declarations as multiline templates rather than long strings containing escaped newlines, preserving their emitted bytes. Preserve this fork's semantics - Carry the full generated Readable/Writable implementation into the string emitter: the any fast path, recursive marker resolution, opaque callables, readonly collection methods and iterators, required numeric properties, extra data properties, and tuple length/position preservation. - Keep callable arguments, results, and attached properties opaque while retaining visibility filtering on noncallable Date/RegExp data properties. Preserve the existing mutable-tuple projection and readonly never behavior. - Keep the readonly-data mapping inline in generated helpers so a schema root named ReadonlyArrayData cannot collide with an implementation alias. The separate helpers package's existing implementation remains unchanged. - Port required-only allOf handling without discarding known-key discovery, conservative safety gates, discriminator behavior, or callback occurrence tracking. Do not probe or replay user callbacks to infer their behavior. - Implement named and collision-safe inline WithRequiredObject forms from the same string body, preserving object constraints, readonly properties, exact optional semantics, and rejection of nonobject/callable inputs. - Inline the object-constraint helper when injected source, generated names, or arbitrary footer declarations make its fixed name unsafe. Keep existing conservative handling when property hooks can rename keys or deprecated fields are excluded; do not expand unrelated allOf behavior. Correct generation regressions - Render an array's element type once, then add its outer array dimension. Do not infer array/tuple structure from an emitted string's prefix/suffix. This preserves arrays of unions, tuple items, constant arrays, custom array types, and nested arrayLength elements independently of composition order. - Emit readonly (readonly number[])[] for nested immutable arrays rather than invalid readonly readonly number[][]. Preserve parentheses around other low-precedence element types and rest-array operands as needed. - Escape backticks, backslashes, and interpolation delimiters in generated path template literals. Match escaped parameter placeholders after escaping the URL so unusual parameter names still become typed interpolations. - Flatten multiline enum descriptions into a single line comment, and omit redundant single-member union/intersection wrappers. - Recognize nonempty braced root types independently of inline formatting and surrounding comments. Retain inner literal/template text, distinguish comment-only empty bodies, and reject outer unions/intersections/conditionals instead of turning them into invalid interface members. - When postTransform is configured, emit nonempty object-shaped $defs roots as aliases, including when the hook leaves the root unchanged. This permits mapped root types without adding a TypeScript grammar classifier. Keep the existing fallback for empty, nonbraced, and composed roots. Public API migration (major) - openapiTS() returns Promise<string>, containing the generated body with a trailing newline, instead of Promise<ts.Node[]>. - transform returns a type string or { schema: string, questionToken }; postTransform receives and returns type strings. - transformProperty receives and returns the structured object { name, optional, readonly, type, comment?, indent }. Initialize readonly from the schema/options and honor the returned value for both ordinary properties and $defs properties. Keep name, optionality, type, and JSDoc customization available without requiring callers to parse a property line. - Provide tsComment() for indented JSDoc. GlobalContext.injectFooter now holds source declarations or deferred OperationsDeclaration instances. - Replace AST-only helpers such as stringToAST(), tsModifiers(), and QUESTION_TOKEN with source builders. Existing AST callbacks must migrate; changing the application's compiler version alone is not that migration. - Retain astToString() as a source-fragment joiner/trailing-newline helper. Reject AST inputs and the removed printer-options argument explicitly instead of accepting obsolete formatting options and silently ignoring them. - Emit injected source without parsing and reprinting it through TypeScript. Document this behavior, the callback migration, and the $defs alias policy. Packaging, documentation, and CI - Remove the generator's TypeScript peer dependency. Published runtime code and declarations contain no TypeScript compiler imports. - Correct CommonJS declarations to match the existing runtime namespace with default and named exports. Disable unbuild's declaration-only export-equals rewrite for this package through its supported declaration-options hook; keep the actual CommonJS runtime export shape unchanged. - Publish @types/js-yaml and json-schema-to-ts because Redocly's public declarations reference them. Update the lockfile so strict consumers do not need undeclared dependencies or skipLibCheck to typecheck the API. - Update English, Japanese, and Chinese Node API documentation, installation examples, contributor guidance, and the major changeset. - Add an isolated packed-consumer test using the actual CLI, ESM and CommonJS APIs, all three hooks, public declarations, and generated type assertions. Reuse the existing helpers assertion suite against the packed helpers and both generated mutable/immutable helper implementations, including negative assertions for accidental widening or lost visibility/readonly constraints. - Cover required-only allOf separately from property-hook customization, respecting the fork's existing safety gate. Include commented mapped-root output in the packed compiler checks as well as the focused unit tests. - Keep network-dependent consumer installs separate from the normal unit suite. Run them outside workspace module resolution, with install scripts disabled and compiler-free resolution checked explicitly. Assert that packed artifacts exclude tests/scripts; exclude consumer fixture sources from the generator's ordinary tsconfig until they are generated in isolation. - Add two consumer CI jobs, one each for Node 22 and 24. Each builds once and checks TypeScript 5.9.3, 6.0.3, 7.0.2, and no installed compiler in separate consumer projects. Retain all eight combinations without eight repeated workspace installs/builds, alongside the existing TS5/TS6 workspace matrix. Validation and scope - Final uncached workspace suite on Node 24 / TypeScript 6: 865 passing tests (377 generator, 451 fetch, 37 React Query), plus helper type assertions, example compilation, builds, and package export checks. - Generator suite on TypeScript 5.9.3: 377 passing tests on both Node 22.23.2 and Node 24.19.0. Generator, example, and helper typechecks pass on both. - All eight final packed-consumer combinations pass on Node 22.23.2 and 24.19.0, including TypeScript 7.0.2 strict declarations and compiler-free CLI/API generation. Package lint/typechecks, documentation build, and git diff --check pass; existing Biome configuration warnings remain. - Framework typechecks for Vue, SvelteKit, and Next.js, plus the nonstrict fetch typecheck, passed during integration. Browser end-to-end tests were not rerun as part of this generator adaptation. - Three independent review rounds covered correctness, maintainability, minimality, packaging, and repository conventions. Keep the established allOf safeguards and deferred operations representation; avoid a general type parser, footer registry, or unrelated historical arrayLength changes. - Consumer compatibility does not imply running all workspace build tools under TS7: workspace development continues to use the classic TS5/TS6 API. Upstream: openapi-ts#2868 Context: openapi-ts#2841 Fork TS6 work: 219be04 Co-authored-by: im10furry <im10furry@users.noreply.github.com>
Changes
Closes #2841. Supersedes #2867. Related: #2862, #2818.
TypeScript 7 (the native compiler) is
lateston npm and its root module ships no classiccompiler API.
openapi-typescriptbuilt its output withts.factory/createPrinter/createSourceFile, so a TS7 application hits two hard blockers:npm i typescript@7 openapi-typescriptfails outright:--legacy-peer-deps) crashes on first import:This PR does what @mrlubos asked for in #2841 — "remove the entire TypeScript dependency and switch
to raw string templates" — so TypeScript is no longer a dependency at all.
src/lib/ts.tsrewritten as a string builder. The helper surface keeps its shape (tsUnion,tsLiteral,oapiRef,tsEnum, …) but now returns source text, plus new line-oriented builders(
typeLiteral,tupleType,propertySignature,indexSignature,typeAlias,interfaceDecl,enumDecl) andtsComment()for JSDoc.lib/utils.tsandtransform/index.tsmigrated. Multi-line constructs threadan explicit indentation level, reproducing the printer's 4-space nesting exactly.
trailing whitespace stripped on every comment line; nested unions/intersections parenthesised
(including inside array types and after
readonly); tuples always multi-line while{}is inline;string literals escaped the way
createStringLiteraldoes ("emoji🎉"→"emoji\uD83C\uDF89",while the
tsLiteralUTF-8 workaround stays verbatim); and the$defs/ template-literal /operationsassembly keeps its exact shape.openapiTS()resolves to astring— the complete file body, ending in a newline.astToString()is retained as a normalizer for older call sites.peerDependencies.typescriptremoved;typescriptstays a devDependency for the repo's owntsc. The builtdist/contains zero references totypescript.Breaking changes (major)
openapiTS(): Promise<ts.Node[]>openapiTS(): Promise<string>transform→ts.TypeNode | TransformObjecttransform→string | { schema: string; questionToken: boolean }postTransform(type: ts.TypeNode)postTransform(type: string)transformProperty(property: ts.PropertySignature)transformProperty(property: { name; optional; type; comment?; indent })GlobalContext.injectFooter: ts.Node[]GlobalContext.injectFooter: FooterDeclaration[]stringToAST(),tsModifiers(),QUESTION_TOKENMigration for AST callbacks is mechanical:
ts.factory.createTypeReferenceNode("Date")→"Date".Attach JSDoc from
transformPropertywithtsComment(tags, property.indent).The
injectoption is now emitted verbatim rather than re-printed by the printer.Two bugs fixed along the way
Both produced invalid TypeScript; both are covered by new tests.
x-enum-descriptionsentry leaked a bare token into the enum body(
// line1\nline2→ now// line1 line2).pathParamsAsTypes, a URL containing a backtick broke out of the generated template literaltype (
`/a`b/${string}`→ now`/a\`b/${string}`).One deliberate, semantically-neutral output change:
tsUnion()/tsIntersection()no longer emit aredundant single-member union, so
(string)[][]becomesstring[][].How to Review
packages/openapi-typescript/src/lib/ts.ts. Everything else is a mechanical portagainst it;
transform/schema-object.tsis the largest consumer.ts.factory.createX(...)becominga string builder call plus an
indentargument. The interesting parts are$defs, the array /arrayLengthbranches, and theoperationsassembly.transform/index.tskeeps atypeLiteralMembers()helper that mirrors the old.members?.lengthcheck on the generated AST node, which is how the root
paths/webhooks/components/$defsinterface-vs-fallback decision still works.
stringripples throughevery transform, so the repo cannot be green in between. The byte-exact snapshot suite is what
makes that safe.
Verification
Output parity. Every committed example snapshot diffs clean, including the 123,077-line GitHub
API outputs:
Beyond the committed fixtures, I ran the published 7.13.0 and this branch side by side over a
purpose-built schema (discriminators, allOf/oneOf/anyOf, patternProperties,
$defs, prefixItems,readOnly/writeOnly,
x-enum-*metadata, non-ASCII in descriptions, enum values, property names and$reftargets) across 27 option combinations — including a kitchen-sink run — and every pair isbyte-identical.
pnpm run update:examplesproduces no diff.Suite.
pnpm test→ 26 files / 297 tests pass (byte-exacttoMatchFileSnapshotfixtures, inlinewantassertions, CLI snapshot tests, CJS bundle, Node API).tsc --noEmit,tsc -p tsconfig.examples.json --noEmitandbiome checkare clean (the 3 remainingnoTemplateCurlyInStringwarnings are pre-existing on untouched lines).Compiler matrix — same schema generated, then type-checked with each compiler:
tsc --noEmit --stricton outputEnd-to-end under TS7, the exact scenario from #2841:
With no TypeScript installed at all, both the CLI and the Node API (including
transform,postTransformandtransformProperty) work —typescriptdoes not even resolve from the consumerproject.
Checklist
docs/updated (if necessary)pnpm run update:examplesrun (only applicable for openapi-typescript)Notes
ja/zhtranslations ofdocs/node.mdstill show the old AST examples and need a follow-uptranslation pass.
openapi-typescript-helpers,openapi-fetch, or any other package.