From ecad295426f40f6162d2bc4f5496f46f280a6a1c Mon Sep 17 00:00:00 2001 From: Lubos Date: Mon, 10 Nov 2025 00:51:42 +0800 Subject: [PATCH] chore: wip --- dev/openapi-ts.config.ts | 8 +- .../src/plugins/@hey-api/sdk/shared/class.ts | 89 +++++---- .../src/plugins/valibot/shared/export.ts | 27 +-- .../src/plugins/valibot/shared/pipesToAst.ts | 11 +- packages/openapi-ts/src/ts-dsl/base.ts | 57 +++--- packages/openapi-ts/src/ts-dsl/class.ts | 21 +- packages/openapi-ts/src/ts-dsl/expr.ts | 4 +- packages/openapi-ts/src/ts-dsl/field.ts | 8 +- packages/openapi-ts/src/ts-dsl/func.ts | 12 +- packages/openapi-ts/src/ts-dsl/index.ts | 62 +++++- packages/openapi-ts/src/ts-dsl/method.ts | 8 +- .../openapi-ts/src/ts-dsl/mixins/generics.ts | 3 +- .../openapi-ts/src/ts-dsl/mixins/param.ts | 14 +- packages/openapi-ts/src/ts-dsl/mixins/type.ts | 37 ++-- packages/openapi-ts/src/ts-dsl/new.ts | 3 +- packages/openapi-ts/src/ts-dsl/param.ts | 8 +- packages/openapi-ts/src/ts-dsl/type.ts | 187 ++---------------- packages/openapi-ts/src/ts-dsl/type/attr.ts | 19 ++ packages/openapi-ts/src/ts-dsl/type/base.ts | 39 ++++ packages/openapi-ts/src/ts-dsl/type/expr.ts | 43 ++++ .../openapi-ts/src/ts-dsl/type/literal.ts | 18 ++ packages/openapi-ts/src/ts-dsl/type/object.ts | 19 ++ packages/openapi-ts/src/ts-dsl/type/param.ts | 24 +++ packages/openapi-ts/src/ts-dsl/type/prop.ts | 44 +++++ packages/openapi-ts/src/ts-dsl/var.ts | 8 +- 25 files changed, 436 insertions(+), 337 deletions(-) create mode 100644 packages/openapi-ts/src/ts-dsl/type/attr.ts create mode 100644 packages/openapi-ts/src/ts-dsl/type/base.ts create mode 100644 packages/openapi-ts/src/ts-dsl/type/expr.ts create mode 100644 packages/openapi-ts/src/ts-dsl/type/literal.ts create mode 100644 packages/openapi-ts/src/ts-dsl/type/object.ts create mode 100644 packages/openapi-ts/src/ts-dsl/type/param.ts create mode 100644 packages/openapi-ts/src/ts-dsl/type/prop.ts diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts index 58653937d..57f671c3e 100644 --- a/dev/openapi-ts.config.ts +++ b/dev/openapi-ts.config.ts @@ -36,14 +36,14 @@ export default defineConfig(() => { // }, path: path.resolve( getSpecsPath(), - // '3.0.x', - '3.1.x', - // 'circular.yaml', + '3.0.x', + // '3.1.x', + 'circular.yaml', // 'dutchie.json', // 'invalid', // 'full.yaml', // 'openai.yaml', - 'opencode.yaml', + // 'opencode.yaml', // 'sdk-instance.yaml', // 'string-with-format.yaml', // 'transformers.json', diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/class.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/class.ts index 5a01884cb..1bef86988 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/class.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/class.ts @@ -68,7 +68,7 @@ const createRegistryClass = ({ f .private() .readonly() - .type($.type('Map').generics('string', 'T')) + // .type($.type('Map').generics('string', 'T')) .assign($.new('Map')), ) .newline() @@ -132,12 +132,13 @@ const createClientClass = ({ .param('args', (p) => p .optional(optionalClient) - .type() - .object((o) => - o.prop('client', (p) => - p.optional(optionalClient).type(symbolClient.placeholder), - ), - ), + // .type($.type + // .expr('todo') + // // .object() + // // .prop('client', (p) => + // // p.optional(optionalClient).type(symbolClient.placeholder), + // // ), + // ), ) .do( $('this') @@ -173,14 +174,14 @@ export const generateClassSdk = ({ context: plugin.context, operation, }); - const symbolResponse = isNuxtClient - ? plugin.querySymbol({ - category: 'type', - resource: 'operation', - resourceId: operation.id, - role: 'response', - }) - : undefined; + // const symbolResponse = isNuxtClient + // ? plugin.querySymbol({ + // category: 'type', + // resource: 'operation', + // resourceId: operation.id, + // role: 'response', + // }) + // : undefined; const classes = operationClasses({ context: plugin.context, @@ -266,28 +267,30 @@ export const generateClassSdk = ({ m .generic(nuxtTypeComposable, (t) => t - .extends( - plugin.referenceSymbol({ - category: 'external', - resource: 'client.Composable', - }).placeholder, - ) - .default($.type.literal('$fetch')), + // .extends( + // plugin.referenceSymbol({ + // category: 'external', + // resource: 'client.Composable', + // }).placeholder, + // ) + // .default($.type.literal('$fetch')), ) .generic(nuxtTypeDefault, (t) => - t.$if(symbolResponse, (t, s) => - t.extends(s.placeholder).default(s.placeholder), - ), + t + // t.$if(symbolResponse, (t, s) => + // t, + // // t.extends(s.placeholder).default(s.placeholder), + // ), ), (m) => m.generic('ThrowOnError', (t) => t - .extends('boolean') - .default( - ('throwOnError' in client.config - ? client.config.throwOnError - : false) ?? false, - ), + // .extends('boolean') + // .default( + // ('throwOnError' in client.config + // ? client.config.throwOnError + // : false) ?? false, + // ), ), ) .params(...toParameterDeclarations(opParameters.parameters)) @@ -438,25 +441,21 @@ export const generateClassSdk = ({ category: 'client', }); const isClientRequired = !plugin.config.client || !symClient; - const symbolClient = plugin.referenceSymbol({ - category: 'external', - resource: 'client.Client', - }); + // const symbolClient = plugin.referenceSymbol({ + // category: 'external', + // resource: 'client.Client', + // }); const ctor = $.init((i) => i .param('args', (p) => p .optional(!isClientRequired) - .type() - .object((o) => - o - .prop('client', (p) => - p - .optional(!isClientRequired) - .type(symbolClient.placeholder), - ) - .prop('key', (p) => p.optional().type('string')), - ), + // .type($.type + // .expr('todo') + // // .object() + // // .prop('client', (p) => p.optional(!isClientRequired).type(symbolClient.placeholder)) + // // .prop('key', (p) => p.optional().type('string')), + // ) ) .do( $('super').call('args'), diff --git a/packages/openapi-ts/src/plugins/valibot/shared/export.ts b/packages/openapi-ts/src/plugins/valibot/shared/export.ts index f19f2c536..7d5963b5f 100644 --- a/packages/openapi-ts/src/plugins/valibot/shared/export.ts +++ b/packages/openapi-ts/src/plugins/valibot/shared/export.ts @@ -1,11 +1,10 @@ import type { Symbol } from '@hey-api/codegen-core'; -import type ts from 'typescript'; import type { IR } from '~/ir/types'; import { createSchemaComment } from '~/plugins/shared/utils/schema'; -import { tsc } from '~/tsc'; +import { $ } from '~/ts-dsl'; -import { identifiers } from '../v1/constants'; +// import { identifiers } from '../v1/constants'; import { pipesToAst } from './pipesToAst'; import type { Ast, IrSchemaToAstOptions } from './types'; @@ -25,19 +24,13 @@ export const exportAst = ({ resource: 'valibot.v', }); - const statement = tsc.constVariable({ - comment: plugin.config.comments - ? createSchemaComment({ schema }) - : undefined, - exportConst: symbol.exported, - expression: pipesToAst({ pipes: ast.pipes, plugin }), - name: symbol.placeholder, - typeName: state.hasLazyExpression.value - ? (tsc.propertyAccessExpression({ - expression: v.placeholder, - name: ast.typeName || identifiers.types.GenericSchema.text, - }) as unknown as ts.TypeNode) - : undefined, - }); + const statement = $.const(symbol.placeholder) + .export(symbol.exported) + .$if(plugin.config.comments && createSchemaComment({ schema }), (c, v) => c.describe(v as Array)) + // .$if(state.hasLazyExpression.value, (c) => + // // c.type($.type(v.placeholder)) + // // c.type($.type(v.placeholder).attr(ast.typeName || identifiers.types.GenericSchema)) + // ) + .assign(pipesToAst({ pipes: ast.pipes, plugin })); plugin.setSymbolValue(symbol, statement); }; diff --git a/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts b/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts index dabc1682c..319902c1e 100644 --- a/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts +++ b/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts @@ -1,6 +1,6 @@ import type ts from 'typescript'; -import { tsc } from '~/tsc'; +import { $ } from '~/ts-dsl'; import type { ValibotPlugin } from '../types'; import { identifiers } from '../v1/constants'; @@ -20,12 +20,5 @@ export const pipesToAst = ({ category: 'external', resource: 'valibot.v', }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.methods.pipe, - }), - parameters: pipes, - }); - return expression; + return $(v.placeholder).attr(identifiers.methods.pipe).call(...pipes).$render(); }; diff --git a/packages/openapi-ts/src/ts-dsl/base.ts b/packages/openapi-ts/src/ts-dsl/base.ts index 329537f58..c59919449 100644 --- a/packages/openapi-ts/src/ts-dsl/base.ts +++ b/packages/openapi-ts/src/ts-dsl/base.ts @@ -101,6 +101,9 @@ export abstract class TsDsl implements ITsDsl { if (typeof input === 'string') { return this.$expr(input) as NodeOfMaybe; } + if (typeof input === 'boolean') { + return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as NodeOfMaybe; + } if (input instanceof Array) { return input.map((item) => this._render(item)) as NodeOfMaybe; } @@ -122,20 +125,21 @@ export abstract class TsDsl implements ITsDsl { }); } - protected $type(type: I): TypeOfMaybe { - if (type === undefined) { + protected $type(input: I, args?: ReadonlyArray): TypeOfMaybe { + if (input === undefined) { return undefined as TypeOfMaybe; } - if (typeof type === 'string') { - return ts.factory.createTypeReferenceNode( - type, - ) as unknown as TypeOfMaybe; + if (typeof input === 'string') { + return ts.factory.createTypeReferenceNode(input, args) as TypeOfMaybe; } - if (typeof type === 'boolean') { - const literal = type ? ts.factory.createTrue() : ts.factory.createFalse(); + if (typeof input === 'boolean') { + const literal = input ? ts.factory.createTrue() : ts.factory.createFalse(); return ts.factory.createLiteralTypeNode(literal) as TypeOfMaybe; } - return this._render(type as any) as TypeOfMaybe; + if (input instanceof Array) { + return input.map((item) => this.$type(item, args)) as TypeOfMaybe; + } + return this._render(input as any) as TypeOfMaybe; } private _render(value: MaybeTsDsl): T { @@ -152,25 +156,30 @@ type NodeOf = ? ReadonlyArray ? N : U> : I extends string ? ts.Expression - : I extends TsDsl - ? N - : I extends ts.Node - ? I - : never; + : I extends boolean + ? ts.Expression + : I extends TsDsl + ? N + : I extends ts.Node + ? I + : never; type TypeOfMaybe = undefined extends I ? TypeOf> | undefined : TypeOf; -type TypeOf = I extends string - ? ts.TypeNode - : I extends boolean - ? ts.LiteralTypeNode - : I extends TsDsl - ? N - : I extends ts.TypeNode - ? I - : never; +type TypeOf = + I extends ReadonlyArray + ? ReadonlyArray> + : I extends string + ? ts.TypeNode + : I extends boolean + ? ts.LiteralTypeNode + : I extends TsDsl + ? N + : I extends ts.TypeNode + ? I + : never; export type MaybeTsDsl = // if T includes string in the union @@ -182,3 +191,5 @@ export type MaybeTsDsl = T extends ts.Node ? T | TsDsl : never; + +export type TypeOfTsDsl = T extends TsDsl ? U : never; diff --git a/packages/openapi-ts/src/ts-dsl/class.ts b/packages/openapi-ts/src/ts-dsl/class.ts index e1317e4c5..cb2cea341 100644 --- a/packages/openapi-ts/src/ts-dsl/class.ts +++ b/packages/openapi-ts/src/ts-dsl/class.ts @@ -19,8 +19,8 @@ import { import { NewlineTsDsl } from './newline'; export class ClassTsDsl extends TsDsl { - private heritageClauses: Array = []; private body: Array | NewlineTsDsl> = []; + private extended: Array = []; private modifiers = createModifierAccessor(this); private name: string; @@ -38,15 +38,7 @@ export class ClassTsDsl extends TsDsl { /** Adds a base class to extend from. */ extends(base?: WithString | false | null): this { - if (!base) return this; - this.heritageClauses.push( - ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ - ts.factory.createExpressionWithTypeArguments( - this.$expr(base), - undefined, - ), - ]), - ); + if (base) this.extended.push(base); return this; } @@ -84,7 +76,14 @@ export class ClassTsDsl extends TsDsl { [...this.$decorators(), ...this.modifiers.list()], ts.factory.createIdentifier(this.name), this.$generics(), - this.heritageClauses, + this.extended.map((extended) => + ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ + ts.factory.createExpressionWithTypeArguments( + this.$expr(extended), + undefined, + ), + ]) + ), body, ); } diff --git a/packages/openapi-ts/src/ts-dsl/expr.ts b/packages/openapi-ts/src/ts-dsl/expr.ts index 9167bc82b..f07aeb37b 100644 --- a/packages/openapi-ts/src/ts-dsl/expr.ts +++ b/packages/openapi-ts/src/ts-dsl/expr.ts @@ -10,9 +10,9 @@ import { OperatorMixin } from './mixins/operator'; export class ExprTsDsl extends TsDsl { private _exprInput: MaybeTsDsl; - constructor(id: MaybeTsDsl) { + constructor(expr: MaybeTsDsl) { super(); - this._exprInput = id; + this._exprInput = expr; } $render(): ts.Expression { diff --git a/packages/openapi-ts/src/ts-dsl/field.ts b/packages/openapi-ts/src/ts-dsl/field.ts index 12832ad89..f0c5c7075 100644 --- a/packages/openapi-ts/src/ts-dsl/field.ts +++ b/packages/openapi-ts/src/ts-dsl/field.ts @@ -13,16 +13,16 @@ import { ReadonlyMixin, StaticMixin, } from './mixins/modifiers'; -import { createTypeAccessor, type TypeAccessor } from './mixins/type'; +import { createType, type Type } from './mixins/type'; import { ValueMixin } from './mixins/value'; export class FieldTsDsl extends TsDsl { private modifiers = createModifierAccessor(this); private name: string; - private _type: TypeAccessor = createTypeAccessor(this); + private _type: Type = createType(this); /** Sets the property's type. */ - type: TypeAccessor['fn'] = this._type.fn; + type: Type['fn'] = this._type.fn; constructor(name: string, fn?: (f: FieldTsDsl) => void) { super(); @@ -36,7 +36,7 @@ export class FieldTsDsl extends TsDsl { [...this.$decorators(), ...this.modifiers.list()], ts.factory.createIdentifier(this.name), undefined, - this._type.$render(), + this.$type(this._type), this.$value(), ); } diff --git a/packages/openapi-ts/src/ts-dsl/func.ts b/packages/openapi-ts/src/ts-dsl/func.ts index d89a16cc8..d86bea2ae 100644 --- a/packages/openapi-ts/src/ts-dsl/func.ts +++ b/packages/openapi-ts/src/ts-dsl/func.ts @@ -18,7 +18,7 @@ import { } from './mixins/modifiers'; import { OptionalMixin } from './mixins/optional'; import { ParamMixin } from './mixins/param'; -import { createTypeAccessor, type TypeAccessor } from './mixins/type'; +import { createType, type Type } from './mixins/type'; type FuncMode = 'arrow' | 'decl' | 'expr'; @@ -32,10 +32,10 @@ class ImplFuncTsDsl extends TsDsl< private mode: FuncMode; private modifiers = createModifierAccessor(this); private name?: string; - private _returns: TypeAccessor> = createTypeAccessor(this); + private _returns: Type> = createType(this); /** Sets the return type. */ - returns: TypeAccessor>['fn'] = this._returns.fn; + returns: Type>['fn'] = this._returns.fn; constructor(); constructor(fn: (f: ImplFuncTsDsl<'arrow'>) => void); @@ -83,7 +83,7 @@ class ImplFuncTsDsl extends TsDsl< this.name, this.$generics(), this.$params(), - this._returns.$render(), + this.$type(this._returns), ts.factory.createBlock(this.$do(), true), ) as any; } @@ -95,7 +95,7 @@ class ImplFuncTsDsl extends TsDsl< this.name ? ts.factory.createIdentifier(this.name) : undefined, this.$generics(), this.$params(), - this._returns.$render(), + this.$type(this._returns), ts.factory.createBlock(this.$do(), true), ) as any; } @@ -110,7 +110,7 @@ class ImplFuncTsDsl extends TsDsl< [...this.modifiers.list()], this.$generics(), this.$params(), - this._returns.$render(), + this.$type(this._returns), undefined, exprBody, ) as any; diff --git a/packages/openapi-ts/src/ts-dsl/index.ts b/packages/openapi-ts/src/ts-dsl/index.ts index 40ff9137f..360e05428 100644 --- a/packages/openapi-ts/src/ts-dsl/index.ts +++ b/packages/openapi-ts/src/ts-dsl/index.ts @@ -23,30 +23,51 @@ import { ReturnTsDsl } from './return'; import { SetterTsDsl } from './setter'; import { TemplateTsDsl } from './template'; import { ThrowTsDsl } from './throw'; -import { TypeTsDsl } from './type'; +// import { type } from './type'; import { VarTsDsl } from './var'; const base = { + /** Creates a property access expression (e.g. `obj.foo`). */ attr: (...args: ConstructorParameters) => new AttrTsDsl(...args), + + /** Creates an await expression (e.g. `await promise`). */ await: (...args: ConstructorParameters) => new AwaitTsDsl(...args), + + /** Creates a binary expression (e.g. `a + b`). */ binary: (...args: ConstructorParameters) => new BinaryTsDsl(...args), + + /** Creates a function or method call expression (e.g. `fn(arg)`). */ call: (...args: ConstructorParameters) => new CallTsDsl(...args), + + /** Creates a class declaration or expression. */ class: (...args: ConstructorParameters) => new ClassTsDsl(...args), + + /** Creates a constant variable declaration (`const`). */ const: (...args: ConstructorParameters) => new VarTsDsl(...args).const(), + + /** Creates a decorator expression (e.g. `@decorator`). */ decorator: (...args: ConstructorParameters) => new DecoratorTsDsl(...args), + + /** Creates a describe block (used for tests or descriptions). */ describe: (...args: ConstructorParameters) => new DescribeTsDsl(...args), + + /** Creates a general expression node. */ expr: (...args: ConstructorParameters) => new ExprTsDsl(...args), + + /** Creates a field declaration in a class or object. */ field: (...args: ConstructorParameters) => new FieldTsDsl(...args), + + /** Creates a function expression or declaration. */ func: ((nameOrFn?: any, fn?: any) => { if (nameOrFn === undefined) return new FuncTsDsl(); if (typeof nameOrFn !== 'string') return new FuncTsDsl(nameOrFn); @@ -58,42 +79,79 @@ const base = { (name: string): FuncTsDsl<'decl'>; (name: string, fn: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'decl'>; }, + + /** Creates a getter method declaration. */ getter: (...args: ConstructorParameters) => new GetterTsDsl(...args), + + /** Creates an if statement. */ if: (...args: ConstructorParameters) => new IfTsDsl(...args), + + /** Creates an initialization block or statement. */ init: (...args: ConstructorParameters) => new InitTsDsl(...args), + + /** Creates a let variable declaration (`let`). */ let: (...args: ConstructorParameters) => new VarTsDsl(...args).let(), + + /** Creates a literal value (e.g. string, number, boolean). */ literal: (...args: ConstructorParameters) => new LiteralTsDsl(...args), + + /** Creates a method declaration inside a class or object. */ method: (...args: ConstructorParameters) => new MethodTsDsl(...args), + + /** Creates a new expression (e.g. `new ClassName()`). */ new: (...args: ConstructorParameters) => new NewTsDsl(...args), + + /** Creates a newline (for formatting purposes). */ newline: (...args: ConstructorParameters) => new NewlineTsDsl(...args), + + /** Creates a logical NOT expression (e.g. `!expr`). */ not: (...args: ConstructorParameters) => new NotTsDsl(...args), + + /** Creates an object literal expression. */ object: (...args: ConstructorParameters) => new ObjectTsDsl(...args), + + /** Creates a parameter declaration for functions or methods. */ param: (...args: ConstructorParameters) => new ParamTsDsl(...args), + + /** Creates a pattern for destructuring or matching. */ pattern: (...args: ConstructorParameters) => new PatternTsDsl(...args), + + /** Creates a return statement. */ return: (...args: ConstructorParameters) => new ReturnTsDsl(...args), + + /** Creates a setter method declaration. */ setter: (...args: ConstructorParameters) => new SetterTsDsl(...args), + + /** Creates a template literal expression. */ template: (...args: ConstructorParameters) => new TemplateTsDsl(...args), + + /** Creates a throw statement. */ throw: (...args: ConstructorParameters) => new ThrowTsDsl(...args), - type: TypeTsDsl, + + /** Provides access to type-related DSL utilities. */ + // type, + + /** Creates a variable declaration (var). */ var: (...args: ConstructorParameters) => new VarTsDsl(...args), }; +/** Creates a general expression node. */ export const $ = Object.assign( (...args: ConstructorParameters) => new ExprTsDsl(...args), base, diff --git a/packages/openapi-ts/src/ts-dsl/method.ts b/packages/openapi-ts/src/ts-dsl/method.ts index a0dc238d0..e18bdb0e7 100644 --- a/packages/openapi-ts/src/ts-dsl/method.ts +++ b/packages/openapi-ts/src/ts-dsl/method.ts @@ -18,15 +18,15 @@ import { } from './mixins/modifiers'; import { OptionalMixin } from './mixins/optional'; import { ParamMixin } from './mixins/param'; -import { createTypeAccessor, type TypeAccessor } from './mixins/type'; +import { createType, type Type } from './mixins/type'; export class MethodTsDsl extends TsDsl { private modifiers = createModifierAccessor(this); private name: string; - private _returns: TypeAccessor = createTypeAccessor(this); + private _returns: Type = createType(this); /** Sets the return type. */ - returns: TypeAccessor['fn'] = this._returns.fn; + returns: Type['fn'] = this._returns.fn; constructor(name: string, fn?: (m: MethodTsDsl) => void) { super(); @@ -43,7 +43,7 @@ export class MethodTsDsl extends TsDsl { this.questionToken, this.$generics(), this.$params(), - this._returns.$render(), + this.$type(this._returns), ts.factory.createBlock(this.$do(), true), ); } diff --git a/packages/openapi-ts/src/ts-dsl/mixins/generics.ts b/packages/openapi-ts/src/ts-dsl/mixins/generics.ts index 34f814b9d..80ef9428e 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/generics.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/generics.ts @@ -1,7 +1,7 @@ import ts from 'typescript'; import { type MaybeTsDsl, TsDsl } from '../base'; -import { TypeParamTsDsl } from '../type'; +import { TypeParamTsDsl } from '../type/param'; export class GenericsMixin extends TsDsl { protected _generics?: Array>; @@ -24,6 +24,7 @@ export class GenericsMixin extends TsDsl { protected $generics(): | ReadonlyArray | undefined { + console.log('hi') return this._generics?.map((g) => { if (typeof g === 'string') { return ts.factory.createTypeParameterDeclaration( diff --git a/packages/openapi-ts/src/ts-dsl/mixins/param.ts b/packages/openapi-ts/src/ts-dsl/mixins/param.ts index 3b05afead..41e7131b9 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/param.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/param.ts @@ -1,29 +1,27 @@ import type ts from 'typescript'; +import type { TypeOfTsDsl } from '../base'; import { type MaybeTsDsl, TsDsl } from '../base'; import { ParamTsDsl } from '../param'; export class ParamMixin extends TsDsl { - private _params?: Array>; + private _params?: Array>>; /** Adds a parameter. */ - param( - name: string | ((p: ParamTsDsl) => void), - fn?: (p: ParamTsDsl) => void, - ): this { - const p = new ParamTsDsl(name, fn); + param(...args: ConstructorParameters): this { + const p = new ParamTsDsl(...args); (this._params ??= []).push(p); return this; } /** Adds multiple parameters. */ - params(...params: ReadonlyArray>): this { + params(...params: ReadonlyArray>>): this { (this._params ??= []).push(...params); return this; } /** Renders the parameters into an array of `ParameterDeclaration`s. */ - protected $params(): ReadonlyArray { + protected $params(): ReadonlyArray> { if (!this._params) return []; return this.$node(this._params); } diff --git a/packages/openapi-ts/src/ts-dsl/mixins/type.ts b/packages/openapi-ts/src/ts-dsl/mixins/type.ts index ff0708d22..a16a9d246 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/type.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/type.ts @@ -1,41 +1,28 @@ import type ts from 'typescript'; -import type { TsDsl } from '../base'; -import type { TypeInput } from '../type'; -import { TypeTsDsl } from '../type'; +import type { MaybeTsDsl, TsDsl } from '../base'; +import { type as TypeDsl } from '../type'; -export interface TypeAccessor { +export type Type = { $render(): ts.TypeNode | undefined; - fn(): ReturnType; - fn(type: TypeInput): Parent; + fn(expr: MaybeTsDsl): This; } -/** Provides `.type()`-like access with internal state management. */ -export function createTypeAccessor( - parent: Parent, -): TypeAccessor { - const $type = parent['$type'].bind(parent); +/** Provides access to `TypeTsDsl` on an arbitrary method. */ +export function createType(_this: This): Type { + let _type: MaybeTsDsl | undefined; - let _type: ReturnType | undefined; - let input: TypeInput | undefined; - - function fn(): ReturnType; - function fn(type: TypeInput): Parent; - function fn(type?: TypeInput): ReturnType | Parent { - if (type === undefined) { - if (!_type) _type = TypeTsDsl(); - return _type; - } - input = type; - return parent; + function _fn(expr: MaybeTsDsl): This { + _type = TypeDsl(expr) + return _this; } function $render(): ts.TypeNode | undefined { - return _type?.$render() ?? $type(input); + return _this['$type'](_type) } return { $render, - fn, + fn: _fn, }; } diff --git a/packages/openapi-ts/src/ts-dsl/new.ts b/packages/openapi-ts/src/ts-dsl/new.ts index 86c0eb885..ce23a42a3 100644 --- a/packages/openapi-ts/src/ts-dsl/new.ts +++ b/packages/openapi-ts/src/ts-dsl/new.ts @@ -22,11 +22,10 @@ export class NewTsDsl extends TsDsl { /** Builds the `NewExpression` node. */ $render(): ts.NewExpression { - const types = this._generics?.map((arg) => this.$type(arg)); return ts.factory.createNewExpression( this.$node(this.classExpr), // @ts-expect-error --- generics are not officially supported on 'new' expressions yet - types, + this.$type(this._generics), this.$args(), ); } diff --git a/packages/openapi-ts/src/ts-dsl/param.ts b/packages/openapi-ts/src/ts-dsl/param.ts index 466270498..2f6555b05 100644 --- a/packages/openapi-ts/src/ts-dsl/param.ts +++ b/packages/openapi-ts/src/ts-dsl/param.ts @@ -6,15 +6,15 @@ import { mixin } from './mixins/apply'; import { DecoratorMixin } from './mixins/decorator'; import { OptionalMixin } from './mixins/optional'; import { PatternMixin } from './mixins/pattern'; -import { createTypeAccessor, type TypeAccessor } from './mixins/type'; +import { createType, type Type } from './mixins/type'; import { ValueMixin } from './mixins/value'; export class ParamTsDsl extends TsDsl { private name?: string; - private _type: TypeAccessor = createTypeAccessor(this); + private _type: Type = createType(this); /** Sets the parameter's type. */ - type: TypeAccessor['fn'] = this._type.fn; + type: Type['fn'] = this._type.fn; constructor( name: string | ((p: ParamTsDsl) => void), @@ -40,7 +40,7 @@ export class ParamTsDsl extends TsDsl { undefined, name, this.questionToken, - this._type.$render(), + this.$type(this._type), this.$value(), ); } diff --git a/packages/openapi-ts/src/ts-dsl/type.ts b/packages/openapi-ts/src/ts-dsl/type.ts index 865a3ea0d..be29eda9f 100644 --- a/packages/openapi-ts/src/ts-dsl/type.ts +++ b/packages/openapi-ts/src/ts-dsl/type.ts @@ -1,175 +1,24 @@ -/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */ -import ts from 'typescript'; +import { TypeExprTsDsl } from './type/expr'; +// import { TypeLiteralTsDsl } from './type/literal'; +// import { TypeObjectTsDsl } from './type/object'; +// import { TypeParamTsDsl } from './type/param'; -import type { MaybeTsDsl, WithString } from './base'; -import { TsDsl } from './base'; -import { LiteralTsDsl } from './literal'; -import { mixin } from './mixins/apply'; -import { GenericsMixin } from './mixins/generics'; -import { OptionalMixin } from './mixins/optional'; +const base = { + expr: (...args: ConstructorParameters) => + new TypeExprTsDsl(...args), -export type TypeInput = WithString>; + // literal: (...args: ConstructorParameters) => + // new TypeLiteralTsDsl(...args), -export abstract class BaseTypeTsDsl< - T extends ts.TypeNode | ts.TypeParameterDeclaration, -> extends TsDsl { - protected base?: ts.EntityName; - protected constraint?: TypeInput; - protected defaultValue?: TypeInput; + // object: (...args: ConstructorParameters) => + // new TypeObjectTsDsl(...args), - constructor(base?: WithString) { - super(); - if (base) this.base = this.$expr(base); - } + // param: (...args: ConstructorParameters) => + // new TypeParamTsDsl(...args), +}; - default(value: TypeInput): this { - this.defaultValue = value; - return this; - } - - extends(constraint: TypeInput): this { - this.constraint = constraint; - return this; - } -} - -export class TypeLiteralTsDsl extends TsDsl { - private literal: LiteralTsDsl; - - constructor(value: string | number | boolean) { - super(); - this.literal = new LiteralTsDsl(value); - } - - $render(): ts.LiteralTypeNode { - const expr = this.$node(this.literal); - return ts.factory.createLiteralTypeNode(expr); - } -} - -export class TypeObjectTsDsl extends TsDsl { - private props: Array = []; - - constructor(fn?: (o: TypeObjectTsDsl) => void) { - super(); - fn?.(this); - } - - /** Adds a property signature (returns property builder). */ - prop(name: string, fn: (p: TypePropTsDsl) => void): this { - const propTsDsl = new TypePropTsDsl(name, fn); - this.props.push(propTsDsl); - return this; - } - - $render(): ts.TypeLiteralNode { - return ts.factory.createTypeLiteralNode(this.$node(this.props)); - } -} - -export class TypeParamTsDsl extends BaseTypeTsDsl { - constructor( - name?: WithString, - fn?: (base: TypeParamTsDsl) => void, - ) { - super(name); - fn?.(this); - } - - $render(): ts.TypeParameterDeclaration { - if (!this.base) throw new Error('Missing type name'); - return ts.factory.createTypeParameterDeclaration( - undefined, - this.base as ts.Identifier, - this.$type(this.constraint), - this.$type(this.defaultValue), - ); - } -} - -export interface TypeParamTsDsl extends GenericsMixin {} -mixin(TypeParamTsDsl, GenericsMixin); - -export class TypePropTsDsl extends TsDsl { - private name: string; - private typeInput?: TypeInput; - - constructor(name: string, fn: (p: TypePropTsDsl) => void) { - super(); - this.name = name; - fn(this); - } - - /** Sets the property type. */ - type(type: TypeInput): this { - this.typeInput = type; - return this; - } - - /** Builds and returns the property signature. */ - $render(): ts.TypeElement { - if (!this.typeInput) { - throw new Error(`Type not specified for property '${this.name}'`); - } - const typeNode = - typeof this.typeInput === 'string' - ? ts.factory.createTypeReferenceNode(this.typeInput) - : (this.typeInput as ts.TypeNode); - return ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier(this.name), - this.questionToken, - typeNode, - ); - } -} - -export interface TypePropTsDsl extends OptionalMixin {} -mixin(TypePropTsDsl, OptionalMixin); - -export class TypeReferenceTsDsl extends BaseTypeTsDsl { - private _object?: TypeObjectTsDsl; - - constructor( - name?: WithString, - fn?: (base: TypeReferenceTsDsl) => void, - ) { - super(name); - fn?.(this); - } - - /** Starts an object type literal (e.g. `{ foo: string }`). */ - object(fn?: (o: TypeObjectTsDsl) => void): this { - this._object = new TypeObjectTsDsl(fn); - return this; - } - - $render(): ts.TypeNode { - if (this._object) return this.$node(this._object); - if (!this.base) throw new Error('Missing base type'); - const types = this._generics?.map((arg) => this.$type(arg)); - return ts.factory.createTypeReferenceNode( - this.base, - // @ts-expect-error --- generics are not officially supported on type references yet - types, - ); - } -} - -export interface TypeReferenceTsDsl extends GenericsMixin {} -mixin(TypeReferenceTsDsl, GenericsMixin); - -export const TypeTsDsl = Object.assign( - (...args: ConstructorParameters) => - new TypeReferenceTsDsl(...args), - { - literal: (...args: ConstructorParameters) => - new TypeLiteralTsDsl(...args), - object: (...args: ConstructorParameters) => - new TypeObjectTsDsl(...args), - param: (...args: ConstructorParameters) => - new TypeParamTsDsl(...args), - ref: (...args: ConstructorParameters) => - new TypeReferenceTsDsl(...args), - }, +/** Creates a general expression node. */ +export const type = Object.assign( + (...args: ConstructorParameters) => new TypeExprTsDsl(...args), + base, ); diff --git a/packages/openapi-ts/src/ts-dsl/type/attr.ts b/packages/openapi-ts/src/ts-dsl/type/attr.ts new file mode 100644 index 000000000..c2aafda78 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/attr.ts @@ -0,0 +1,19 @@ +import ts from "typescript"; + +import type { MaybeTsDsl, WithString } from "../base"; +import { TsDsl } from "../base"; + +export class TypeAttrTsDsl extends TsDsl { + private left: MaybeTsDsl; + private right: WithString; + + constructor(left: MaybeTsDsl, right: WithString) { + super(); + this.left = this.$expr(left); + this.right = this.$expr(right); + } + + $render(): ts.EntityName { + return ts.factory.createQualifiedName(this.$node(this.left), this.right); + } +} diff --git a/packages/openapi-ts/src/ts-dsl/type/base.ts b/packages/openapi-ts/src/ts-dsl/type/base.ts new file mode 100644 index 000000000..af442f22c --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/base.ts @@ -0,0 +1,39 @@ +import type ts from "typescript"; + +import type { MaybeTsDsl, WithString } from "../base"; +import { TsDsl } from "../base"; +import type { TypeAttrTsDsl } from "./attr"; + +export type TypeInput = string | boolean | MaybeTsDsl; + +export abstract class BaseTypeTsDsl< + T extends ts.TypeNode | ts.TypeParameterDeclaration = ts.TypeNode, +> extends TsDsl { + protected base?: WithString | TypeAttrTsDsl; + protected constraint?: TypeInput; + protected defaultValue?: TypeInput; + + constructor(base?: WithString) { + super(); + this.base = this.$expr(base); + } + + /** Access a type-level property (qualified name). */ + // attr(name: WithString): this { + // if (!this.base) throw new Error('Cannot access property on undefined base'); + // // this.base = new TypeAttrTsDsl(this.base, name); + // return this; + // } + + /** Sets a default type for this type parameter (e.g., `T = Foo`). */ + default(value: TypeInput): this { + this.defaultValue = value; + return this; + } + + /** Sets a constraint type for this type parameter (e.g., `T extends Foo`). */ + extends(constraint: TypeInput): this { + this.constraint = constraint; + return this; + } +} diff --git a/packages/openapi-ts/src/ts-dsl/type/expr.ts b/packages/openapi-ts/src/ts-dsl/type/expr.ts new file mode 100644 index 000000000..925a70e95 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/expr.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */ +import type ts from "typescript"; + +import type { MaybeTsDsl } from "../base"; +import { TsDsl } from "../base"; +import { mixin } from "../mixins/apply"; +import { GenericsMixin } from "../mixins/generics"; + +export class TypeExprTsDsl extends TsDsl { + private _exprInput: MaybeTsDsl; + // private _object?: TypeObjectTsDsl; + + constructor( + // name?: WithString, + expr: MaybeTsDsl, + // fn?: (base: TypeExprTsDsl) => void, + ) { + super(); + this._exprInput = expr; + } + + /** Starts an object type literal (e.g. `{ foo: string }`). */ + // object(fn?: (o: TypeObjectTsDsl) => void): this { + // this._object = new TypeObjectTsDsl(fn); + // return this; + // } + + $render(): ts.TypeNode { + console.log('hi') + // if (this._object) return this.$node(this._object); + // if (!this.base) throw new Error('Missing base type'); + return this.$type(this._exprInput); + + // return ts.factory.createTypeReferenceNode( + // this.base, + // // @ts-expect-error --- generics are not officially supported on type references yet + // this.$type(this._generics), + // ); + } +} + +export interface TypeExprTsDsl extends GenericsMixin {} +mixin(TypeExprTsDsl, GenericsMixin); diff --git a/packages/openapi-ts/src/ts-dsl/type/literal.ts b/packages/openapi-ts/src/ts-dsl/type/literal.ts new file mode 100644 index 000000000..673deac90 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/literal.ts @@ -0,0 +1,18 @@ +import ts from "typescript"; + +import { TsDsl } from "../base"; +import { LiteralTsDsl } from "../literal"; + +export class TypeLiteralTsDsl extends TsDsl { + private value: string | number | boolean; + + constructor(value: string | number | boolean) { + super(); + this.value = value; + } + + $render(): ts.LiteralTypeNode { + const literal = new LiteralTsDsl(this.value); + return ts.factory.createLiteralTypeNode(this.$node(literal)); + } +} diff --git a/packages/openapi-ts/src/ts-dsl/type/object.ts b/packages/openapi-ts/src/ts-dsl/type/object.ts new file mode 100644 index 000000000..3512f236f --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/object.ts @@ -0,0 +1,19 @@ +import ts from "typescript"; + +import { TsDsl } from "../base"; +import { TypePropTsDsl } from "./prop"; + +export class TypeObjectTsDsl extends TsDsl { + private props: Array = []; + + /** Adds a property signature. */ + prop(name: string, fn: (p: TypePropTsDsl) => void): this { + const propTsDsl = new TypePropTsDsl(name, fn); + this.props.push(propTsDsl); + return this; + } + + $render(): ts.TypeLiteralNode { + return ts.factory.createTypeLiteralNode(this.$node(this.props)); + } +} diff --git a/packages/openapi-ts/src/ts-dsl/type/param.ts b/packages/openapi-ts/src/ts-dsl/type/param.ts new file mode 100644 index 000000000..7c7e37bdc --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/param.ts @@ -0,0 +1,24 @@ +import ts from "typescript"; + +import { TsDsl, type WithString } from "../base"; + +export class TypeParamTsDsl extends TsDsl { + constructor( + _name?: WithString, + fn?: (base: TypeParamTsDsl) => void, + ) { + super(); + fn?.(this); + } + + $render(): ts.TypeParameterDeclaration { + // if (!this.base) throw new Error('Missing type name'); + return ts.factory.createTypeParameterDeclaration( + undefined, + 'todo', + // this.base as ts.Identifier, + // this.$type(this.constraint), + // this.$type(this.defaultValue), + ); + } +} diff --git a/packages/openapi-ts/src/ts-dsl/type/prop.ts b/packages/openapi-ts/src/ts-dsl/type/prop.ts new file mode 100644 index 000000000..c3133f2fe --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/type/prop.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */ +import ts from "typescript"; + +import { TsDsl } from "../base"; +import { mixin } from "../mixins/apply"; +import { OptionalMixin } from "../mixins/optional"; +import type { TypeInput } from "./base"; + +export class TypePropTsDsl extends TsDsl { + private name: string; + private typeInput?: TypeInput; + + constructor(name: string, fn: (p: TypePropTsDsl) => void) { + super(); + this.name = name; + fn(this); + } + + /** Sets the property type. */ + type(type: TypeInput): this { + this.typeInput = type; + return this; + } + + /** Builds and returns the property signature. */ + $render(): ts.TypeElement { + if (!this.typeInput) { + throw new Error(`Type not specified for property '${this.name}'`); + } + const typeNode = + typeof this.typeInput === 'string' + ? ts.factory.createTypeReferenceNode(this.typeInput) + : (this.typeInput as ts.TypeNode); + return ts.factory.createPropertySignature( + undefined, + ts.factory.createIdentifier(this.name), + this.questionToken, + typeNode, + ); + } +} + +export interface TypePropTsDsl extends OptionalMixin {} +mixin(TypePropTsDsl, OptionalMixin); diff --git a/packages/openapi-ts/src/ts-dsl/var.ts b/packages/openapi-ts/src/ts-dsl/var.ts index 97db066e0..69474b408 100644 --- a/packages/openapi-ts/src/ts-dsl/var.ts +++ b/packages/openapi-ts/src/ts-dsl/var.ts @@ -10,12 +10,18 @@ import { ExportMixin, } from './mixins/modifiers'; import { PatternMixin } from './mixins/pattern'; +import type { Type } from './mixins/type'; +import { createType } from './mixins/type'; import { ValueMixin } from './mixins/value'; export class VarTsDsl extends TsDsl { private kind: ts.NodeFlags = ts.NodeFlags.None; private modifiers = createModifierAccessor(this); private name?: string; + private _type: Type = createType(this); + + /** Sets the variable's type. */ + type: Type['fn'] = this._type.fn; constructor(name?: string) { super(); @@ -48,7 +54,7 @@ export class VarTsDsl extends TsDsl { ts.factory.createVariableDeclaration( name, undefined, - undefined, + this.$type(this._type), this.$value(), ), ],