diff --git a/packages/openapi-ts/src/ts-dsl/decl/func.ts b/packages/openapi-ts/src/ts-dsl/decl/func.ts index 06f18e4bd..903a58462 100644 --- a/packages/openapi-ts/src/ts-dsl/decl/func.ts +++ b/packages/openapi-ts/src/ts-dsl/decl/func.ts @@ -10,6 +10,7 @@ import { DocMixin } from '../mixins/doc'; import { AbstractMixin, AsyncMixin, + ExportMixin, PrivateMixin, ProtectedMixin, PublicMixin, @@ -29,11 +30,13 @@ const Mixed = AbstractMixin( DecoratorMixin( DoMixin( DocMixin( - ParamMixin( - PrivateMixin( - ProtectedMixin( - PublicMixin( - StaticMixin(TypeParamsMixin(TypeReturnsMixin(TsDsl))), + ExportMixin( + ParamMixin( + PrivateMixin( + ProtectedMixin( + PublicMixin( + StaticMixin(TypeParamsMixin(TypeReturnsMixin(TsDsl))), + ), ), ), ), diff --git a/packages/openapi-ts/src/ts-dsl/decl/method.ts b/packages/openapi-ts/src/ts-dsl/decl/method.ts index 0aa44c9b6..d52d20e6a 100644 --- a/packages/openapi-ts/src/ts-dsl/decl/method.ts +++ b/packages/openapi-ts/src/ts-dsl/decl/method.ts @@ -19,8 +19,11 @@ import { TypeParamsMixin } from '../mixins/type-params'; import { TypeReturnsMixin } from '../mixins/type-returns'; import { BlockTsDsl } from '../stmt/block'; import { TokenTsDsl } from '../token'; +import { f } from '../utils/factories'; import { safeAccessorName } from '../utils/name'; +export type MethodCtor = (name: NodeName, fn?: (m: MethodTsDsl) => void) => MethodTsDsl; + const Mixed = AbstractMixin( AsyncMixin( DecoratorMixin( @@ -78,3 +81,5 @@ export class MethodTsDsl extends Mixed { return this.$docs(node); } } + +f.method.set((...args) => new MethodTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/decl/param.ts b/packages/openapi-ts/src/ts-dsl/decl/param.ts index e451ddea8..fc3cc9390 100644 --- a/packages/openapi-ts/src/ts-dsl/decl/param.ts +++ b/packages/openapi-ts/src/ts-dsl/decl/param.ts @@ -45,8 +45,8 @@ export class ParamTsDsl extends Mixed { } /** Sets the parameter type. */ - type(type: string | TypeTsDsl): this { - this._type = type instanceof TypeTsDsl ? type : new TypeExprTsDsl(type); + type(node: NodeName | TypeTsDsl): this { + this._type = node instanceof TypeTsDsl ? node : new TypeExprTsDsl(node); return this; } diff --git a/packages/openapi-ts/src/ts-dsl/expr/array.ts b/packages/openapi-ts/src/ts-dsl/expr/array.ts index 1d1c82007..3e13c1da6 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/array.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/array.ts @@ -6,19 +6,19 @@ import { TsDsl } from '../base'; import { AsMixin } from '../mixins/as'; import { ExprMixin } from '../mixins/expr'; import { LayoutMixin } from '../mixins/layout'; +import { SpreadMixin } from '../mixins/spread'; import { LiteralTsDsl } from './literal'; -const Mixed = AsMixin(ExprMixin(LayoutMixin(TsDsl))); +export type ArrayExpr = string | number | boolean | MaybeTsDsl; + +const Mixed = AsMixin(ExprMixin(LayoutMixin(SpreadMixin(TsDsl)))); export class ArrayTsDsl extends Mixed { readonly '~dsl' = 'ArrayTsDsl'; - protected _elements: Array< - | { expr: MaybeTsDsl; kind: 'element' } - | { expr: MaybeTsDsl; kind: 'spread' } - > = []; + protected _elements: Array> = []; - constructor(...exprs: Array>) { + constructor(...exprs: Array) { super(); this.elements(...exprs); } @@ -26,40 +26,29 @@ export class ArrayTsDsl extends Mixed { override analyze(ctx: AnalysisContext): void { super.analyze(ctx); for (const item of this._elements) { - ctx.analyze(item.expr); + ctx.analyze(item); } } /** Adds a single array element. */ - element(expr: string | number | boolean | MaybeTsDsl): this { + element(expr: ArrayExpr): this { const node = typeof expr === 'string' || typeof expr === 'number' || typeof expr === 'boolean' ? new LiteralTsDsl(expr) : expr; - this._elements.push({ expr: node, kind: 'element' }); + this._elements.push(node); return this; } /** Adds multiple array elements. */ - elements(...exprs: ReadonlyArray>): this { + elements(...exprs: ReadonlyArray): this { for (const expr of exprs) this.element(expr); return this; } - /** Adds a spread element (`...expr`). */ - spread(expr: MaybeTsDsl): this { - this._elements.push({ expr, kind: 'spread' }); - return this; - } - override toAst() { - const elements = this._elements.map((item) => { - const node = this.$node(item.expr); - return item.kind === 'spread' ? ts.factory.createSpreadElement(node) : node; - }); - return ts.factory.createArrayLiteralExpression( - elements, + this._elements.map((item) => this.$node(item)), this.$multiline(this._elements.length), ); } diff --git a/packages/openapi-ts/src/ts-dsl/expr/attr.ts b/packages/openapi-ts/src/ts-dsl/expr/attr.ts index a00077bd7..3d6c19d4e 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/attr.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/attr.ts @@ -8,6 +8,7 @@ import { AsMixin } from '../mixins/as'; import { ExprMixin } from '../mixins/expr'; import { OperatorMixin } from '../mixins/operator'; import { OptionalMixin } from '../mixins/optional'; +import { SpreadMixin } from '../mixins/spread'; import { TokenTsDsl } from '../token'; import { f } from '../utils/factories'; import { regexp } from '../utils/regexp'; @@ -18,7 +19,9 @@ export type AttrCtor = (left: AttrLeft, right: NodeName) => AttrTsDsl; const Mixed = AsMixin( ExprMixin( - OperatorMixin(OptionalMixin(TsDsl)), + OperatorMixin( + OptionalMixin(SpreadMixin(TsDsl)), + ), ), ); diff --git a/packages/openapi-ts/src/ts-dsl/expr/call.ts b/packages/openapi-ts/src/ts-dsl/expr/call.ts index 9eff383e2..c1001024e 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/call.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/call.ts @@ -7,6 +7,7 @@ import { TsDsl } from '../base'; import { ArgsMixin } from '../mixins/args'; import { AsMixin } from '../mixins/as'; import { ExprMixin } from '../mixins/expr'; +import { SpreadMixin } from '../mixins/spread'; import { TypeArgsMixin } from '../mixins/type-args'; import { f } from '../utils/factories'; @@ -14,7 +15,7 @@ export type CallArgs = ReadonlyArray; export type CallCallee = NodeName | MaybeTsDsl; export type CallCtor = (callee: CallCallee, ...args: CallArgs) => CallTsDsl; -const Mixed = ArgsMixin(AsMixin(ExprMixin(TypeArgsMixin(TsDsl)))); +const Mixed = ArgsMixin(AsMixin(ExprMixin(SpreadMixin(TypeArgsMixin(TsDsl))))); export class CallTsDsl extends Mixed { readonly '~dsl' = 'CallTsDsl'; diff --git a/packages/openapi-ts/src/ts-dsl/expr/expr.ts b/packages/openapi-ts/src/ts-dsl/expr/expr.ts index 01374884e..0a9a58dfe 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/expr.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/expr.ts @@ -7,11 +7,12 @@ import { TsDsl } from '../base'; import { AsMixin } from '../mixins/as'; import { ExprMixin } from '../mixins/expr'; import { OperatorMixin } from '../mixins/operator'; +import { SpreadMixin } from '../mixins/spread'; import { TypeExprMixin } from '../mixins/type-expr'; type Id = NodeName | MaybeTsDsl; -const Mixed = AsMixin(ExprMixin(OperatorMixin(TypeExprMixin(TsDsl)))); +const Mixed = AsMixin(ExprMixin(OperatorMixin(SpreadMixin(TypeExprMixin(TsDsl))))); export class ExprTsDsl extends Mixed { readonly '~dsl' = 'ExprTsDsl'; diff --git a/packages/openapi-ts/src/ts-dsl/expr/new.ts b/packages/openapi-ts/src/ts-dsl/expr/new.ts index 909dad949..cc857da7d 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/new.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/new.ts @@ -7,6 +7,7 @@ import { TsDsl } from '../base'; import { ArgsMixin } from '../mixins/args'; import { AsMixin } from '../mixins/as'; import { ExprMixin } from '../mixins/expr'; +import { SpreadMixin } from '../mixins/spread'; import { TypeArgsMixin } from '../mixins/type-args'; import { f } from '../utils/factories'; @@ -14,7 +15,7 @@ export type NewArgs = ReadonlyArray; export type NewExpr = NodeName | MaybeTsDsl; export type NewCtor = (expr: NewExpr, ...args: NewArgs) => NewTsDsl; -const Mixed = ArgsMixin(AsMixin(ExprMixin(TypeArgsMixin(TsDsl)))); +const Mixed = ArgsMixin(AsMixin(ExprMixin(SpreadMixin(TypeArgsMixin(TsDsl))))); export class NewTsDsl extends Mixed { readonly '~dsl' = 'NewTsDsl'; diff --git a/packages/openapi-ts/src/ts-dsl/expr/object.ts b/packages/openapi-ts/src/ts-dsl/expr/object.ts index caa7d11b1..34bb28fd7 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/object.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/object.ts @@ -3,16 +3,16 @@ import ts from 'typescript'; import type { MaybeTsDsl } from '../base'; import { TsDsl } from '../base'; +import type { MethodTsDsl } from '../decl/method'; import { AsMixin } from '../mixins/as'; import { ExprMixin } from '../mixins/expr'; import { HintMixin } from '../mixins/hint'; import { LayoutMixin } from '../mixins/layout'; +import { f } from '../utils/factories'; import { ObjectPropTsDsl } from './prop'; type Expr = NodeName | MaybeTsDsl; type Stmt = NodeName | MaybeTsDsl; -type ExprFn = Expr | ((p: ObjectPropTsDsl) => void); -type StmtFn = Stmt | ((p: ObjectPropTsDsl) => void); const Mixed = AsMixin(ExprMixin(HintMixin(LayoutMixin(TsDsl)))); @@ -22,9 +22,13 @@ export class ObjectTsDsl extends Mixed { protected _props = new Map(); protected _spreadCounter = 0; - constructor(...props: Array) { + constructor(...props: Array | [(o: ObjectTsDsl) => void]) { super(); - this.props(...props); + if (props.length === 1 && typeof props[0] === 'function') { + props[0](this); + } else { + this.props(...(props as Array)); + } } override analyze(ctx: AnalysisContext): void { @@ -43,7 +47,7 @@ export class ObjectTsDsl extends Mixed { } /** Adds a computed property (e.g., `{ [expr]: value }`), or removes if null. */ - computed(name: string, expr: ExprFn | null): this { + computed(name: string, expr: Expr | null): this { if (expr === null) { this._props.delete(`computed:${name}`); } else { @@ -56,7 +60,7 @@ export class ObjectTsDsl extends Mixed { } /** Adds a getter property (e.g., `{ get foo() { ... } }`), or removes if null. */ - getter(name: string, stmt: StmtFn | null): this { + getter(name: string, stmt: Stmt | null): this { if (stmt === null) { this._props.delete(`getter:${name}`); } else { @@ -75,8 +79,21 @@ export class ObjectTsDsl extends Mixed { return this._props.size === 0; } + /** Adds a method property (e.g., `{ foo() { ... } }`), or removes if null. */ + method(name: string, fn: ((m: MethodTsDsl) => void) | null): this { + if (fn === null) { + this._props.delete(`method:${name}`); + } else { + this._props.set( + `method:${name}`, + new ObjectPropTsDsl({ kind: 'method', name }).value(f.method(name, fn)), + ); + } + return this; + } + /** Adds a property assignment, or removes if null. */ - prop(name: string, expr: ExprFn | null): this { + prop(name: string, expr: Expr | null): this { if (expr === null) { this._props.delete(`prop:${name}`); } else { @@ -94,7 +111,7 @@ export class ObjectTsDsl extends Mixed { } /** Adds a setter property (e.g., `{ set foo(v) { ... } }`), or removes if null. */ - setter(name: string, stmt: StmtFn | null): this { + setter(name: string, stmt: Stmt | null): this { if (stmt === null) { this._props.delete(`setter:${name}`); } else { @@ -104,7 +121,7 @@ export class ObjectTsDsl extends Mixed { } /** Adds a spread property (e.g., `{ ...options }`). */ - spread(expr: ExprFn): this { + spread(expr: Expr): this { const key = `spread:${this._spreadCounter++}`; this._props.set(key, new ObjectPropTsDsl({ kind: 'spread' }).value(expr)); return this; diff --git a/packages/openapi-ts/src/ts-dsl/expr/prop.ts b/packages/openapi-ts/src/ts-dsl/expr/prop.ts index 308ba82a0..76d1ac376 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/prop.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/prop.ts @@ -7,17 +7,20 @@ import { TsDsl } from '../base'; import { GetterTsDsl } from '../decl/getter'; import { SetterTsDsl } from '../decl/setter'; import { DocMixin } from '../mixins/doc'; +import type { f } from '../utils/factories'; import { safePropName } from '../utils/name'; import { IdTsDsl } from './id'; -type Expr = NodeName | MaybeTsDsl; -type Stmt = NodeName | MaybeTsDsl; - -export type ObjectPropKind = 'computed' | 'getter' | 'prop' | 'setter' | 'spread'; +export type ObjectPropKind = 'computed' | 'getter' | 'method' | 'prop' | 'setter' | 'spread'; +export type ObjectPropValue = + | NodeName + | MaybeTsDsl + | ReturnType; type Meta = | { kind: 'computed'; name: string } | { kind: 'getter'; name: string } + | { kind: 'method'; name: string } | { kind: 'prop'; name: string } | { kind: 'setter'; name: string } | { kind: 'spread'; name?: undefined }; @@ -27,7 +30,7 @@ const Mixed = DocMixin(TsDsl); export class ObjectPropTsDsl extends Mixed { readonly '~dsl' = 'ObjectPropTsDsl'; - protected _value?: Ref; + protected _value?: Ref; protected _meta: Meta; constructor(meta: Meta) { @@ -35,14 +38,6 @@ export class ObjectPropTsDsl extends Mixed { this._meta = meta; } - get kind(): ObjectPropKind { - return this._meta.kind; - } - - get propName(): string | undefined { - return this._meta.name; - } - override analyze(ctx: AnalysisContext): void { super.analyze(ctx); ctx.analyze(this._value); @@ -52,12 +47,16 @@ export class ObjectPropTsDsl extends Mixed { return !this.missingRequiredCalls().length; } - value(value: Expr | Stmt | ((p: ObjectPropTsDsl) => void)) { - if (typeof value === 'function') { - value(this); - } else { - this._value = ref(value); - } + get kind(): ObjectPropKind { + return this._meta.kind; + } + + get propName(): string | undefined { + return this._meta.name; + } + + value(value: ObjectPropValue): this { + this._value = ref(value); return this; } @@ -68,19 +67,22 @@ export class ObjectPropTsDsl extends Mixed { if (ts.isStatement(node)) { throw new Error('Invalid spread: object spread must be an expression, not a statement.'); } - const result = ts.factory.createSpreadAssignment(node); + const result = ts.factory.createSpreadAssignment(node as ts.Expression); return this.$docs(result); } if (this._meta.kind === 'getter') { - const getter = new GetterTsDsl(this._meta.name).do(node); + const getter = new GetterTsDsl(this._meta.name).do(node as ts.Statement); const result = this.$node(getter); return this.$docs(result); } if (this._meta.kind === 'setter') { - const setter = new SetterTsDsl(this._meta.name).do(node); + const setter = new SetterTsDsl(this._meta.name).do(node as ts.Statement); const result = this.$node(setter); return this.$docs(result); } + if (this._meta.kind === 'method') { + return this.$docs(node as ts.MethodDeclaration); + } if (ts.isIdentifier(node) && node.text === this._meta.name) { const result = ts.factory.createShorthandPropertyAssignment(this._meta.name); return this.$docs(result); @@ -94,13 +96,13 @@ export class ObjectPropTsDsl extends Mixed { this._meta.kind === 'computed' ? ts.factory.createComputedPropertyName(this.$node(new IdTsDsl(this._meta.name))) : this.$node(safePropName(this._meta.name)), - node, + node as ts.Expression, ); return this.$docs(result); } $validate(): asserts this is this & { - _value: Expr | Stmt; + _value: ObjectPropValue; kind: ObjectPropKind; } { const missing = this.missingRequiredCalls(); diff --git a/packages/openapi-ts/src/ts-dsl/expr/spread.ts b/packages/openapi-ts/src/ts-dsl/expr/spread.ts new file mode 100644 index 000000000..084eea422 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/expr/spread.ts @@ -0,0 +1,34 @@ +import type { AnalysisContext, NodeName, Ref } from '@hey-api/codegen-core'; +import { ref } from '@hey-api/codegen-core'; +import ts from 'typescript'; + +import type { MaybeTsDsl } from '../base'; +import { TsDsl } from '../base'; +import { f } from '../utils/factories'; + +export type SpreadExpr = NodeName | MaybeTsDsl; +export type SpreadCtor = (expr: SpreadExpr) => SpreadTsDsl; + +const Mixed = TsDsl; + +export class SpreadTsDsl extends Mixed { + readonly '~dsl' = 'SpreadTsDsl'; + + protected _expr: Ref; + + constructor(expr: SpreadExpr) { + super(); + this._expr = ref(expr); + } + + override analyze(ctx: AnalysisContext): void { + super.analyze(ctx); + ctx.analyze(this._expr); + } + + override toAst() { + return ts.factory.createSpreadElement(this.$node(this._expr)); + } +} + +f.spread.set((...args) => new SpreadTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/expr/typeof.ts b/packages/openapi-ts/src/ts-dsl/expr/typeof.ts index 99dae0cdc..623090132 100644 --- a/packages/openapi-ts/src/ts-dsl/expr/typeof.ts +++ b/packages/openapi-ts/src/ts-dsl/expr/typeof.ts @@ -1,4 +1,4 @@ -import type { AnalysisContext } from '@hey-api/codegen-core'; +import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; import ts from 'typescript'; import type { MaybeTsDsl } from '../base'; @@ -6,7 +6,7 @@ import { TsDsl } from '../base'; import { OperatorMixin } from '../mixins/operator'; import { f } from '../utils/factories'; -export type TypeOfExpr = string | MaybeTsDsl; +export type TypeOfExpr = NodeName | MaybeTsDsl; export type TypeOfExprCtor = (expr: TypeOfExpr) => TypeOfExprTsDsl; const Mixed = OperatorMixin(TsDsl); diff --git a/packages/openapi-ts/src/ts-dsl/index.ts b/packages/openapi-ts/src/ts-dsl/index.ts index 254095318..282f25c39 100644 --- a/packages/openapi-ts/src/ts-dsl/index.ts +++ b/packages/openapi-ts/src/ts-dsl/index.ts @@ -1,3 +1,4 @@ +import type { NodeName } from '@hey-api/codegen-core'; import type ts from 'typescript'; import { ClassTsDsl } from './decl/class'; @@ -28,6 +29,7 @@ import { PostfixTsDsl } from './expr/postfix'; import { PrefixTsDsl } from './expr/prefix'; import { ObjectPropTsDsl } from './expr/prop'; import { RegExpTsDsl } from './expr/regexp'; +import { SpreadTsDsl } from './expr/spread'; import { TemplateTsDsl } from './expr/template'; import { TernaryTsDsl } from './expr/ternary'; import { TypeOfExprTsDsl } from './expr/typeof'; @@ -61,6 +63,7 @@ import { TypeParamTsDsl } from './type/param'; import { TypeQueryTsDsl } from './type/query'; import { TypeTemplateTsDsl } from './type/template'; import { TypeTupleTsDsl } from './type/tuple'; +import { TypeTupleMemberTsDsl } from './type/tuple-member'; import { LazyTsDsl } from './utils/lazy'; const tsDsl = { @@ -135,9 +138,9 @@ const tsDsl = { }) as { (): FuncTsDsl<'arrow'>; (fn: (f: FuncTsDsl<'arrow'>) => void): FuncTsDsl<'arrow'>; - (name: string): FuncTsDsl<'decl'>; - (name: string, fn: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'decl'>; - (name?: string, fn?: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'arrow'> | FuncTsDsl<'decl'>; + (name: NodeName): FuncTsDsl<'decl'>; + (name: NodeName, fn: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'decl'>; + (name?: NodeName, fn?: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'arrow'> | FuncTsDsl<'decl'>; }, /** Creates a getter method declaration. */ @@ -213,6 +216,9 @@ const tsDsl = { /** Creates a setter method declaration. */ setter: (...args: ConstructorParameters) => new SetterTsDsl(...args), + /** Creates a spread element from an expression (e.g., `...expr`). */ + spread: (...args: ConstructorParameters) => new SpreadTsDsl(...args), + /** Wraps an expression or statement-like value into a `StmtTsDsl`. */ stmt: (...args: ConstructorParameters) => new StmtTsDsl(...args), @@ -287,6 +293,10 @@ const tsDsl = { /** Creates a tuple type (e.g., [A, B, C]). */ tuple: (...args: ConstructorParameters) => new TypeTupleTsDsl(...args), + + /** Creates a named tuple element (e.g., `[resolver?: R]`). */ + tupleMember: (...args: ConstructorParameters) => + new TypeTupleMemberTsDsl(...args), }, ), diff --git a/packages/openapi-ts/src/ts-dsl/mixins/args.ts b/packages/openapi-ts/src/ts-dsl/mixins/args.ts index e299df019..da6f382f5 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/args.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/args.ts @@ -5,11 +5,11 @@ import type ts from 'typescript'; import type { MaybeTsDsl } from '../base'; import type { BaseCtor, MixinCtor } from './types'; -type Arg = NodeName | MaybeTsDsl; +type Arg = NodeName | MaybeTsDsl; export interface ArgsMethods extends Node { - /** Renders the arguments into an array of `Expression`s. */ - $args(): ReadonlyArray; + /** Renders the arguments into an array of `Expression`s or `SpreadElement`s. */ + $args(): ReadonlyArray; /** Adds a single expression argument. */ arg(arg: Arg | undefined): this; /** Adds one or more expression arguments. */ @@ -42,7 +42,7 @@ export function ArgsMixin>(Base: TB return this; } - protected $args(): ReadonlyArray { + protected $args(): ReadonlyArray { return this.$node(this._args).map((arg) => this.$node(arg)); } } diff --git a/packages/openapi-ts/src/ts-dsl/mixins/spread.ts b/packages/openapi-ts/src/ts-dsl/mixins/spread.ts new file mode 100644 index 000000000..e2417972f --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/mixins/spread.ts @@ -0,0 +1,24 @@ +import type { AnalysisContext, Node } from '@hey-api/codegen-core'; +import type ts from 'typescript'; + +import { f } from '../utils/factories'; +import type { BaseCtor, MixinCtor } from './types'; + +export interface SpreadMethods extends Node { + /** Produces a spread element from the current expression (e.g., `...expr`). */ + spread(): ReturnType; +} + +export function SpreadMixin>(Base: TBase) { + abstract class Spread extends Base { + override analyze(ctx: AnalysisContext): void { + super.analyze(ctx); + } + + protected spread(): ReturnType { + return f.spread(this); + } + } + + return Spread as unknown as MixinCtor; +} diff --git a/packages/openapi-ts/src/ts-dsl/stmt/if.ts b/packages/openapi-ts/src/ts-dsl/stmt/if.ts index 8d7a1b8f1..c75077c98 100644 --- a/packages/openapi-ts/src/ts-dsl/stmt/if.ts +++ b/packages/openapi-ts/src/ts-dsl/stmt/if.ts @@ -1,4 +1,4 @@ -import type { AnalysisContext } from '@hey-api/codegen-core'; +import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; import ts from 'typescript'; import type { MaybeTsDsl } from '../base'; @@ -7,7 +7,7 @@ import type { DoExpr } from '../mixins/do'; import { DoMixin } from '../mixins/do'; import { BlockTsDsl } from './block'; -export type IfCondition = string | MaybeTsDsl; +export type IfCondition = NodeName | MaybeTsDsl; const Mixed = DoMixin(TsDsl); diff --git a/packages/openapi-ts/src/ts-dsl/stmt/var.ts b/packages/openapi-ts/src/ts-dsl/stmt/var.ts index 766036fc1..f39a8df99 100644 --- a/packages/openapi-ts/src/ts-dsl/stmt/var.ts +++ b/packages/openapi-ts/src/ts-dsl/stmt/var.ts @@ -52,8 +52,8 @@ export class VarTsDsl extends Mixed { } /** Sets the variable type. */ - type(type: string | TypeTsDsl): this { - this._type = type instanceof TypeTsDsl ? type : new TypeExprTsDsl(type); + type(node: NodeName | TypeTsDsl): this { + this._type = node instanceof TypeTsDsl ? node : new TypeExprTsDsl(node); return this; } diff --git a/packages/openapi-ts/src/ts-dsl/type/tuple-member.ts b/packages/openapi-ts/src/ts-dsl/type/tuple-member.ts new file mode 100644 index 000000000..cb02f9d7e --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/tuple-member.ts @@ -0,0 +1,64 @@ +import type { AnalysisContext, NodeName, NodeScope, Ref } from '@hey-api/codegen-core'; +import { ref } from '@hey-api/codegen-core'; +import ts from 'typescript'; + +import type { TypeTsDsl as TypeTsDslType } from '../base'; +import { TsDsl } from '../base'; +import { OptionalMixin } from '../mixins/optional'; +import { f } from '../utils/factories'; + +export type TypeTupleMemberCtor = (name: NodeName) => TypeTupleMemberTsDsl; + +const Mixed = OptionalMixin(TsDsl); + +export class TypeTupleMemberTsDsl extends Mixed { + readonly '~dsl' = 'TypeTupleMemberTsDsl'; + override scope: NodeScope = 'type'; + + protected _type?: Ref; + + constructor(name: NodeName) { + super(); + this.name.set(name); + } + + override analyze(ctx: AnalysisContext): void { + super.analyze(ctx); + ctx.analyze(this.name); + ctx.analyze(this._type); + } + + /** Returns true when all required builder calls are present. */ + get isValid(): boolean { + return !this.missingRequiredCalls().length; + } + + type(node: NodeName | TypeTsDslType): this { + this._type = ref(node); + return this; + } + + override toAst() { + this.$validate(); + return ts.factory.createNamedTupleMember( + undefined, + this.$node(this.name) as ts.Identifier, + this._optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, + this.$type(this._type!), + ); + } + + $validate(): asserts this { + const missing = this.missingRequiredCalls(); + if (!missing.length) return; + throw new Error(`Tuple member missing ${missing.join(' and ')}`); + } + + private missingRequiredCalls(): Array { + const missing: Array = []; + if (!this._type) missing.push('.\u200Btype()'); + return missing; + } +} + +f.type.tupleMember.set((name) => new TypeTupleMemberTsDsl(name)); diff --git a/packages/openapi-ts/src/ts-dsl/type/tuple.ts b/packages/openapi-ts/src/ts-dsl/type/tuple.ts index 6577650eb..2d1b5a0eb 100644 --- a/packages/openapi-ts/src/ts-dsl/type/tuple.ts +++ b/packages/openapi-ts/src/ts-dsl/type/tuple.ts @@ -4,15 +4,17 @@ import ts from 'typescript'; import type { TypeTsDsl } from '../base'; import { TsDsl } from '../base'; +export type TupleElement = string | ts.TypeNode | TypeTsDsl; + const Mixed = TsDsl; export class TypeTupleTsDsl extends Mixed { readonly '~dsl' = 'TypeTupleTsDsl'; override scope: NodeScope = 'type'; - protected _elements: Array = []; + protected _elements: Array = []; - constructor(...nodes: Array) { + constructor(...nodes: Array) { super(); this.elements(...nodes); } @@ -24,7 +26,7 @@ export class TypeTupleTsDsl extends Mixed { } } - elements(...types: Array): this { + elements(...types: Array): this { this._elements.push(...types); return this; } diff --git a/packages/openapi-ts/src/ts-dsl/utils/factories.ts b/packages/openapi-ts/src/ts-dsl/utils/factories.ts index 58d8071ce..bad16c31b 100644 --- a/packages/openapi-ts/src/ts-dsl/utils/factories.ts +++ b/packages/openapi-ts/src/ts-dsl/utils/factories.ts @@ -1,14 +1,17 @@ +import type { MethodCtor } from '../decl/method'; import type { AsCtor } from '../expr/as'; import type { AttrCtor } from '../expr/attr'; import type { AwaitCtor } from '../expr/await'; import type { CallCtor } from '../expr/call'; import type { NewCtor } from '../expr/new'; +import type { SpreadCtor } from '../expr/spread'; import type { TypeOfExprCtor } from '../expr/typeof'; import type { ReturnCtor } from '../stmt/return'; import type { TypeExprCtor } from '../type/expr'; import type { TypeIdxCtor } from '../type/idx'; import type { TypeOperatorCtor } from '../type/operator'; import type { TypeQueryCtor } from '../type/query'; +import type { TypeTupleMemberCtor } from '../type/tuple-member'; type Ctor = (...args: Array) => any; @@ -46,12 +49,18 @@ export const f = { /** Factory for creating function or method call expressions (e.g., `fn(arg)`). */ call: createFactory('call'), + /** Factory for creating method declarations (e.g., `{ foo() { ... } }`). */ + method: createFactory('method'), + /** Factory for creating new expressions (e.g., `new ClassName()`). */ new: createFactory('new'), /** Factory for creating return statements. */ return: createFactory('return'), + /** Factory for creating spread expressions (e.g., `...expr`). */ + spread: createFactory('spread'), + /** Factories for creating type nodes. */ type: { /** Factory for creating basic type references or type expressions (e.g., Foo or Foo). */ @@ -65,6 +74,9 @@ export const f = { /** Factory for creating type query nodes (e.g., `typeof Foo`). */ query: createFactory('type.query'), + + /** Factory for creating named tuple elements (e.g., `[resolver?: R]`). */ + tupleMember: createFactory('type.tupleMember'), }, /** Factory for creating `typeof` expressions (e.g., `typeof value`). */