···11export type { IBiMap as BiMap } from './bimap/types';
22export type { IBinding as Binding } from './bindings/types';
33export { createBinding, mergeBindings } from './bindings/utils';
44+export { debug } from './debug';
45export type {
56 IProjectRenderMeta as ProjectRenderMeta,
67 ISymbolMeta as SymbolMeta,
+35
packages/codegen-core/src/symbols/symbol.ts
···11+import { debug } from '../debug';
12import type { ISymbolMeta } from '../extensions';
23import type { IFileOut } from '../files/types';
34import { wrapId } from '../renderer/utils';
···171172 );
172173 }
173174175175+ /**
176176+ * Custom file path resolver, if provided.
177177+ */
174178 get getFilePath(): ((symbol: Symbol) => string | undefined) | undefined {
175179 return this.canonical._getFilePath;
176180 }
···196200 return this.canonical._meta;
197201 }
198202203203+ /**
204204+ * User-intended name before aliasing or conflict resolution.
205205+ */
199206 get name(): string {
200207 return this.canonical._name;
201208 }
···211218 * Add a direct dependency on another symbol.
212219 */
213220 addDependency(symbol: Symbol): void {
221221+ this.assertCanonical();
214222 if (symbol !== this) this._dependencies.add(symbol);
215223 }
216224···232240 * @param exported — Whether the symbol is exported.
233241 */
234242 setExported(exported: boolean): void {
243243+ this.assertCanonical();
235244 this._exported = exported;
236245 }
237246···241250 * @param list — Source files re‑exporting this symbol.
242251 */
243252 setExportFrom(list: ReadonlyArray<string>): void {
253253+ this.assertCanonical();
244254 this._exportFrom = list;
245255 }
246256···250260 * This may only be set once.
251261 */
252262 setFile(file: IFileOut): void {
263263+ this.assertCanonical();
253264 if (this._file && this._file !== file) {
254265 throw new Error('Symbol is already assigned to a different file.');
255266 }
···262273 * This may only be set once.
263274 */
264275 setFinalName(name: string): void {
276276+ this.assertCanonical();
265277 if (this._finalName && this._finalName !== name) {
266278 throw new Error('Symbol finalName has already been resolved.');
267279 }
···274286 * @param kind — The import strategy (named/default/namespace).
275287 */
276288 setImportKind(kind: SymbolImportKind): void {
289289+ this.assertCanonical();
277290 this._importKind = kind;
278291 }
279292···283296 * @param kind — The new symbol kind.
284297 */
285298 setKind(kind: SymbolKind): void {
299299+ this.assertCanonical();
286300 this._kind = kind;
287301 }
288302···292306 * @param name — The new name.
293307 */
294308 setName(name: string): void {
309309+ this.assertCanonical();
295310 this._name = name;
296311 }
297312···301316 * This may only be set once.
302317 */
303318 setRootNode(node: ISyntaxNode): void {
319319+ this.assertCanonical();
304320 if (this._rootNode && this._rootNode !== node) {
305321 throw new Error('Symbol is already bound to a different root DSL node.');
306322 }
···312328 */
313329 toString(): string {
314330 return `[Symbol ${this.name}#${this.id}]`;
331331+ }
332332+333333+ /**
334334+ * Ensures this symbol is canonical before allowing mutation.
335335+ *
336336+ * A symbol that has been marked as a stub (i.e., its `_canonical` points
337337+ * to a different symbol) may not be mutated. This guard throws an error
338338+ * if any setter attempts to modify a stub, preventing accidental writes
339339+ * to non‑canonical instances.
340340+ *
341341+ * @throws {Error} If the symbol is a stub and is being mutated.
342342+ * @private
343343+ */
344344+ private assertCanonical(): void {
345345+ if (this._canonical && this._canonical !== this) {
346346+ const message = `Illegal mutation of stub symbol ${this.toString()} → canonical: ${this._canonical.toString()}`;
347347+ debug(message, "symbol");
348348+ throw new Error(message);
349349+ }
315350 }
316351}
+1-1
packages/openapi-ts/README.md
···11<div align="center">
22- <img alt="Two people looking at the blueprint" height="214" src="https://heyapi.dev/images/blueprint-640w.png" width="320">
22+ <img alt="Two people looking at the TypeScript logo" height="214" src="https://heyapi.dev/images/hero-920w.png" width="320">
33 <h1><b>OpenAPI TypeScript</b></h1>
44 <p><em>“OpenAPI codegen that just works.”</em><br/><sub>— Guillermo Rauch, CEO of Vercel</sub></p>
55</div>
+1-1
packages/openapi-ts/src/cli.ts
···8585 }
8686 : {};
87878888- if (userConfig.debug || stringToBoolean(process.env.DEBUG)) {
8888+ if (userConfig.debug) {
8989 (userConfig.logs as Record<string, unknown>).level = 'debug';
9090 delete userConfig.debug;
9191 } else if (userConfig.silent) {
+65-13
packages/openapi-ts/src/ts-dsl/base.ts
···11// TODO: symbol should be protected, but needs to be public to satisfy types
22import type { Symbol, SyntaxNode } from '@hey-api/codegen-core';
33+import { debug } from '@hey-api/codegen-core';
34import ts from 'typescript';
4556export type MaybeArray<T> = T | ReadonlyArray<T>;
···910 $render(): T;
1011}
11121313+// @deprecated
1214export type Constructor<T = ITsDsl> = new (...args: ReadonlyArray<any>) => T;
13151416export abstract class TsDsl<T extends ts.Node = ts.Node> implements ITsDsl<T> {
1717+ /** Render this DSL node into a concrete TypeScript AST node. */
1818+ protected abstract _render(): T;
1919+1520 /** Walk this node and its children with a visitor. */
1621 abstract traverse(visitor: (node: SyntaxNode) => void): void;
1717-1818- /** Render this DSL node into a concrete TypeScript AST node. */
1919- abstract $render(): T;
20222123 /** Parent DSL node in the constructed syntax tree. */
2224 protected parent?: TsDsl<any>;
···99101 return this;
100102 }
101103104104+ /** Render this DSL node into a concrete TypeScript AST node. */
105105+ $render(): T {
106106+ if (!this.parent) {
107107+ this._validate();
108108+ }
109109+ return this._render();
110110+ }
111111+102112 /** Returns all locally declared names within this node. */
103113 getLocalNames(): Iterable<string> {
104114 return [];
···118128 /** Assigns the parent DSL node, enforcing a single-parent invariant. */
119129 setParent(parent: TsDsl<any>): this {
120130 if (this.parent && this.parent !== parent) {
121121- throw new Error(
122122- `DSL node already has a parent (${this.parent.constructor.name}); cannot reassign to ${parent.constructor.name}.`,
123123- );
131131+ const message = `${this.constructor.name} already had a parent (${this.parent.constructor.name}), new parent attempted: ${parent.constructor.name}`;
132132+ debug(message, 'dsl');
133133+ throw new Error(message);
134134+ }
135135+136136+ if (this.symbol) {
137137+ const message = `${this.constructor.name} has BOTH a symbol and a parent. This violates DSL invariants.`;
138138+ debug(message, 'dsl');
139139+ throw new Error(message);
124140 }
141141+125142 this.parent = parent;
126143 return this;
127144 }
···175192 }
176193177194 /** Returns the root symbol associated with this DSL subtree. */
178178- protected getRootSymbol(): Symbol | undefined {
179179- // eslint-disable-next-line @typescript-eslint/no-this-alias
180180- let n: TsDsl<any> | undefined = this;
181181- while (n) {
182182- if (n.symbol) return n.symbol;
183183- n = n.parent;
195195+ protected getRootSymbol(): Symbol {
196196+ if (!this.parent && !this.symbol) {
197197+ const message = `${this.constructor.name} has neither a parent nor a symbol — root symbol resolution failed.`;
198198+ debug(message, 'dsl');
199199+ throw new Error(message);
184200 }
185185- return undefined;
201201+ return this.parent ? this.parent.getRootSymbol() : this.symbol!.canonical;
186202 }
187203188204 /** Unwraps nested DSL nodes into raw TypeScript AST nodes. */
···190206 return (
191207 value instanceof TsDsl ? value.$render() : value
192208 ) as I extends TsDsl<infer N> ? N : I;
209209+ }
210210+211211+ /** Validate DSL invariants. */
212212+ protected _validate(): void {
213213+ if (!this.parent && !this.symbol) {
214214+ const message = `${this.constructor.name}: top-level DSL node has no symbol`;
215215+ debug(message, 'dsl');
216216+ throw new Error(message);
217217+ }
218218+219219+ if (this.parent && this.symbol) {
220220+ const message = `${this.constructor.name}: non-top-level node must not have a symbol`;
221221+ debug(message, 'dsl');
222222+ throw new Error(message);
223223+ }
224224+225225+ if (this.parent === undefined && this.symbol === undefined) {
226226+ const message = `${this.constructor.name}: non-root DSL node is missing a parent`;
227227+ debug(message, 'dsl');
228228+ throw new Error(message);
229229+ }
230230+231231+ if (this.symbol && this.symbol.canonical !== this.symbol) {
232232+ const message = `${this.constructor.name}: DSL node is holding a non-canonical (stub) symbol`;
233233+ debug(message, 'dsl');
234234+ throw new Error(message);
235235+ }
236236+237237+ this.traverse((node) => {
238238+ const dsl = node as TsDsl<any>;
239239+ if (dsl !== this && dsl.parent !== this) {
240240+ const message = `${dsl.constructor.name}: child node has incorrect or missing parent`;
241241+ debug(message, 'dsl');
242242+ throw new Error(message);
243243+ }
244244+ });
193245 }
194246}
195247
+19-37
packages/openapi-ts/src/ts-dsl/decl/class.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { Symbol, SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
76import { NewlineTsDsl } from '../layout/newline';
88-import { mixin } from '../mixins/apply';
97import { DecoratorMixin } from '../mixins/decorator';
108import { DocMixin } from '../mixins/doc';
1111-import {
1212- AbstractMixin,
1313- createModifierAccessor,
1414- DefaultMixin,
1515- ExportMixin,
1616-} from '../mixins/modifiers';
99+import { AbstractMixin, DefaultMixin, ExportMixin } from '../mixins/modifiers';
1710import { TypeParamsMixin } from '../mixins/type-params';
1811import { FieldTsDsl } from './field';
1912import { InitTsDsl } from './init';
2013import { MethodTsDsl } from './method';
21142222-export class ClassTsDsl extends TsDsl<ts.ClassDeclaration> {
1515+const Mixed = AbstractMixin(
1616+ DecoratorMixin(
1717+ DefaultMixin(
1818+ DocMixin(ExportMixin(TypeParamsMixin(TsDsl<ts.ClassDeclaration>))),
1919+ ),
2020+ ),
2121+);
2222+2323+export class ClassTsDsl extends Mixed {
2324 protected baseClass?: Symbol | string;
2425 protected body: Array<MaybeTsDsl<ts.ClassElement | NewlineTsDsl>> = [];
2525- protected modifiers = createModifierAccessor(this);
2626- protected name: string;
2626+ protected name: Symbol | string;
27272828 constructor(name: Symbol | string) {
2929 super();
···3131 this.name = name;
3232 return;
3333 }
3434- this.name = name.finalName;
3434+ this.name = name;
3535 this.symbol = name;
3636 this.symbol.setKind('class');
3737 this.symbol.setRootNode(this);
···4040 /** Adds one or more class members (fields, methods, etc.). */
4141 do(...items: ReadonlyArray<MaybeTsDsl<ts.ClassElement | ts.Node>>): this {
4242 for (const item of items) {
4343- if (item && typeof item === 'object' && 'setParent' in item) {
4343+ if (typeof item === 'object' && 'setParent' in item) {
4444 item.setParent(this);
4545 }
4646 // @ts-expect-error --- IGNORE ---
···5454 if (!base) return this;
5555 this.baseClass = base;
5656 if (typeof base !== 'string') {
5757- const symbol = this.getRootSymbol();
5858- if (symbol) symbol.addDependency(base);
5757+ this.getRootSymbol().addDependency(base);
5958 }
6059 return this;
6160 }
···8786 return this;
8887 }
89889090- /** Walk this node and its children with a visitor. */
9189 traverse(visitor: (node: SyntaxNode) => void): void {
9290 console.log(visitor);
9391 }
94929595- /** Builds the `ClassDeclaration` node. */
9696- $render(): ts.ClassDeclaration {
9393+ protected override _render() {
9794 const body = this.$node(this.body) as ReadonlyArray<ts.ClassElement>;
9595+ const name =
9696+ typeof this.name === 'string' ? this.name : this.name.finalName;
9897 return ts.factory.createClassDeclaration(
9999- [...this.$decorators(), ...this.modifiers.list()],
100100- this.name,
9898+ [...this.$decorators(), ...this.modifiers],
9999+ name,
101100 this.$generics(),
102101 this._renderHeritage(),
103102 body,
···118117 ];
119118 }
120119}
121121-122122-export interface ClassTsDsl
123123- extends AbstractMixin,
124124- DecoratorMixin,
125125- DefaultMixin,
126126- DocMixin,
127127- ExportMixin,
128128- TypeParamsMixin {}
129129-mixin(
130130- ClassTsDsl,
131131- AbstractMixin,
132132- DecoratorMixin,
133133- DefaultMixin,
134134- DocMixin,
135135- ExportMixin,
136136- TypeParamsMixin,
137137-);
+5-10
packages/openapi-ts/src/ts-dsl/decl/decorator.ts
···11-/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */
21import type { Symbol, SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { ArgsMixin } from '../mixins/args';
971010-export class DecoratorTsDsl extends TsDsl<ts.Decorator> {
88+const Mixed = ArgsMixin(TsDsl<ts.Decorator>);
99+1010+export class DecoratorTsDsl extends Mixed {
1111 protected name: Symbol | string | MaybeTsDsl<ts.Expression>;
12121313 constructor(
···1717 super();
1818 this.name = name;
1919 if (typeof name !== 'string' && 'id' in name) {
2020- const symbol = this.getRootSymbol();
2121- if (symbol) symbol.addDependency(name);
2020+ this.getRootSymbol().addDependency(name);
2221 }
2322 this.args(...args);
2423 }
25242626- /** Walk this node and its children with a visitor. */
2725 traverse(visitor: (node: SyntaxNode) => void): void {
2826 console.log(visitor);
2927 }
30283131- $render(): ts.Decorator {
2929+ protected override _render() {
3230 const target =
3331 typeof this.name !== 'string' && 'id' in this.name
3432 ? this.$maybeId(this.name.finalName)
···4240 );
4341 }
4442}
4545-4646-export interface DecoratorTsDsl extends ArgsMixin {}
4747-mixin(DecoratorTsDsl, ArgsMixin);
+6-16
packages/openapi-ts/src/ts-dsl/decl/enum.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { Symbol, SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { DocMixin } from '../mixins/doc';
99-import {
1010- ConstMixin,
1111- createModifierAccessor,
1212- ExportMixin,
1313-} from '../mixins/modifiers';
77+import { ConstMixin, ExportMixin } from '../mixins/modifiers';
148import { EnumMemberTsDsl } from './member';
1591610type Value = string | number | MaybeTsDsl<ts.Expression>;
1711type ValueFn = Value | ((m: EnumMemberTsDsl) => void);
18121919-export class EnumTsDsl extends TsDsl<ts.EnumDeclaration> {
1313+const Mixed = ConstMixin(DocMixin(ExportMixin(TsDsl<ts.EnumDeclaration>)));
1414+1515+export class EnumTsDsl extends Mixed {
2016 private _members: Array<EnumMemberTsDsl> = [];
2117 private _name: string | ts.Identifier;
2222- protected modifiers = createModifierAccessor(this);
23182419 constructor(name: Symbol | string, fn?: (e: EnumTsDsl) => void) {
2520 super();
···4742 return this;
4843 }
49445050- /** Walk this node and its children with a visitor. */
5145 traverse(visitor: (node: SyntaxNode) => void): void {
5246 console.log(visitor);
5347 }
54485555- /** Renders the enum declaration. */
5656- $render(): ts.EnumDeclaration {
4949+ protected override _render() {
5750 return ts.factory.createEnumDeclaration(
5858- this.modifiers.list(),
5151+ this.modifiers,
5952 this._name,
6053 this.$node(this._members),
6154 );
6255 }
6356}
6464-6565-export interface EnumTsDsl extends ConstMixin, DocMixin, ExportMixin {}
6666-mixin(EnumTsDsl, ConstMixin, DocMixin, ExportMixin);
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { AsMixin } from '../mixins/as';
97import { LayoutMixin } from '../mixins/layout';
108import { LiteralTsDsl } from './literal';
1191212-export class ArrayTsDsl extends TsDsl<ts.ArrayLiteralExpression> {
1010+const Mixed = AsMixin(LayoutMixin(TsDsl<ts.ArrayLiteralExpression>));
1111+1212+export class ArrayTsDsl extends Mixed {
1313 protected _elements: Array<
1414 | { expr: MaybeTsDsl<ts.Expression>; kind: 'element' }
1515 | { expr: MaybeTsDsl<ts.Expression>; kind: 'spread' }
···5050 return this;
5151 }
52525353- /** Walk this node and its children with a visitor. */
5453 traverse(visitor: (node: SyntaxNode) => void): void {
5554 console.log(visitor);
5655 }
57565858- $render(): ts.ArrayLiteralExpression {
5757+ protected override _render() {
5958 const elements = this._elements.map((item) => {
6059 const node = this.$node(item.expr);
6160 return item.kind === 'spread'
···6968 );
7069 }
7170}
7272-7373-export interface ArrayTsDsl extends AsMixin, LayoutMixin {}
7474-mixin(ArrayTsDsl, AsMixin, LayoutMixin);
+4-7
packages/openapi-ts/src/ts-dsl/expr/as.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl, TypeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { AsMixin, registerLazyAccessAsFactory } from '../mixins/as';
97import { ExprMixin } from '../mixins/expr';
1081111-export class AsTsDsl extends TsDsl<ts.AsExpression> {
99+const Mixed = AsMixin(ExprMixin(TsDsl<ts.AsExpression>));
1010+1111+export class AsTsDsl extends Mixed {
1212 protected expr: string | MaybeTsDsl<ts.Expression>;
1313 protected type: string | TypeTsDsl;
1414···2626 console.log(visitor);
2727 }
28282929- $render() {
2929+ protected override _render() {
3030 return ts.factory.createAsExpression(
3131 this.$node(this.expr),
3232 this.$type(this.type),
3333 );
3434 }
3535}
3636-3737-export interface AsTsDsl extends AsMixin, ExprMixin {}
3838-mixin(AsTsDsl, AsMixin, ExprMixin);
39364037registerLazyAccessAsFactory((...args) => new AsTsDsl(...args));
+12-14
packages/openapi-ts/src/ts-dsl/expr/attr.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
43···6576import type { MaybeTsDsl } from '../base';
87import { TsDsl } from '../base';
99-import { mixin } from '../mixins/apply';
108import { AsMixin } from '../mixins/as';
119import { ExprMixin, registerLazyAccessAttrFactory } from '../mixins/expr';
1210import { OperatorMixin } from '../mixins/operator';
···1412import { TokenTsDsl } from '../token';
1513import { LiteralTsDsl } from './literal';
16141717-export class AttrTsDsl extends TsDsl<
1818- ts.PropertyAccessExpression | ts.ElementAccessExpression
1919-> {
1515+const Mixed = AsMixin(
1616+ ExprMixin(
1717+ OperatorMixin(
1818+ OptionalMixin(
1919+ TsDsl<ts.PropertyAccessExpression | ts.ElementAccessExpression>,
2020+ ),
2121+ ),
2222+ ),
2323+);
2424+2525+export class AttrTsDsl extends Mixed {
2026 protected left: string | MaybeTsDsl<ts.Expression>;
2127 protected right: string | ts.MemberName | number;
2228···2935 this.right = right;
3036 }
31373232- /** Walk this node and its children with a visitor. */
3338 traverse(visitor: (node: SyntaxNode) => void): void {
3439 console.log(visitor);
3540 }
36413737- $render(): ts.PropertyAccessExpression | ts.ElementAccessExpression {
4242+ protected override _render() {
3843 const leftNode = this.$node(this.left);
3944 validTypescriptIdentifierRegExp.lastIndex = 0;
4045 if (
···6772 );
6873 }
6974}
7070-7171-export interface AttrTsDsl
7272- extends AsMixin,
7373- ExprMixin,
7474- OperatorMixin,
7575- OptionalMixin {}
7676-mixin(AttrTsDsl, AsMixin, ExprMixin, OperatorMixin, OptionalMixin);
77757876registerLazyAccessAttrFactory((...args) => new AttrTsDsl(...args));
+4-8
packages/openapi-ts/src/ts-dsl/expr/await.ts
···11-/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { ExprMixin, registerLazyAccessAwaitFactory } from '../mixins/expr';
971010-export class AwaitTsDsl extends TsDsl<ts.AwaitExpression> {
88+const Mixed = ExprMixin(TsDsl<ts.AwaitExpression>);
99+1010+export class AwaitTsDsl extends Mixed {
1111 protected _awaitExpr: string | MaybeTsDsl<ts.Expression>;
12121313 constructor(expr: string | MaybeTsDsl<ts.Expression>) {
···1515 this._awaitExpr = expr;
1616 }
17171818- /** Walk this node and its children with a visitor. */
1918 traverse(visitor: (node: SyntaxNode) => void): void {
2019 console.log(visitor);
2120 }
22212323- $render(): ts.AwaitExpression {
2222+ protected override _render() {
2423 return ts.factory.createAwaitExpression(this.$node(this._awaitExpr));
2524 }
2625}
2727-2828-export interface AwaitTsDsl extends ExprMixin {}
2929-mixin(AwaitTsDsl, ExprMixin);
30263127registerLazyAccessAwaitFactory((...args) => new AwaitTsDsl(...args));
+4-8
packages/openapi-ts/src/ts-dsl/expr/binary.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { AsMixin } from '../mixins/as';
97import { ExprMixin } from '../mixins/expr';
108···2826 | '??'
2927 | '||';
30283131-export class BinaryTsDsl extends TsDsl<ts.BinaryExpression> {
2929+const Mixed = AsMixin(ExprMixin(TsDsl<ts.BinaryExpression>));
3030+3131+export class BinaryTsDsl extends Mixed {
3232 protected _base: Expr;
3333 protected _expr?: Expr;
3434 protected _op?: Op;
···120120 return this.opAndExpr('*', expr);
121121 }
122122123123- /** Walk this node and its children with a visitor. */
124123 traverse(visitor: (node: SyntaxNode) => void): void {
125124 console.log(visitor);
126125 }
127126128128- $render(): ts.BinaryExpression {
127127+ protected override _render() {
129128 if (!this._op) {
130129 throw new Error('BinaryTsDsl: missing operator');
131130 }
···172171 return token;
173172 }
174173}
175175-176176-export interface BinaryTsDsl extends AsMixin, ExprMixin {}
177177-mixin(BinaryTsDsl, AsMixin, ExprMixin);
+6-12
packages/openapi-ts/src/ts-dsl/expr/call.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { ArgsMixin } from '../mixins/args';
97import { AsMixin } from '../mixins/as';
108import { ExprMixin, registerLazyAccessCallFactory } from '../mixins/expr';
119import { TypeArgsMixin } from '../mixins/type-args';
12101313-export class CallTsDsl extends TsDsl<ts.CallExpression> {
1111+const Mixed = ArgsMixin(
1212+ AsMixin(ExprMixin(TypeArgsMixin(TsDsl<ts.CallExpression>))),
1313+);
1414+1515+export class CallTsDsl extends Mixed {
1416 protected _callee: string | MaybeTsDsl<ts.Expression>;
15171618 constructor(
···2426 );
2527 }
26282727- /** Walk this node and its children with a visitor. */
2829 traverse(visitor: (node: SyntaxNode) => void): void {
2930 console.log(visitor);
3031 }
31323232- $render(): ts.CallExpression {
3333+ protected override _render() {
3334 return ts.factory.createCallExpression(
3435 this.$node(this._callee),
3536 this.$generics(),
···3738 );
3839 }
3940}
4040-4141-export interface CallTsDsl
4242- extends ArgsMixin,
4343- AsMixin,
4444- ExprMixin,
4545- TypeArgsMixin {}
4646-mixin(CallTsDsl, ArgsMixin, AsMixin, ExprMixin, TypeArgsMixin);
47414842registerLazyAccessCallFactory((expr, args) => new CallTsDsl(expr, ...args));
+6-12
packages/openapi-ts/src/ts-dsl/expr/expr.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import type ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { AsMixin } from '../mixins/as';
97import { ExprMixin } from '../mixins/expr';
108import { OperatorMixin } from '../mixins/operator';
119import { TypeExprMixin } from '../mixins/type-expr';
12101313-export class ExprTsDsl extends TsDsl<ts.Expression> {
1111+const Mixed = AsMixin(
1212+ ExprMixin(OperatorMixin(TypeExprMixin(TsDsl<ts.Expression>))),
1313+);
1414+1515+export class ExprTsDsl extends Mixed {
1416 protected _exprInput: string | MaybeTsDsl<ts.Expression>;
15171618 constructor(id: string | MaybeTsDsl<ts.Expression>) {
···1820 this._exprInput = id;
1921 }
20222121- /** Walk this node and its children with a visitor. */
2223 traverse(visitor: (node: SyntaxNode) => void): void {
2324 console.log(visitor);
2425 }
25262626- $render(): ts.Expression {
2727+ protected override _render() {
2728 return this.$node(this._exprInput);
2829 }
2930}
3030-3131-export interface ExprTsDsl
3232- extends AsMixin,
3333- ExprMixin,
3434- OperatorMixin,
3535- TypeExprMixin {}
3636-mixin(ExprTsDsl, AsMixin, ExprMixin, OperatorMixin, TypeExprMixin);
+4-3
packages/openapi-ts/src/ts-dsl/expr/id.ts
···3344import { TsDsl } from '../base';
5566-export class IdTsDsl extends TsDsl<ts.Identifier> {
66+const Mixed = TsDsl<ts.Identifier>;
77+88+export class IdTsDsl extends Mixed {
79 protected name: string;
810911 constructor(name: string) {
···1113 this.name = name;
1214 }
13151414- /** Walk this node and its children with a visitor. */
1516 traverse(visitor: (node: SyntaxNode) => void): void {
1617 console.log(visitor);
1718 }
18191919- $render(): ts.Identifier {
2020+ protected override _render() {
2021 return ts.factory.createIdentifier(this.name);
2122 }
2223}
+4-8
packages/openapi-ts/src/ts-dsl/expr/literal.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/no-empty-object-type */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import { TsDsl } from '../base';
65import { PrefixTsDsl } from '../expr/prefix';
77-import { mixin } from '../mixins/apply';
86import { AsMixin } from '../mixins/as';
971010-export class LiteralTsDsl extends TsDsl<ts.LiteralTypeNode['literal']> {
88+const Mixed = AsMixin(TsDsl<ts.LiteralTypeNode['literal']>);
99+1010+export class LiteralTsDsl extends Mixed {
1111 protected value: string | number | boolean | null;
12121313 constructor(value: string | number | boolean | null) {
···1515 this.value = value;
1616 }
17171818- /** Walk this node and its children with a visitor. */
1918 traverse(visitor: (node: SyntaxNode) => void): void {
2019 console.log(visitor);
2120 }
22212323- $render(): ts.LiteralTypeNode['literal'] {
2222+ protected override _render() {
2423 if (typeof this.value === 'boolean') {
2524 return this.value ? ts.factory.createTrue() : ts.factory.createFalse();
2625 }
···3736 throw new Error(`Unsupported literal: ${String(this.value)}`);
3837 }
3938}
4040-4141-export interface LiteralTsDsl extends AsMixin {}
4242-mixin(LiteralTsDsl, AsMixin);
+4-9
packages/openapi-ts/src/ts-dsl/expr/new.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { ArgsMixin } from '../mixins/args';
97import { ExprMixin } from '../mixins/expr';
108import { TypeArgsMixin } from '../mixins/type-args';
1191212-export class NewTsDsl extends TsDsl<ts.NewExpression> {
1010+const Mixed = ArgsMixin(ExprMixin(TypeArgsMixin(TsDsl<ts.NewExpression>)));
1111+1212+export class NewTsDsl extends Mixed {
1313 protected classExpr: string | MaybeTsDsl<ts.Expression>;
14141515 constructor(
···2121 this.args(...args);
2222 }
23232424- /** Walk this node and its children with a visitor. */
2524 traverse(visitor: (node: SyntaxNode) => void): void {
2625 console.log(visitor);
2726 }
28272929- /** Builds the `NewExpression` node. */
3030- $render(): ts.NewExpression {
2828+ protected override _render() {
3129 return ts.factory.createNewExpression(
3230 this.$node(this.classExpr),
3331 this.$generics(),
···3533 );
3634 }
3735}
3838-3939-export interface NewTsDsl extends ArgsMixin, ExprMixin, TypeArgsMixin {}
4040-mixin(NewTsDsl, ArgsMixin, ExprMixin, TypeArgsMixin);
+6-13
packages/openapi-ts/src/ts-dsl/expr/object.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { AsMixin } from '../mixins/as';
97import { ExprMixin } from '../mixins/expr';
108import { HintMixin } from '../mixins/hint';
···1614type ExprFn = Expr | ((p: ObjectPropTsDsl) => void);
1715type StmtFn = Stmt | ((p: ObjectPropTsDsl) => void);
18161919-export class ObjectTsDsl extends TsDsl<ts.ObjectLiteralExpression> {
1717+const Mixed = AsMixin(
1818+ ExprMixin(HintMixin(LayoutMixin(TsDsl<ts.ObjectLiteralExpression>))),
1919+);
2020+2121+export class ObjectTsDsl extends Mixed {
2022 protected _props: Array<ObjectPropTsDsl> = [];
21232224 constructor(...props: Array<ObjectPropTsDsl>) {
···7274 return this;
7375 }
74767575- /** Walk this node and its children with a visitor. */
7677 traverse(visitor: (node: SyntaxNode) => void): void {
7778 console.log(visitor);
7879 }
79808080- /** Builds and returns the object literal expression. */
8181- $render(): ts.ObjectLiteralExpression {
8181+ protected override _render() {
8282 return ts.factory.createObjectLiteralExpression(
8383 this.$node(this._props),
8484 this.$multiline(this._props.length),
8585 );
8686 }
8787}
8888-8989-export interface ObjectTsDsl
9090- extends AsMixin,
9191- ExprMixin,
9292- HintMixin,
9393- LayoutMixin {}
9494-mixin(ObjectTsDsl, AsMixin, ExprMixin, HintMixin, LayoutMixin);
+4-4
packages/openapi-ts/src/ts-dsl/expr/prefix.ts
···44import type { MaybeTsDsl } from '../base';
55import { TsDsl } from '../base';
6677-export class PrefixTsDsl extends TsDsl<ts.PrefixUnaryExpression> {
77+const Mixed = TsDsl<ts.PrefixUnaryExpression>;
88+99+export class PrefixTsDsl extends Mixed {
810 protected _expr?: string | MaybeTsDsl<ts.Expression>;
911 protected _op?: ts.PrefixUnaryOperator;
1012···4143 return this;
4244 }
43454444- /** Walk this node and its children with a visitor. */
4546 traverse(visitor: (node: SyntaxNode) => void): void {
4647 console.log(visitor);
4748 }
48494949- /** Renders the prefix unary expression node. */
5050- $render(): ts.PrefixUnaryExpression {
5050+ protected override _render() {
5151 if (!this._expr) {
5252 throw new Error('Missing expression for prefix unary expression');
5353 }
+4-8
packages/openapi-ts/src/ts-dsl/expr/prop.ts
···11-/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
43···65import { TsDsl } from '../base';
76import { GetterTsDsl } from '../decl/getter';
87import { SetterTsDsl } from '../decl/setter';
99-import { mixin } from '../mixins/apply';
108import { DocMixin } from '../mixins/doc';
119import { safePropName } from '../utils/prop';
1210import { IdTsDsl } from './id';
···2220 | { kind: 'setter'; name: string }
2321 | { kind: 'spread'; name?: undefined };
24222525-export class ObjectPropTsDsl extends TsDsl<ts.ObjectLiteralElementLike> {
2323+const Mixed = DocMixin(TsDsl<ts.ObjectLiteralElementLike>);
2424+2525+export class ObjectPropTsDsl extends Mixed {
2626 protected _value?: Expr | Stmt;
2727 protected meta: Meta;
2828···3636 return this.missingRequiredCalls().length === 0;
3737 }
38383939- /** Walk this node and its children with a visitor. */
4039 traverse(visitor: (node: SyntaxNode) => void): void {
4140 console.log(visitor);
4241 }
···5049 return this;
5150 }
52515353- $render(): ts.ObjectLiteralElementLike {
5252+ protected override _render() {
5453 this.$validate();
5554 const node = this.$node(this._value);
5655 if (this.meta.kind === 'spread') {
···104103 return missing;
105104 }
106105}
107107-108108-export interface ObjectPropTsDsl extends DocMixin {}
109109-mixin(ObjectPropTsDsl, DocMixin);
+4-4
packages/openapi-ts/src/ts-dsl/expr/regexp.ts
···1111 [K in Avail]: `${K}${RegexFlags<Exclude<Avail, K>>}`;
1212 }[Avail];
13131414-export class RegExpTsDsl extends TsDsl<ts.RegularExpressionLiteral> {
1414+const Mixed = TsDsl<ts.RegularExpressionLiteral>;
1515+1616+export class RegExpTsDsl extends Mixed {
1517 protected pattern: string;
1618 protected flags?: RegexFlags;
1719···2123 this.flags = flags;
2224 }
23252424- /** Walk this node and its children with a visitor. */
2526 traverse(visitor: (node: SyntaxNode) => void): void {
2627 console.log(visitor);
2728 }
28292929- /** Emits a RegularExpressionLiteral node. */
3030- $render(): ts.RegularExpressionLiteral {
3030+ protected override _render() {
3131 const patternContent =
3232 this.pattern.startsWith('/') && this.pattern.endsWith('/')
3333 ? this.pattern.slice(1, -1)
+4-5
packages/openapi-ts/src/ts-dsl/expr/template.ts
···44import type { MaybeTsDsl } from '../base';
55import { TsDsl } from '../base';
6677-export class TemplateTsDsl extends TsDsl<
88- ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral
99-> {
77+const Mixed = TsDsl<ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral>;
88+99+export class TemplateTsDsl extends Mixed {
1010 protected parts: Array<string | MaybeTsDsl<ts.Expression>> = [];
11111212 constructor(value?: string | MaybeTsDsl<ts.Expression>) {
···1919 return this;
2020 }
21212222- /** Walk this node and its children with a visitor. */
2322 traverse(visitor: (node: SyntaxNode) => void): void {
2423 console.log(visitor);
2524 }
26252727- $render(): ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral {
2626+ protected override _render() {
2827 const parts = this.$node(this.parts);
29283029 const normalized: Array<string | ts.Expression> = [];
+4-3
packages/openapi-ts/src/ts-dsl/expr/ternary.ts
···44import type { MaybeTsDsl } from '../base';
55import { TsDsl } from '../base';
6677-export class TernaryTsDsl extends TsDsl<ts.ConditionalExpression> {
77+const Mixed = TsDsl<ts.ConditionalExpression>;
88+99+export class TernaryTsDsl extends Mixed {
810 protected _condition?: string | MaybeTsDsl<ts.Expression>;
911 protected _then?: string | MaybeTsDsl<ts.Expression>;
1012 protected _else?: string | MaybeTsDsl<ts.Expression>;
···2931 return this;
3032 }
31333232- /** Walk this node and its children with a visitor. */
3334 traverse(visitor: (node: SyntaxNode) => void): void {
3435 console.log(visitor);
3536 }
36373737- $render(): ts.ConditionalExpression {
3838+ protected override _render() {
3839 if (!this._condition) throw new Error('Missing condition in ternary');
3940 if (!this._then) throw new Error('Missing then expression in ternary');
4041 if (!this._else) throw new Error('Missing else expression in ternary');
+4-8
packages/openapi-ts/src/ts-dsl/expr/typeof.ts
···11-/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */
21import type { SyntaxNode } from '@hey-api/codegen-core';
32import ts from 'typescript';
4354import type { MaybeTsDsl } from '../base';
65import { TsDsl } from '../base';
77-import { mixin } from '../mixins/apply';
86import { OperatorMixin } from '../mixins/operator';
97import { registerLazyAccessTypeOfExprFactory } from '../mixins/type-expr';
1081111-export class TypeOfExprTsDsl extends TsDsl<ts.TypeOfExpression> {
99+const Mixed = OperatorMixin(TsDsl<ts.TypeOfExpression>);
1010+1111+export class TypeOfExprTsDsl extends Mixed {
1212 protected _expr: string | MaybeTsDsl<ts.Expression>;
13131414 constructor(expr: string | MaybeTsDsl<ts.Expression>) {
···1616 this._expr = expr;
1717 }
18181919- /** Walk this node and its children with a visitor. */
2019 traverse(visitor: (node: SyntaxNode) => void): void {
2120 console.log(visitor);
2221 }
23222424- $render(): ts.TypeOfExpression {
2323+ protected override _render() {
2524 return ts.factory.createTypeOfExpression(this.$node(this._expr));
2625 }
2726}
2828-2929-export interface TypeOfExprTsDsl extends OperatorMixin {}
3030-mixin(TypeOfExprTsDsl, OperatorMixin);
31273228registerLazyAccessTypeOfExprFactory((...args) => new TypeOfExprTsDsl(...args));
+4-4
packages/openapi-ts/src/ts-dsl/layout/doc.ts
···5959 return node;
6060 }
61616262- /** Walk this node and its children with a visitor. */
6363- traverse(visitor: (node: SyntaxNode) => void): void {
6464- console.log(visitor);
6262+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
6363+ traverse(_visitor: (node: SyntaxNode) => void): void {
6464+ // noop
6565 }
66666767- $render(): ts.Node {
6767+ protected override _render(): ts.Node {
6868 // this class does not build a standalone node;
6969 // it modifies other nodes via `apply()`.
7070 // Return a dummy comment node for compliance.
+4-4
packages/openapi-ts/src/ts-dsl/layout/hint.ts
···4141 return node;
4242 }
43434444- /** Walk this node and its children with a visitor. */
4545- traverse(visitor: (node: SyntaxNode) => void): void {
4646- console.log(visitor);
4444+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4545+ traverse(_visitor: (node: SyntaxNode) => void): void {
4646+ // noop
4747 }
48484949- $render(): ts.Node {
4949+ protected override _render(): ts.Node {
5050 // this class does not build a standalone node;
5151 // it modifies other nodes via `apply()`.
5252 // Return a dummy comment node for compliance.
+4-4
packages/openapi-ts/src/ts-dsl/layout/newline.ts
···55import { IdTsDsl } from '../expr/id';
6677export class NewlineTsDsl extends TsDsl<ts.Identifier> {
88- /** Walk this node and its children with a visitor. */
99- traverse(visitor: (node: SyntaxNode) => void): void {
1010- console.log(visitor);
88+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
99+ traverse(_visitor: (node: SyntaxNode) => void): void {
1010+ // noop
1111 }
12121313- $render(): ts.Identifier {
1313+ protected override _render(): ts.Identifier {
1414 return this.$node(new IdTsDsl('\n'));
1515 }
1616}
+4-4
packages/openapi-ts/src/ts-dsl/layout/note.ts
···3939 return node;
4040 }
41414242- /** Walk this node and its children with a visitor. */
4343- traverse(visitor: (node: SyntaxNode) => void): void {
4444- console.log(visitor);
4242+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4343+ traverse(_visitor: (node: SyntaxNode) => void): void {
4444+ // noop
4545 }
46464747- $render(): ts.Node {
4747+ protected override _render(): ts.Node {
4848 // this class does not build a standalone node;
4949 // it modifies other nodes via `apply()`.
5050 // Return a dummy comment node for compliance.
-25
packages/openapi-ts/src/ts-dsl/mixins/apply.ts
···11-/* eslint-disable @typescript-eslint/no-unsafe-function-type */
22-export function mixin(target: Function, ...sources: ReadonlyArray<Function>) {
33- const targetProto = target.prototype;
44- for (const src of sources) {
55- let resolvedSource = src;
66- if (typeof src === 'function') {
77- try {
88- const candidate = src(target);
99- if (candidate?.prototype) {
1010- resolvedSource = candidate;
1111- }
1212- } catch {
1313- // noop
1414- }
1515- }
1616- const sourceProto = resolvedSource.prototype;
1717- if (!sourceProto) continue;
1818- for (const [key, descriptor] of Object.entries(
1919- Object.getOwnPropertyDescriptors(sourceProto),
2020- )) {
2121- if (key === 'constructor') continue;
2222- Object.defineProperty(targetProto, key, descriptor);
2323- }
2424- }
2525-}
+30-17
packages/openapi-ts/src/ts-dsl/mixins/args.ts
···11import type ts from 'typescript';
2233import type { MaybeTsDsl } from '../base';
44-import { TsDsl } from '../base';
44+import type { BaseCtor, MixinCtor } from './types';
55+66+export interface ArgsMethods {
77+ /** Renders the arguments into an array of `Expression`s. */
88+ $args(): ReadonlyArray<ts.Expression>;
99+ /** Adds a single expression argument. */
1010+ arg(arg: string | MaybeTsDsl<ts.Expression>): this;
1111+ /** Adds one or more expression arguments. */
1212+ args(...args: ReadonlyArray<string | MaybeTsDsl<ts.Expression>>): this;
1313+}
514615/**
716 * Adds `.arg()` and `.args()` for managing expression arguments in call-like nodes.
817 */
99-export abstract class ArgsMixin extends TsDsl {
1010- protected _args?: Array<string | MaybeTsDsl<ts.Expression>>;
1818+export function ArgsMixin<T extends ts.Node, TBase extends BaseCtor<T>>(
1919+ Base: TBase,
2020+) {
2121+ abstract class Args extends Base {
2222+ protected _args: Array<string | MaybeTsDsl<ts.Expression>> = [];
11231212- /** Adds a single expression argument. */
1313- arg(arg: string | MaybeTsDsl<ts.Expression>): this {
1414- (this._args ??= []).push(arg);
1515- return this;
1616- }
2424+ protected arg(arg: string | MaybeTsDsl<ts.Expression>): this {
2525+ this._args.push(arg);
2626+ return this;
2727+ }
2828+2929+ protected args(
3030+ ...args: ReadonlyArray<string | MaybeTsDsl<ts.Expression>>
3131+ ): this {
3232+ this._args.push(...args);
3333+ return this;
3434+ }
17351818- /** Adds one or more expression arguments. */
1919- args(...args: ReadonlyArray<string | MaybeTsDsl<ts.Expression>>): this {
2020- (this._args ??= []).push(...args);
2121- return this;
3636+ protected $args(): ReadonlyArray<ts.Expression> {
3737+ return this.$node(this._args).map((arg) => this.$maybeId(arg));
3838+ }
2239 }
23402424- /** Renders the arguments into an array of `Expression`s. */
2525- protected $args(): ReadonlyArray<ts.Expression> {
2626- if (!this._args) return [];
2727- return this.$node(this._args).map((arg) => this.$maybeId(arg));
2828- }
4141+ return Args as unknown as MixinCtor<TBase, ArgsMethods>;
2942}
+14-6
packages/openapi-ts/src/ts-dsl/mixins/as.ts
···2233import type { MaybeTsDsl, TypeTsDsl } from '../base';
44import type { AsTsDsl } from '../expr/as';
55+import type { BaseCtor, MixinCtor } from './types';
5667type AsFactory = (
78 expr: string | MaybeTsDsl<ts.Expression>,
···1314 asFactory = factory;
1415}
15161616-export class AsMixin {
1717+export interface AsMethods {
1718 /** Creates an `as` type assertion expression (e.g. `value as Type`). */
1818- as(
1919- this: string | MaybeTsDsl<ts.Expression>,
2020- type: string | TypeTsDsl,
2121- ): AsTsDsl {
2222- return asFactory!(this, type);
1919+ as(type: string | TypeTsDsl): AsTsDsl;
2020+}
2121+2222+export function AsMixin<T extends ts.Expression, TBase extends BaseCtor<T>>(
2323+ Base: TBase,
2424+) {
2525+ abstract class As extends Base {
2626+ protected as(type: string | TypeTsDsl): AsTsDsl {
2727+ return asFactory!(this, type);
2828+ }
2329 }
3030+3131+ return As as unknown as MixinCtor<TBase, AsMethods>;
2432}