From c041e962604765e0f342fda970a2550367aa2142 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 8 Nov 2025 03:33:50 +0800 Subject: [PATCH] fix(@pinia/colada): correctly access instantiated SDKs --- .changeset/lemon-plants-shop.md | 5 + dev/openapi-ts.config.ts | 4 +- packages/openapi-ts/src/generate/renderer.ts | 6 +- .../src/plugins/@hey-api/sdk/shared/class.ts | 13 +- .../src/plugins/@pinia/colada/v0/plugin.ts | 57 +++--- .../openapi-ts/src/plugins/swr/v2/plugin.ts | 57 +++--- .../openapi-ts/src/plugins/swr/v2/useSwr.ts | 104 +++-------- packages/openapi-ts/src/ts-dsl/await.ts | 18 ++ packages/openapi-ts/src/ts-dsl/base.ts | 71 ++++++-- packages/openapi-ts/src/ts-dsl/call.ts | 6 + packages/openapi-ts/src/ts-dsl/const.ts | 52 ------ packages/openapi-ts/src/ts-dsl/describe.ts | 2 +- packages/openapi-ts/src/ts-dsl/field.ts | 2 +- packages/openapi-ts/src/ts-dsl/func.ts | 162 ++++++++++++++++++ packages/openapi-ts/src/ts-dsl/getter.ts | 2 +- packages/openapi-ts/src/ts-dsl/index.ts | 27 ++- packages/openapi-ts/src/ts-dsl/init.ts | 2 +- packages/openapi-ts/src/ts-dsl/method.ts | 2 +- packages/openapi-ts/src/ts-dsl/object.ts | 13 +- packages/openapi-ts/src/ts-dsl/param.ts | 2 +- packages/openapi-ts/src/ts-dsl/setter.ts | 2 +- packages/openapi-ts/src/ts-dsl/type.ts | 4 +- packages/openapi-ts/src/ts-dsl/var.ts | 154 +++++++++++++++++ 23 files changed, 550 insertions(+), 217 deletions(-) create mode 100644 .changeset/lemon-plants-shop.md create mode 100644 packages/openapi-ts/src/ts-dsl/await.ts delete mode 100644 packages/openapi-ts/src/ts-dsl/const.ts create mode 100644 packages/openapi-ts/src/ts-dsl/func.ts create mode 100644 packages/openapi-ts/src/ts-dsl/var.ts diff --git a/.changeset/lemon-plants-shop.md b/.changeset/lemon-plants-shop.md new file mode 100644 index 0000000000..8097e850f5 --- /dev/null +++ b/.changeset/lemon-plants-shop.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +@pinia/colada: correctly access instantiated SDKs diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts index ea1c40dbb1..f32526c302 100644 --- a/dev/openapi-ts.config.ts +++ b/dev/openapi-ts.config.ts @@ -42,8 +42,8 @@ export default defineConfig(() => { // 'dutchie.json', // 'invalid', // 'full.yaml', - 'openai.yaml', - // 'opencode.yaml', + // 'openai.yaml', + 'opencode.yaml', // 'sdk-instance.yaml', // 'string-with-format.yaml', // 'transformers.json', diff --git a/packages/openapi-ts/src/generate/renderer.ts b/packages/openapi-ts/src/generate/renderer.ts index aa976eb5ef..08cbfdea3b 100644 --- a/packages/openapi-ts/src/generate/renderer.ts +++ b/packages/openapi-ts/src/generate/renderer.ts @@ -13,6 +13,7 @@ import { createBinding, mergeBindings, renderIds } from '@hey-api/codegen-core'; import ts from 'typescript'; import { ensureValidIdentifier } from '~/openApi/shared/utils/identifier'; +import { TsDsl } from '~/ts-dsl'; import { tsc } from '~/tsc'; import { tsNodeToString } from '~/tsc/utils'; @@ -186,7 +187,10 @@ export class TypeScriptRenderer implements Renderer { for (const symbolId of file.symbols.body) { const value = project.symbols.getValue(symbolId); - if (typeof value === 'string') { + if (value instanceof TsDsl) { + const node = value.$render(); + lines.push(tsNodeToString({ node, unescape: true })); + } else if (typeof value === 'string') { lines.push(value); } else if (value instanceof Array) { for (const node of value) { 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 50d8c2062c..be40236118 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 @@ -387,7 +387,7 @@ export const generateClassSdk = ({ plugin.config.instance ? $.new(refChildClass.placeholder).args( $.object((o) => - o.prop('client', () => $('this').attr('client')), + o.prop('client', $('this').attr('client')), ), ) : $(refChildClass.placeholder), @@ -402,7 +402,7 @@ export const generateClassSdk = ({ ? $.return( $.new(refChildClass.placeholder).args( $.object((o) => - o.prop('client', () => $('this').attr('client')), + o.prop('client', $('this').attr('client')), ), ), ) @@ -426,7 +426,7 @@ export const generateClassSdk = ({ const node = createClientClass({ plugin, symbol: symbolHeyApiClient, - }).$render(); + }); plugin.setSymbolValue(symbolHeyApiClient, node); } @@ -486,7 +486,7 @@ export const generateClassSdk = ({ plugin, sdkName: symbol.placeholder, symbol: symbolHeyApiRegistry, - }).$render(); + }); plugin.setSymbolValue(symbolHeyApiRegistry, node); const registryNode = $.field(registryName, (f) => f @@ -509,11 +509,10 @@ export const generateClassSdk = ({ category: 'external', resource: '@angular/core.Injectable', }).placeholder, - (o) => o.prop('providedIn', () => $.literal('root')), + (o) => o.prop('providedIn', $.literal('root')), ), ) - .do(...currentClass.nodes) - .$render(); + .do(...currentClass.nodes); plugin.setSymbolValue(symbol, node); }; diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/v0/plugin.ts b/packages/openapi-ts/src/plugins/@pinia/colada/v0/plugin.ts index 6f115d6198..57ddd4317a 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/v0/plugin.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/v0/plugin.ts @@ -1,3 +1,4 @@ +import { registryName } from '~/plugins/@hey-api/sdk/shared/class'; import { operationClasses } from '~/plugins/@hey-api/sdk/shared/operation'; import { stringCase } from '~/utils/stringCase'; @@ -64,32 +65,36 @@ export const handlerV0: PiniaColadaPlugin['Handler'] = ({ plugin }) => { }) : undefined; const entry = classes ? classes.values().next().value : undefined; - const queryFn = - // TODO: this should use class graph to determine correct path string - // as it's really easy to break once we change the class casing - entry - ? [ - plugin.referenceSymbol({ - category: 'utility', - resource: 'class', - resourceId: entry.path[0], - tool: 'sdk', - }).placeholder, - ...entry.path.slice(1).map((className: string) => - stringCase({ - case: 'camelCase', - value: className, - }), - ), - entry.methodName, - ] - .filter(Boolean) - .join('.') - : plugin.referenceSymbol({ - category: 'sdk', - resource: 'operation', - resourceId: operation.id, - }).placeholder; + // TODO: this should use class graph to determine correct path string + // as it's really easy to break once we change the class casing + let queryFn: string; + if (entry) { + const symbolClass = plugin.referenceSymbol({ + category: 'utility', + resource: 'class', + resourceId: entry.path[0], + tool: 'sdk', + }); + queryFn = [ + symbolClass.placeholder, + ...(sdkPlugin.config.instance ? [registryName, 'get()'] : []), + ...entry.path.slice(1).map((className) => + stringCase({ + case: 'camelCase', + value: className, + }), + ), + entry.methodName, + ] + .filter(Boolean) + .join('.'); + } else { + queryFn = plugin.referenceSymbol({ + category: 'sdk', + resource: 'operation', + resourceId: operation.id, + }).placeholder; + } if (plugin.hooks.operation.isQuery(operation)) { if (plugin.config.queryOptions.enabled) { diff --git a/packages/openapi-ts/src/plugins/swr/v2/plugin.ts b/packages/openapi-ts/src/plugins/swr/v2/plugin.ts index 7dbbbccded..730e2ebfb0 100644 --- a/packages/openapi-ts/src/plugins/swr/v2/plugin.ts +++ b/packages/openapi-ts/src/plugins/swr/v2/plugin.ts @@ -1,3 +1,4 @@ +import { registryName } from '~/plugins/@hey-api/sdk/shared/class'; import { operationClasses } from '~/plugins/@hey-api/sdk/shared/operation'; import { stringCase } from '~/utils/stringCase'; @@ -29,32 +30,36 @@ export const handlerV2: SwrPlugin['Handler'] = ({ plugin }) => { }) : undefined; const entry = classes ? classes.values().next().value : undefined; - const queryFn = - // TODO: this should use class graph to determine correct path string - // as it's really easy to break once we change the class casing - entry - ? [ - plugin.referenceSymbol({ - category: 'utility', - resource: 'class', - resourceId: entry.path[0], - tool: 'sdk', - }).placeholder, - ...entry.path.slice(1).map((className) => - stringCase({ - case: 'camelCase', - value: className, - }), - ), - entry.methodName, - ] - .filter(Boolean) - .join('.') - : plugin.referenceSymbol({ - category: 'sdk', - resource: 'operation', - resourceId: operation.id, - }).placeholder; + // TODO: this should use class graph to determine correct path string + // as it's really easy to break once we change the class casing + let queryFn: string; + if (entry) { + const symbolClass = plugin.referenceSymbol({ + category: 'utility', + resource: 'class', + resourceId: entry.path[0], + tool: 'sdk', + }); + queryFn = [ + symbolClass.placeholder, + ...(sdkPlugin.config.instance ? [registryName, 'get()'] : []), + ...entry.path.slice(1).map((className) => + stringCase({ + case: 'camelCase', + value: className, + }), + ), + entry.methodName, + ] + .filter(Boolean) + .join('.'); + } else { + queryFn = plugin.referenceSymbol({ + category: 'sdk', + resource: 'operation', + resourceId: operation.id, + }).placeholder; + } if (plugin.hooks.operation.isQuery(operation)) { // if (plugin.config.queryOptions.enabled) { diff --git a/packages/openapi-ts/src/plugins/swr/v2/useSwr.ts b/packages/openapi-ts/src/plugins/swr/v2/useSwr.ts index 79d19b24d4..d01aa74cdc 100644 --- a/packages/openapi-ts/src/plugins/swr/v2/useSwr.ts +++ b/packages/openapi-ts/src/plugins/swr/v2/useSwr.ts @@ -6,7 +6,8 @@ import { createOperationComment, hasOperationSse, } from '~/plugins/shared/utils/operation'; -import { tsc } from '~/tsc'; +import type { TsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import type { SwrPlugin } from '../types'; @@ -35,86 +36,37 @@ export const createUseSwr = ({ }), }); - const awaitSdkExpression = tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: queryFn, - parameters: [ - tsc.objectExpression({ - multiLine: true, - obj: [ - // { - // spread: optionsParamName, - // }, - // { - // spread: 'queryKey[0]', - // }, - // { - // key: 'signal', - // shorthand: true, - // value: tsc.identifier({ - // text: 'signal', - // }), - // }, - { - key: 'throwOnError', - value: true, - }, - ], - }), - ], - }), - }); - const statements: Array = []; + const awaitSdkFn = $(queryFn) + .call($.object((o) => o.prop('throwOnError', $.literal(true)))) + .await(); + + const statements: Array> = []; if (plugin.getPluginOrThrow('@hey-api/sdk').config.responseStyle === 'data') { - statements.push( - tsc.returnVariable({ - expression: awaitSdkExpression, - }), - ); + statements.push($.return(awaitSdkFn)); } else { statements.push( - tsc.constVariable({ - destructure: true, - expression: awaitSdkExpression, - name: 'data', - }), - tsc.returnVariable({ - expression: 'data', - }), + $.const().object('data').assign(awaitSdkFn), + $.return('data'), ); } - const statement = tsc.constVariable({ - comment: plugin.config.comments - ? createOperationComment({ operation }) - : undefined, - exportConst: symbolUseQueryFn.exported, - expression: tsc.arrowFunction({ - parameters: [ - // { - // isRequired: isRequiredOptions, - // name: optionsParamName, - // type: typeData, - // }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.callExpression({ - functionName: symbolUseSwr.placeholder, - parameters: [ - tsc.stringLiteral({ - text: operation.path, - }), - tsc.arrowFunction({ - async: true, - statements, - }), - ], - }), - }), - ], - }), - name: symbolUseQueryFn.placeholder, - }); + const statement = $.const(symbolUseQueryFn.placeholder) + .export(symbolUseQueryFn.exported) + .$if( + plugin.config.comments && createOperationComment({ operation }), + (c, v) => c.describe(v as Array), + ) + .assign( + $.func().do( + $.return( + $(symbolUseSwr.placeholder).call( + $.literal(operation.path), + $.func() + .async() + .do(...statements), + ), + ), + ), + ); plugin.setSymbolValue(symbolUseQueryFn, statement); }; diff --git a/packages/openapi-ts/src/ts-dsl/await.ts b/packages/openapi-ts/src/ts-dsl/await.ts new file mode 100644 index 0000000000..3a3a77865d --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/await.ts @@ -0,0 +1,18 @@ +import ts from 'typescript'; + +import type { ExprInput, MaybeTsDsl } from './base'; +import { TsDsl } from './base'; + +export class AwaitTsDsl extends TsDsl { + private exprNode: MaybeTsDsl; + + constructor(expr: MaybeTsDsl) { + super(); + this.exprNode = expr; + } + + $render(): ts.AwaitExpression { + const expr = this.$node(this.exprNode); + return ts.factory.createAwaitExpression(expr); + } +} diff --git a/packages/openapi-ts/src/ts-dsl/base.ts b/packages/openapi-ts/src/ts-dsl/base.ts index cfe2e3af58..b4f85f20ec 100644 --- a/packages/openapi-ts/src/ts-dsl/base.ts +++ b/packages/openapi-ts/src/ts-dsl/base.ts @@ -23,20 +23,71 @@ export abstract class TsDsl implements ITsDsl { value: V, ifTrue: (self: T, v: Exclude) => R | void, ifFalse?: (self: T, v: Extract) => R | void, + ): R | T; + $if( + this: T, + value: V, + ifTrue: (v: Exclude) => R | void, + ifFalse?: (v: Extract) => R | void, + ): R | T; + $if( + this: T, + value: V, + ifTrue: () => R | void, + ifFalse?: () => R | void, + ): R | T; + $if( + this: T, + value: V, + ifTrue: any, + ifFalse?: any, ): R | T { if (value) { - const result = ifTrue( - this, - value as Exclude, - ); - return result ?? this; + // Try calling with (self, value), then (value), then () + let result: R | void | undefined; + try { + result = ifTrue?.(this, value as Exclude); + } catch { + // ignore and try other signatures + } + if (result === undefined) { + try { + result = ifTrue?.(value as Exclude); + } catch { + // ignore and try zero-arg + } + } + if (result === undefined) { + try { + result = ifTrue?.(); + } catch { + // swallow + } + } + return (result ?? this) as R | T; } if (ifFalse) { - const result = ifFalse( - this, - value as Extract, - ); - return result ?? this; + let result: R | void | undefined; + try { + result = ifFalse?.(this, value as Extract); + } catch { + // ignore + } + if (result === undefined) { + try { + result = ifFalse?.(value as Extract); + } catch { + // ignore + } + } + if (result === undefined) { + try { + result = ifFalse?.(); + } catch { + // ignore + } + } + return (result ?? this) as R | T; } return this; } diff --git a/packages/openapi-ts/src/ts-dsl/call.ts b/packages/openapi-ts/src/ts-dsl/call.ts index b1732fbfed..40dc2c3595 100644 --- a/packages/openapi-ts/src/ts-dsl/call.ts +++ b/packages/openapi-ts/src/ts-dsl/call.ts @@ -1,5 +1,6 @@ import ts from 'typescript'; +import { AwaitTsDsl } from './await'; import type { ExprInput, MaybeTsDsl } from './base'; import { TsDsl } from './base'; @@ -22,6 +23,11 @@ export class CallTsDsl extends TsDsl { return this; } + /** Await the result of the call expression. */ + await(): AwaitTsDsl { + return new AwaitTsDsl(this); + } + $render(): ts.CallExpression { const calleeNode = this.$node(this.callee); const argsNodes = this.$node(this.callArgs).map((arg) => this.$expr(arg)); diff --git a/packages/openapi-ts/src/ts-dsl/const.ts b/packages/openapi-ts/src/ts-dsl/const.ts deleted file mode 100644 index ce55f61209..0000000000 --- a/packages/openapi-ts/src/ts-dsl/const.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -import ts from 'typescript'; - -import { TsDsl } from './base'; -import { mixin } from './mixins/apply'; -import { DescribeMixin } from './mixins/describe'; -import { - createModifierAccessor, - DefaultMixin, - ExportMixin, -} from './mixins/modifiers'; -import { ValueMixin } from './mixins/value'; - -export class ConstTsDsl extends TsDsl { - private modifiers = createModifierAccessor(this); - private name: string; - - constructor(name: string) { - super(); - this.name = name; - } - - $render(): ts.VariableStatement { - return ts.factory.createVariableStatement( - this.modifiers.list(), - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(this.name), - undefined, - undefined, - this.$node(this.initializer), - ), - ], - ts.NodeFlags.Const, - ), - ); - } -} - -export interface ConstTsDsl - extends DefaultMixin, - DescribeMixin, - ExportMixin, - ValueMixin {} -mixin( - ConstTsDsl, - DefaultMixin, - [DescribeMixin, { overrideRender: true }], - ExportMixin, - ValueMixin, -); diff --git a/packages/openapi-ts/src/ts-dsl/describe.ts b/packages/openapi-ts/src/ts-dsl/describe.ts index ad45940b6f..41d166629f 100644 --- a/packages/openapi-ts/src/ts-dsl/describe.ts +++ b/packages/openapi-ts/src/ts-dsl/describe.ts @@ -17,7 +17,7 @@ export class DescribeTsDsl extends TsDsl { this.add(...lines); } } - if (fn) fn(this); + fn?.(this); } add(...lines: ReadonlyArray): this { diff --git a/packages/openapi-ts/src/ts-dsl/field.ts b/packages/openapi-ts/src/ts-dsl/field.ts index 6c934e890b..9e97fc1b3b 100644 --- a/packages/openapi-ts/src/ts-dsl/field.ts +++ b/packages/openapi-ts/src/ts-dsl/field.ts @@ -24,7 +24,7 @@ export class FieldTsDsl extends TsDsl { constructor(name: string, fn?: (f: FieldTsDsl) => void) { super(); this.name = name; - if (fn) fn(this); + fn?.(this); } /** Sets the property's type. */ diff --git a/packages/openapi-ts/src/ts-dsl/func.ts b/packages/openapi-ts/src/ts-dsl/func.ts new file mode 100644 index 0000000000..7d3de211e2 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/func.ts @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ +import ts from 'typescript'; + +import type { MaybeTsDsl } from './base'; +import { TsDsl } from './base'; +import { mixin } from './mixins/apply'; +import { DecoratorMixin } from './mixins/decorator'; +import { DescribeMixin } from './mixins/describe'; +import { GenericsMixin } from './mixins/generics'; +import { + AbstractMixin, + AsyncMixin, + createModifierAccessor, + PrivateMixin, + ProtectedMixin, + PublicMixin, + StaticMixin, +} from './mixins/modifiers'; +import { OptionalMixin } from './mixins/optional'; +import { ParamMixin } from './mixins/param'; +import { createTypeAccessor } from './mixins/type'; + +type FuncMode = 'arrow' | 'decl' | 'expr'; + +class ImplFuncTsDsl extends TsDsl< + M extends 'decl' + ? ts.FunctionDeclaration + : M extends 'expr' + ? ts.FunctionExpression + : ts.ArrowFunction +> { + private body: Array> = []; + private mode: FuncMode; + private modifiers = createModifierAccessor(this); + private name?: string; + private _returns = createTypeAccessor(this); + + constructor(); + constructor(fn: (f: ImplFuncTsDsl<'arrow'>) => void); + constructor(name: string); + constructor(name: string, fn: (f: ImplFuncTsDsl<'decl'>) => void); + constructor( + nameOrFn?: string | ((f: ImplFuncTsDsl<'arrow'>) => void), + fn?: (f: ImplFuncTsDsl<'decl'>) => void, + ) { + super(); + if (typeof nameOrFn === 'string') { + this.name = nameOrFn; + this.mode = 'decl'; + fn?.(this as unknown as FuncTsDsl<'decl'>); + } else { + this.mode = 'arrow'; + nameOrFn?.(this as unknown as FuncTsDsl<'arrow'>); + } + } + + arrow(): FuncTsDsl<'arrow'> { + this.mode = 'arrow'; + return this as unknown as FuncTsDsl<'arrow'>; + } + + decl(): FuncTsDsl<'decl'> { + this.mode = 'decl'; + return this as unknown as FuncTsDsl<'decl'>; + } + + expr(): FuncTsDsl<'expr'> { + this.mode = 'expr'; + return this as unknown as FuncTsDsl<'expr'>; + } + + do(...items: ReadonlyArray>): this { + this.body.push(...items); + return this; + } + + /** Sets the return type. */ + returns = this._returns.method; + + $render(): M extends 'decl' + ? ts.FunctionDeclaration + : M extends 'expr' + ? ts.FunctionExpression + : ts.ArrowFunction { + const builtParams = this.$node(this._params ?? []); + const builtBody = this.$stmt(this.body); + + if (this.mode === 'decl') { + if (!this.name) throw new Error('Function declaration requires a name'); + return ts.factory.createFunctionDeclaration( + [...(this.decorators ?? []), ...this.modifiers.list()], + undefined, + this.name, + this.$generics(), + builtParams, + this._returns.$render(), + ts.factory.createBlock(builtBody, true), + ) as any; + } + + if (this.mode === 'expr') { + return ts.factory.createFunctionExpression( + [...this.modifiers.list()], + undefined, + this.name ? ts.factory.createIdentifier(this.name) : undefined, + this.$generics(), + builtParams, + this._returns.$render(), + ts.factory.createBlock(builtBody, true), + ) as any; + } + + const exprBody = + builtBody.length === 1 && ts.isReturnStatement(builtBody[0]!) + ? (builtBody[0].expression ?? ts.factory.createBlock(builtBody, true)) + : ts.factory.createBlock(builtBody, true); + + return ts.factory.createArrowFunction( + [...this.modifiers.list()], + this.$generics(), + builtParams, + this._returns.$render(), + undefined, + exprBody, + ) as any; + } +} + +interface ImplFuncTsDsl + extends AbstractMixin, + AsyncMixin, + DecoratorMixin, + DescribeMixin, + GenericsMixin, + OptionalMixin, + ParamMixin, + PrivateMixin, + ProtectedMixin, + PublicMixin, + StaticMixin {} +mixin( + ImplFuncTsDsl, + AbstractMixin, + AsyncMixin, + DecoratorMixin, + [DescribeMixin, { overrideRender: true }], + GenericsMixin, + OptionalMixin, + ParamMixin, + PrivateMixin, + ProtectedMixin, + PublicMixin, + StaticMixin, +); + +export const FuncTsDsl = ImplFuncTsDsl as { + new (): FuncTsDsl<'arrow'>; + new (fn: (f: FuncTsDsl<'arrow'>) => void): FuncTsDsl<'arrow'>; + new (name: string): FuncTsDsl<'decl'>; + new (name: string, fn: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'decl'>; +} & typeof ImplFuncTsDsl; +export type FuncTsDsl = ImplFuncTsDsl; diff --git a/packages/openapi-ts/src/ts-dsl/getter.ts b/packages/openapi-ts/src/ts-dsl/getter.ts index 3317b8e1eb..f6a343b3d5 100644 --- a/packages/openapi-ts/src/ts-dsl/getter.ts +++ b/packages/openapi-ts/src/ts-dsl/getter.ts @@ -25,7 +25,7 @@ export class GetterTsDsl extends TsDsl { constructor(name: string, fn?: (g: GetterTsDsl) => void) { super(); this.name = name; - if (fn) fn(this); + fn?.(this); } /** Adds one or more expressions to the getter body. */ diff --git a/packages/openapi-ts/src/ts-dsl/index.ts b/packages/openapi-ts/src/ts-dsl/index.ts index 270e3a4b4d..45348bf888 100644 --- a/packages/openapi-ts/src/ts-dsl/index.ts +++ b/packages/openapi-ts/src/ts-dsl/index.ts @@ -1,11 +1,12 @@ import { AttrTsDsl } from './attr'; +import { AwaitTsDsl } from './await'; import { BinaryTsDsl } from './binary'; import { CallTsDsl } from './call'; import { ClassTsDsl } from './class'; -import { ConstTsDsl } from './const'; import { DescribeTsDsl } from './describe'; import { ExprTsDsl } from './expr'; import { FieldTsDsl } from './field'; +import { FuncTsDsl } from './func'; import { GetterTsDsl } from './getter'; import { IfTsDsl } from './if'; import { InitTsDsl } from './init'; @@ -21,29 +22,45 @@ import { SetterTsDsl } from './setter'; import { TemplateTsDsl } from './template'; import { ThrowTsDsl } from './throw'; import { TypeTsDsl } from './type'; +import { VarTsDsl } from './var'; const base = { attr: (...args: ConstructorParameters) => new AttrTsDsl(...args), + await: (...args: ConstructorParameters) => + new AwaitTsDsl(...args), binary: (...args: ConstructorParameters) => new BinaryTsDsl(...args), call: (...args: ConstructorParameters) => new CallTsDsl(...args), class: (...args: ConstructorParameters) => new ClassTsDsl(...args), - const: (...args: ConstructorParameters) => - new ConstTsDsl(...args), + const: (...args: ConstructorParameters) => + new VarTsDsl(...args).const(), describe: (...args: ConstructorParameters) => new DescribeTsDsl(...args), expr: (...args: ConstructorParameters) => new ExprTsDsl(...args), field: (...args: ConstructorParameters) => new FieldTsDsl(...args), + func: ((nameOrFn?: any, fn?: any) => { + if (nameOrFn === undefined) return new FuncTsDsl(); + if (typeof nameOrFn !== 'string') return new FuncTsDsl(nameOrFn); + if (fn === undefined) return new FuncTsDsl(nameOrFn); + return new FuncTsDsl(nameOrFn, fn); + }) as { + (): FuncTsDsl<'arrow'>; + (fn: (f: FuncTsDsl<'arrow'>) => void): FuncTsDsl<'arrow'>; + (name: string): FuncTsDsl<'decl'>; + (name: string, fn: (f: FuncTsDsl<'decl'>) => void): FuncTsDsl<'decl'>; + }, getter: (...args: ConstructorParameters) => new GetterTsDsl(...args), if: (...args: ConstructorParameters) => new IfTsDsl(...args), init: (...args: ConstructorParameters) => new InitTsDsl(...args), + let: (...args: ConstructorParameters) => + new VarTsDsl(...args).let(), literal: (...args: ConstructorParameters) => new LiteralTsDsl(...args), method: (...args: ConstructorParameters) => @@ -67,6 +84,8 @@ const base = { throw: (...args: ConstructorParameters) => new ThrowTsDsl(...args), type: TypeTsDsl, + var: (...args: ConstructorParameters) => + new VarTsDsl(...args), }; export const $ = Object.assign( @@ -74,4 +93,4 @@ export const $ = Object.assign( base, ); -export type { TsDsl } from './base'; +export { TsDsl } from './base'; diff --git a/packages/openapi-ts/src/ts-dsl/init.ts b/packages/openapi-ts/src/ts-dsl/init.ts index ad53bf0f40..1c99d438a5 100644 --- a/packages/openapi-ts/src/ts-dsl/init.ts +++ b/packages/openapi-ts/src/ts-dsl/init.ts @@ -20,7 +20,7 @@ export class InitTsDsl extends TsDsl { constructor(fn?: (i: InitTsDsl) => void) { super(); - if (fn) fn(this); + fn?.(this); } /** Adds one or more statements or expressions to the constructor body. */ diff --git a/packages/openapi-ts/src/ts-dsl/method.ts b/packages/openapi-ts/src/ts-dsl/method.ts index 516e131701..e5ecd60034 100644 --- a/packages/openapi-ts/src/ts-dsl/method.ts +++ b/packages/openapi-ts/src/ts-dsl/method.ts @@ -29,7 +29,7 @@ export class MethodTsDsl extends TsDsl { constructor(name: string, fn?: (m: MethodTsDsl) => void) { super(); this.name = name; - if (fn) fn(this); + fn?.(this); } /** Sets the return type. */ diff --git a/packages/openapi-ts/src/ts-dsl/object.ts b/packages/openapi-ts/src/ts-dsl/object.ts index 7243056053..328ed87cd2 100644 --- a/packages/openapi-ts/src/ts-dsl/object.ts +++ b/packages/openapi-ts/src/ts-dsl/object.ts @@ -10,7 +10,7 @@ export class ObjectTsDsl extends TsDsl { constructor(fn?: (o: ObjectTsDsl) => void) { super(); - if (fn) fn(this); + fn?.(this); } /** Sets automatic line output with optional threshold (default: 3). */ @@ -31,12 +31,17 @@ export class ObjectTsDsl extends TsDsl { return this; } - /** Adds a property assignment using a callback builder. */ + /** Adds a property assignment. */ prop( name: string, - fn: (p: (expr: ExprInput) => ExprTsDsl) => TsDsl, + fn: + | TsDsl + | ((p: (expr: ExprInput) => ExprTsDsl) => TsDsl), ): this { - const result = fn((expr: ExprInput) => new ExprTsDsl(expr)); + const result = + typeof fn === 'function' + ? fn((expr: ExprInput) => new ExprTsDsl(expr)) + : fn; this.props.push({ expr: result, name }); return this; } diff --git a/packages/openapi-ts/src/ts-dsl/param.ts b/packages/openapi-ts/src/ts-dsl/param.ts index a5de4ab29d..7607e8c31c 100644 --- a/packages/openapi-ts/src/ts-dsl/param.ts +++ b/packages/openapi-ts/src/ts-dsl/param.ts @@ -15,7 +15,7 @@ export class ParamTsDsl extends TsDsl { constructor(name: string, fn?: (p: ParamTsDsl) => void) { super(); this.name = name; - if (fn) fn(this); + fn?.(this); } /** Sets the parameter's type. */ diff --git a/packages/openapi-ts/src/ts-dsl/setter.ts b/packages/openapi-ts/src/ts-dsl/setter.ts index 208988a5ac..c153e0dd31 100644 --- a/packages/openapi-ts/src/ts-dsl/setter.ts +++ b/packages/openapi-ts/src/ts-dsl/setter.ts @@ -25,7 +25,7 @@ export class SetterTsDsl extends TsDsl { constructor(name: string, fn?: (s: SetterTsDsl) => void) { super(); this.name = name; - if (fn) fn(this); + fn?.(this); } /** Adds one or more expressions/statements to the setter body. */ diff --git a/packages/openapi-ts/src/ts-dsl/type.ts b/packages/openapi-ts/src/ts-dsl/type.ts index ce4dc49484..e3ae5a2bd7 100644 --- a/packages/openapi-ts/src/ts-dsl/type.ts +++ b/packages/openapi-ts/src/ts-dsl/type.ts @@ -39,7 +39,7 @@ export class TypeReferenceTsDsl extends BaseTypeTsDsl { fn?: (base: TypeReferenceTsDsl) => void, ) { super(name); - if (fn) fn(this); + fn?.(this); } /** Starts an object type literal (e.g. `{ foo: string }`). */ @@ -70,7 +70,7 @@ export class TypeParamTsDsl extends BaseTypeTsDsl { fn?: (base: TypeParamTsDsl) => void, ) { super(name); - if (fn) fn(this); + fn?.(this); } $render(): ts.TypeParameterDeclaration { diff --git a/packages/openapi-ts/src/ts-dsl/var.ts b/packages/openapi-ts/src/ts-dsl/var.ts new file mode 100644 index 0000000000..9941fdfd51 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/var.ts @@ -0,0 +1,154 @@ +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ +import ts from 'typescript'; + +import { TsDsl } from './base'; +import { mixin } from './mixins/apply'; +import { DescribeMixin } from './mixins/describe'; +import { + createModifierAccessor, + DefaultMixin, + ExportMixin, +} from './mixins/modifiers'; +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 pattern?: ReadonlyArray | Record; + private _rest?: string; + + constructor(name?: string) { + super(); + this.name = name; + } + + const(): this { + this.kind = ts.NodeFlags.Const; + return this; + } + + let(): this { + this.kind = ts.NodeFlags.Let; + return this; + } + + object( + ...props: ReadonlyArray< + string | ReadonlyArray | Record + > + ): this { + const entries: Record = {}; + for (const p of props) { + if (typeof p === 'string') { + entries[p] = p; // shorthand + } else if (p instanceof Array) { + for (const name of p) entries[name] = name; + } else { + Object.assign(entries, p); + } + } + this.pattern = entries; + return this; + } + + rest(name: string): this { + this._rest = name; + return this; + } + + tuple(...props: ReadonlyArray | [ReadonlyArray]): this { + const names = + props[0] instanceof Array + ? [...props[0]] + : (props as ReadonlyArray); + this.pattern = names; + return this; + } + + var(): this { + this.kind = ts.NodeFlags.None; + return this; + } + + $render(): ts.VariableStatement { + let _pattern: ts.BindingPattern | undefined; + + if (this.pattern) { + if (this.pattern instanceof Array) { + const elements = this.pattern.map((p) => + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier(p), + ), + ); + + const restEl = this.createRest(); + if (restEl) elements.push(restEl); + + _pattern = ts.factory.createArrayBindingPattern(elements); + } else { + const elements = Object.entries(this.pattern).map(([key, alias]) => + key === alias + ? ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier(key), + ) + : ts.factory.createBindingElement( + undefined, + ts.factory.createIdentifier(key), + ts.factory.createIdentifier(alias), + ), + ); + + const restEl = this.createRest(); + if (restEl) elements.push(restEl); + + _pattern = ts.factory.createObjectBindingPattern(elements); + } + } + const name = _pattern ?? this.name; + if (!name) { + throw new Error('Var must have either a name or a destructuring pattern'); + } + return ts.factory.createVariableStatement( + this.modifiers.list(), + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + name, + undefined, + undefined, + this.$node(this.initializer), + ), + ], + this.kind, + ), + ); + } + + private createRest(): ts.BindingElement | undefined { + return this._rest + ? ts.factory.createBindingElement( + ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), + undefined, + ts.factory.createIdentifier(this._rest), + ) + : undefined; + } +} + +export interface VarTsDsl + extends DefaultMixin, + DescribeMixin, + ExportMixin, + ValueMixin {} +mixin( + VarTsDsl, + DefaultMixin, + [DescribeMixin, { overrideRender: true }], + ExportMixin, + ValueMixin, +);