diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts index b0f0e1bf6..1a0598540 100644 --- a/dev/openapi-ts.config.ts +++ b/dev/openapi-ts.config.ts @@ -357,9 +357,9 @@ export default defineConfig(() => { }, { name: 'arktype', - // types: { - // infer: true, - // }, + types: { + infer: true, + }, }, { // case: 'SCREAMING_SNAKE_CASE', diff --git a/packages/openapi-ts/src/plugins/arktype/constants.ts b/packages/openapi-ts/src/plugins/arktype/constants.ts index 4663f2f05..29d1908bb 100644 --- a/packages/openapi-ts/src/plugins/arktype/constants.ts +++ b/packages/openapi-ts/src/plugins/arktype/constants.ts @@ -1,66 +1,64 @@ -import { tsc } from '~/tsc'; - export const identifiers = { keywords: { - false: tsc.identifier({ text: 'false' }), - true: tsc.identifier({ text: 'true' }), + false: 'false', + true: 'true', }, /** * {@link https://arktype.io/docs/primitives#number Number} */ number: { - Infinity: tsc.identifier({ text: 'Infinity' }), - NaN: tsc.identifier({ text: 'NaN' }), - NegativeInfinity: tsc.identifier({ text: 'NegativeInfinity' }), - epoch: tsc.identifier({ text: 'epoch' }), - integer: tsc.identifier({ text: 'integer' }), - safe: tsc.identifier({ text: 'safe' }), + Infinity: 'Infinity', + NaN: 'NaN', + NegativeInfinity: 'NegativeInfinity', + epoch: 'epoch', + integer: 'integer', + safe: 'safe', }, /** * {@link https://arktype.io/docs/primitives Primitives} */ primitives: { - bigint: tsc.identifier({ text: 'bigint' }), - boolean: tsc.identifier({ text: 'boolean' }), - keywords: tsc.identifier({ text: 'keywords' }), + bigint: 'bigint', + boolean: 'boolean', + keywords: 'keywords', null: 'null', - number: tsc.identifier({ text: 'number' }), + number: 'number', string: 'string', - symbol: tsc.identifier({ text: 'symbol' }), - undefined: tsc.identifier({ text: 'undefined' }), - unit: tsc.identifier({ text: 'unit' }), + symbol: 'symbol', + undefined: 'undefined', + unit: 'unit', }, /** * {@link https://arktype.io/docs/primitives#string String} */ string: { - NFC: tsc.identifier({ text: 'NFC' }), - NFD: tsc.identifier({ text: 'NFD' }), - NFKC: tsc.identifier({ text: 'NFKC' }), - NFKD: tsc.identifier({ text: 'NFKD' }), - alpha: tsc.identifier({ text: 'alpha' }), - alphanumeric: tsc.identifier({ text: 'alphanumeric' }), - base64: tsc.identifier({ text: 'base64' }), - capitalize: tsc.identifier({ text: 'capitalize' }), - creditCard: tsc.identifier({ text: 'creditCard' }), + NFC: 'NFC', + NFD: 'NFD', + NFKC: 'NFKC', + NFKD: 'NFKD', + alpha: 'alpha', + alphanumeric: 'alphanumeric', + base64: 'base64', + capitalize: 'capitalize', + creditCard: 'creditCard', date: 'date', - digits: tsc.identifier({ text: 'digits' }), + digits: 'digits', email: 'email', - epoch: tsc.identifier({ text: 'epoch' }), - hex: tsc.identifier({ text: 'hex' }), - integer: tsc.identifier({ text: 'integer' }), + epoch: 'epoch', + hex: 'hex', + integer: 'integer', ip: 'ip', iso: 'iso', - json: tsc.identifier({ text: 'json' }), - lower: tsc.identifier({ text: 'lower' }), - normalize: tsc.identifier({ text: 'normalize' }), - numeric: tsc.identifier({ text: 'numeric' }), - parse: tsc.identifier({ text: 'parse' }), - preformatted: tsc.identifier({ text: 'preformatted' }), - regex: tsc.identifier({ text: 'regex' }), - semver: tsc.identifier({ text: 'semver' }), - trim: tsc.identifier({ text: 'trim' }), - upper: tsc.identifier({ text: 'upper' }), + json: 'json', + lower: 'lower', + normalize: 'normalize', + numeric: 'numeric', + parse: 'parse', + preformatted: 'preformatted', + regex: 'regex', + semver: 'semver', + trim: 'trim', + upper: 'upper', url: 'url', uuid: 'uuid', v1: 'v1', @@ -76,40 +74,40 @@ export const identifiers = { * {@link https://arktype.io/docs/type-api Type API} */ type: { - $: tsc.identifier({ text: '$' }), - allows: tsc.identifier({ text: 'allows' }), - and: tsc.identifier({ text: 'and' }), - array: tsc.identifier({ text: 'array' }), - as: tsc.identifier({ text: 'as' }), - assert: tsc.identifier({ text: 'assert' }), - brand: tsc.identifier({ text: 'brand' }), - configure: tsc.identifier({ text: 'configure' }), - default: tsc.identifier({ text: 'default' }), - describe: tsc.identifier({ text: 'describe' }), - description: tsc.identifier({ text: 'description' }), - equals: tsc.identifier({ text: 'equals' }), - exclude: tsc.identifier({ text: 'exclude' }), - expression: tsc.identifier({ text: 'expression' }), - extends: tsc.identifier({ text: 'extends' }), - extract: tsc.identifier({ text: 'extract' }), - filter: tsc.identifier({ text: 'filter' }), - from: tsc.identifier({ text: 'from' }), - ifEquals: tsc.identifier({ text: 'ifEquals' }), - ifExtends: tsc.identifier({ text: 'ifExtends' }), - infer: tsc.identifier({ text: 'infer' }), - inferIn: tsc.identifier({ text: 'inferIn' }), - intersect: tsc.identifier({ text: 'intersect' }), - json: tsc.identifier({ text: 'json' }), - meta: tsc.identifier({ text: 'meta' }), - narrow: tsc.identifier({ text: 'narrow' }), - onDeepUndeclaredKey: tsc.identifier({ text: 'onDeepUndeclaredKey' }), - onUndeclaredKey: tsc.identifier({ text: 'onUndeclaredKey' }), - optional: tsc.identifier({ text: 'optional' }), - or: tsc.identifier({ text: 'or' }), - overlaps: tsc.identifier({ text: 'overlaps' }), - pipe: tsc.identifier({ text: 'pipe' }), - select: tsc.identifier({ text: 'select' }), - to: tsc.identifier({ text: 'to' }), - toJsonSchema: tsc.identifier({ text: 'toJsonSchema' }), + $: '$', + allows: 'allows', + and: 'and', + array: 'array', + as: 'as', + assert: 'assert', + brand: 'brand', + configure: 'configure', + default: 'default', + describe: 'describe', + description: 'description', + equals: 'equals', + exclude: 'exclude', + expression: 'expression', + extends: 'extends', + extract: 'extract', + filter: 'filter', + from: 'from', + ifEquals: 'ifEquals', + ifExtends: 'ifExtends', + infer: 'infer', + inferIn: 'inferIn', + intersect: 'intersect', + json: 'json', + meta: 'meta', + narrow: 'narrow', + onDeepUndeclaredKey: 'onDeepUndeclaredKey', + onUndeclaredKey: 'onUndeclaredKey', + optional: 'optional', + or: 'or', + overlaps: 'overlaps', + pipe: 'pipe', + select: 'select', + to: 'to', + toJsonSchema: 'toJsonSchema', }, }; diff --git a/packages/openapi-ts/src/plugins/arktype/shared/export.ts b/packages/openapi-ts/src/plugins/arktype/shared/export.ts index 51ce98986..26ea4ac24 100644 --- a/packages/openapi-ts/src/plugins/arktype/shared/export.ts +++ b/packages/openapi-ts/src/plugins/arktype/shared/export.ts @@ -1,9 +1,8 @@ import type { Symbol } from '@hey-api/codegen-core'; -import 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 '../constants'; import type { ArktypePlugin } from '../types'; @@ -27,38 +26,31 @@ export const exportAst = ({ resource: 'arktype.type', }); - const statement = tsc.constVariable({ - comment: plugin.config.comments - ? createSchemaComment({ schema }) - : undefined, - exportConst: symbol.exported, - expression: tsc.callExpression({ - functionName: type.placeholder, - parameters: [ - ast.def ? tsc.stringLiteral({ text: ast.def }) : ast.expression, - ], - }), - name: symbol.placeholder, - // typeName: ast.typeName - // ? (tsc.propertyAccessExpression({ - // expression: z.placeholder, - // name: ast.typeName, - // }) as unknown as ts.TypeNode) - // : undefined, - }); + const statement = $.const(symbol.placeholder) + .export(symbol.exported) + .$if(plugin.config.comments && createSchemaComment({ schema }), (c, v) => + c.doc(v as ReadonlyArray), + ) + // .type( + // ast.typeName + // ? (tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: ast.typeName, + // }) as unknown as ts.TypeNode) + // : undefined, + // ) + .assign( + $(type.placeholder).call(ast.def ? $.literal(ast.def) : ast.expression), + ); plugin.setSymbolValue(symbol, statement); if (typeInferSymbol) { - const inferType = tsc.typeAliasDeclaration({ - exportType: typeInferSymbol.exported, - name: typeInferSymbol.placeholder, - type: ts.factory.createTypeQueryNode( - ts.factory.createQualifiedName( - ts.factory.createIdentifier(symbol.placeholder), - identifiers.type.infer, - ), - ), - }); + const inferType = $.type + .alias(typeInferSymbol.placeholder) + .export(typeInferSymbol.exported) + .type( + $.type(symbol.placeholder).attr(identifiers.type.infer).typeofType(), + ); plugin.setSymbolValue(typeInferSymbol, inferType); } }; diff --git a/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts b/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts index cc36ea03b..c68359fcf 100644 --- a/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts +++ b/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts @@ -3,12 +3,13 @@ import type ts from 'typescript'; import type { IR } from '~/ir/types'; import type { ToRefs } from '~/plugins'; +import type { $ } from '~/ts-dsl'; import type { ArktypePlugin } from '../types'; export type Ast = { def: string; - expression: ts.Expression; + expression: ReturnType; hasLazyExpression?: boolean; typeName?: string | ts.Identifier; }; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts b/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts index 188f2e7bd..2392b4537 100644 --- a/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts +++ b/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts @@ -5,7 +5,7 @@ import type { IR } from '~/ir/types'; import { buildName } from '~/openApi/shared/utils/name'; import type { SchemaWithType } from '~/plugins/shared/types/schema'; import { toRefs } from '~/plugins/shared/utils/refs'; -import { tsc } from '~/tsc'; +import { $ } from '~/ts-dsl'; import { pathToJsonPointer, refToName } from '~/utils/ref'; import { exportAst } from '../shared/export'; @@ -43,27 +43,14 @@ export const irSchemaToAst = ({ }; const refSymbol = plugin.referenceSymbol(query); if (plugin.isSymbolRegistered(query)) { - const ref = tsc.identifier({ text: refSymbol.placeholder }); + const ref = $(refSymbol.placeholder); ast.expression = ref; } else { - const lazyExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - // expression: z.placeholder, - expression: 'TODO', - name: 'TODO', - // name: identifiers.lazy, - }), - parameters: [ - tsc.arrowFunction({ - returnType: tsc.keywordTypeNode({ keyword: 'any' }), - statements: [ - tsc.returnStatement({ - expression: tsc.identifier({ text: refSymbol.placeholder }), - }), - ], - }), - ], - }); + // expression: z.placeholder, + // name: identifiers.lazy, + const lazyExpression = $('TODO') + .attr('TODO') + .call($.func().returns('any').do($.return(refSymbol.placeholder))); ast.expression = lazyExpression; ast.hasLazyExpression = true; state.hasLazyExpression.value = true; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts index abd6f2ba4..a6018fcb9 100644 --- a/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts @@ -1,6 +1,5 @@ -import ts from 'typescript'; - import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { $ } from '~/ts-dsl'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; import { nullToAst } from './null'; @@ -91,30 +90,11 @@ export const irSchemaWithTypeToAst = ({ resource: 'arktype.type', }); - const expression = ts.factory.createCallExpression( - ts.factory.createIdentifier(type.placeholder), - undefined, - [ - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - 'name', - ts.factory.createStringLiteral('string'), - ), - ts.factory.createPropertyAssignment( - 'platform', - ts.factory.createStringLiteral("'android' | 'ios'"), - ), - ts.factory.createPropertyAssignment( - ts.factory.createComputedPropertyName( - ts.factory.createStringLiteral('versions?'), - ), - ts.factory.createStringLiteral('(number | string)[]'), - ), - ], - true, - ), - ], + const expression = $(type.placeholder).call( + $.object() + .prop('name', $.literal('string')) + .prop('platform', $.literal("'android' | 'ios'")) + .computed('versions?', $.literal('(number | string)[]')), ); return { diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts index 1ca6e0d30..2a011f5ac 100644 --- a/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts @@ -1,9 +1,6 @@ -import ts from 'typescript'; - import type { SchemaWithType } from '~/plugins/shared/types/schema'; import { toRef } from '~/plugins/shared/utils/refs'; -import { tsc } from '~/tsc'; -import { numberRegExp } from '~/utils/regexp'; +import { $ } from '~/ts-dsl'; // import { identifiers } from '../../constants'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; @@ -19,8 +16,8 @@ export const objectToAst = ({ const result: Partial> = {}; // TODO: parser - handle constants - const properties: Array = - []; + + const shape = $.object().pretty(); const required = schema.required ?? []; @@ -41,12 +38,6 @@ export const objectToAst = ({ result.hasLazyExpression = true; } - let propertyName: - | ts.ComputedPropertyName - | ts.StringLiteral - | ts.NumericLiteral - | string = isRequired ? name : `${name}?`; - // if (propertyAst.hasCircularReference) { // properties.push( // tsc.getAccessorDeclaration({ @@ -76,38 +67,11 @@ export const objectToAst = ({ // ); // } - if (propertyName.endsWith('?')) { - propertyName = ts.factory.createComputedPropertyName( - tsc.stringLiteral({ text: propertyName }), - ); + if (isRequired) { + shape.prop(name, propertyAst.expression); } else { - // TODO: parser - abstract safe property name logic - if ( - ((propertyName.match(/^[0-9]/) && propertyName.match(/\D+/g)) || - propertyName.match(/\W/g)) && - !propertyName.startsWith("'") && - !propertyName.endsWith("'") - ) { - propertyName = `'${propertyName}'`; - } - - numberRegExp.lastIndex = 0; - if (numberRegExp.test(propertyName)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = propertyName.startsWith('-') - ? tsc.stringLiteral({ text: name }) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = name; - } + shape.computed(`${name}?`, propertyAst.expression); } - properties.push( - tsc.propertyAssignment({ - initializer: propertyAst.expression, - name: propertyName, - }), - ); } if ( @@ -122,24 +86,12 @@ export const objectToAst = ({ path: toRef([...state.path.value, 'additionalProperties']), }, }); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: 'TODO', - name: 'record', - // name: identifiers.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: 'TODO', - name: 'string', - // name: identifiers.string, - }), - parameters: [], - }), - additionalAst.expression, - ], - }); + // name: identifiers.record, + result.expression = $('TODO').attr('record').call( + // name: identifiers.string, + $('TODO').attr('string').call(), + additionalAst.expression, + ); if (additionalAst.hasLazyExpression) { result.hasLazyExpression = true; } @@ -155,10 +107,7 @@ export const objectToAst = ({ return result as Omit; } - result.expression = ts.factory.createObjectLiteralExpression( - properties, - true, - ); + result.expression = shape; // return with typeName for circular references if (result.hasLazyExpression) { diff --git a/packages/openapi-ts/src/ts-dsl/as.ts b/packages/openapi-ts/src/ts-dsl/as.ts index d383620b6..735110ee0 100644 --- a/packages/openapi-ts/src/ts-dsl/as.ts +++ b/packages/openapi-ts/src/ts-dsl/as.ts @@ -30,4 +30,4 @@ export class AsTsDsl extends TsDsl { export interface AsTsDsl extends ExprMixin {} mixin(AsTsDsl, ExprMixin); -registerLazyAccessAsFactory((expr, type) => new AsTsDsl(expr, type)); +registerLazyAccessAsFactory((...args) => new AsTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/attr.ts b/packages/openapi-ts/src/ts-dsl/attr.ts index c5dfa6358..e2cc6749d 100644 --- a/packages/openapi-ts/src/ts-dsl/attr.ts +++ b/packages/openapi-ts/src/ts-dsl/attr.ts @@ -60,4 +60,4 @@ export interface AttrTsDsl OptionalMixin {} mixin(AttrTsDsl, AssignmentMixin, ExprMixin, OperatorMixin, OptionalMixin); -registerLazyAccessAttrFactory((expr, name) => new AttrTsDsl(expr, name)); +registerLazyAccessAttrFactory((...args) => new AttrTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/await.ts b/packages/openapi-ts/src/ts-dsl/await.ts index 2bb333f0c..c96e04e68 100644 --- a/packages/openapi-ts/src/ts-dsl/await.ts +++ b/packages/openapi-ts/src/ts-dsl/await.ts @@ -22,4 +22,4 @@ export class AwaitTsDsl extends TsDsl { export interface AwaitTsDsl extends ExprMixin {} mixin(AwaitTsDsl, ExprMixin); -registerLazyAccessAwaitFactory((expr) => new AwaitTsDsl(expr)); +registerLazyAccessAwaitFactory((...args) => new AwaitTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/expr.ts b/packages/openapi-ts/src/ts-dsl/expr.ts index 769951743..e384d8e4e 100644 --- a/packages/openapi-ts/src/ts-dsl/expr.ts +++ b/packages/openapi-ts/src/ts-dsl/expr.ts @@ -6,9 +6,7 @@ import { TsDsl } from './base'; import { mixin } from './mixins/apply'; import { ExprMixin } from './mixins/expr'; import { OperatorMixin } from './mixins/operator'; -import { TypeExprTsDsl } from './type/expr'; -import { TypeQueryTsDsl } from './type/query'; -import { TypeOfExprTsDsl } from './typeof'; +import { TypeExprMixin } from './mixins/type-expr'; export class ExprTsDsl extends TsDsl { private _exprInput: string | MaybeTsDsl; @@ -18,22 +16,10 @@ export class ExprTsDsl extends TsDsl { this._exprInput = id; } - returnType(): TypeExprTsDsl { - return new TypeExprTsDsl('ReturnType').generic(this.typeofType()); - } - - typeofExpr(): TypeOfExprTsDsl { - return new TypeOfExprTsDsl(this); - } - - typeofType(): TypeQueryTsDsl { - return new TypeQueryTsDsl(this); - } - $render(): ts.Expression { return this.$node(this._exprInput); } } -export interface ExprTsDsl extends ExprMixin, OperatorMixin {} -mixin(ExprTsDsl, ExprMixin, OperatorMixin); +export interface ExprTsDsl extends ExprMixin, OperatorMixin, TypeExprMixin {} +mixin(ExprTsDsl, ExprMixin, OperatorMixin, TypeExprMixin); diff --git a/packages/openapi-ts/src/ts-dsl/mixins/expr.ts b/packages/openapi-ts/src/ts-dsl/mixins/expr.ts index 895842abd..c08c9412c 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/expr.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/expr.ts @@ -8,10 +8,8 @@ import type { CallTsDsl } from '../call'; import type { ReturnTsDsl } from '../return'; /** - * Access helpers depend on other DSL classes that are initialized later in the - * module graph. We store factory callbacks here and let each class lazily - * register its own implementation once it has finished evaluation. This keeps - * mixin application order predictable and avoids circular import crashes. + * Lazily register factory callbacks to avoid circular imports and + * ensure predictable mixin application order. */ type AsFactory = ( @@ -19,7 +17,7 @@ type AsFactory = ( type: string | TypeTsDsl, ) => AsTsDsl; let asFactory: AsFactory | undefined; -/** Registers the Attr DSL factory after its module has finished evaluating. */ +/** Registers the As DSL factory after its module has finished evaluating. */ export function registerLazyAccessAsFactory(factory: AsFactory): void { asFactory = factory; } diff --git a/packages/openapi-ts/src/ts-dsl/mixins/type-args.ts b/packages/openapi-ts/src/ts-dsl/mixins/type-args.ts index bbec9b944..cb0c56289 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/type-args.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/type-args.ts @@ -20,9 +20,7 @@ export class TypeArgsMixin extends TsDsl { return this; } - /** - * Returns the type arguments as an array of ts.TypeNode nodes. - */ + /** Returns the type arguments as an array of ts.TypeNode nodes. */ protected $generics(): ReadonlyArray | undefined { return this.$type(this._generics); } diff --git a/packages/openapi-ts/src/ts-dsl/mixins/type-expr.ts b/packages/openapi-ts/src/ts-dsl/mixins/type-expr.ts new file mode 100644 index 000000000..0a93e51b9 --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/mixins/type-expr.ts @@ -0,0 +1,64 @@ +import type ts from 'typescript'; + +import type { MaybeTsDsl, TypeTsDsl } from '../base'; +import type { TypeExprTsDsl } from '../type/expr'; +import type { TypeQueryTsDsl } from '../type/query'; +import type { TypeOfExprTsDsl } from '../typeof'; + +/** + * Lazily register factory callbacks to avoid circular imports and + * ensure predictable mixin application order. + */ + +type TypeExprFactory = ( + nameOrFn?: string | ((t: TypeExprTsDsl) => void), + fn?: (t: TypeExprTsDsl) => void, +) => TypeExprTsDsl; +let typeExprFactory: TypeExprFactory | undefined; +/** Registers the TypeExpr DSL factory after its module has finished evaluating. */ +export function registerLazyAccessTypeExprFactory( + factory: TypeExprFactory, +): void { + typeExprFactory = factory; +} + +type TypeOfExprFactory = ( + expr: string | MaybeTsDsl, +) => TypeOfExprTsDsl; +let typeOfExprFactory: TypeOfExprFactory | undefined; +/** Registers the TypeOfExpr DSL factory after its module has finished evaluating. */ +export function registerLazyAccessTypeOfExprFactory( + factory: TypeOfExprFactory, +): void { + typeOfExprFactory = factory; +} + +type TypeQueryFactory = ( + expr: string | MaybeTsDsl, +) => TypeQueryTsDsl; +let typeQueryFactory: TypeQueryFactory | undefined; +/** Registers the TypeQuery DSL factory after its module has finished evaluating. */ +export function registerLazyAccessTypeQueryFactory( + factory: TypeQueryFactory, +): void { + typeQueryFactory = factory; +} + +export class TypeExprMixin { + /** Create a TypeExpr DSL node representing ReturnType. */ + returnType(this: MaybeTsDsl): TypeExprTsDsl { + return typeExprFactory!('ReturnType').generic(typeQueryFactory!(this)); + } + + /** Create a TypeOfExpr DSL node representing typeof this. */ + typeofExpr(this: string | MaybeTsDsl): TypeOfExprTsDsl { + return typeOfExprFactory!(this); + } + + /** Create a TypeQuery DSL node representing typeof this. */ + typeofType( + this: string | MaybeTsDsl, + ): TypeQueryTsDsl { + return typeQueryFactory!(this); + } +} diff --git a/packages/openapi-ts/src/ts-dsl/mixins/type-params.ts b/packages/openapi-ts/src/ts-dsl/mixins/type-params.ts index 230d1f40f..9f305a618 100644 --- a/packages/openapi-ts/src/ts-dsl/mixins/type-params.ts +++ b/packages/openapi-ts/src/ts-dsl/mixins/type-params.ts @@ -22,9 +22,7 @@ export class TypeParamsMixin extends TsDsl { return this; } - /** - * Returns the type parameters as an array of ts.TypeParameterDeclaration nodes. - */ + /** Returns the type parameters as an array of ts.TypeParameterDeclaration nodes. */ protected $generics(): | ReadonlyArray | undefined { diff --git a/packages/openapi-ts/src/ts-dsl/object.ts b/packages/openapi-ts/src/ts-dsl/object.ts index f6c47c746..11723e493 100644 --- a/packages/openapi-ts/src/ts-dsl/object.ts +++ b/packages/openapi-ts/src/ts-dsl/object.ts @@ -14,6 +14,11 @@ import { SetterTsDsl } from './setter'; export class ObjectTsDsl extends TsDsl { private props: Array< + | { + expr: string | MaybeTsDsl; + kind: 'computed'; + name: string; + } | { expr: string | MaybeTsDsl; kind: 'getter'; @@ -28,6 +33,12 @@ export class ObjectTsDsl extends TsDsl { | { expr: string | MaybeTsDsl; kind: 'spread' } > = []; + /** Adds a computed property (e.g. `{ [expr]: value }`). */ + computed(name: string, expr: string | MaybeTsDsl): this { + this.props.push({ expr, kind: 'computed', name }); + return this; + } + /** Adds a getter property (e.g. `{ get foo() { ... } }`). */ getter(name: string, expr: string | MaybeTsDsl): this { this.props.push({ expr, kind: 'getter', name }); @@ -94,8 +105,11 @@ export class ObjectTsDsl extends TsDsl { 'Invalid property: object property value must be an expression, not a statement.', ); } + const name = this.safePropertyName(p.name); return ts.factory.createPropertyAssignment( - this.safePropertyName(p.name), + p.kind === 'computed' + ? ts.factory.createComputedPropertyName(this.$node(name as string)) + : name, node, ); }); diff --git a/packages/openapi-ts/src/ts-dsl/return.ts b/packages/openapi-ts/src/ts-dsl/return.ts index 94a7d8edf..e31491b72 100644 --- a/packages/openapi-ts/src/ts-dsl/return.ts +++ b/packages/openapi-ts/src/ts-dsl/return.ts @@ -22,4 +22,4 @@ export class ReturnTsDsl extends TsDsl { export interface ReturnTsDsl extends ExprMixin {} mixin(ReturnTsDsl, ExprMixin); -registerLazyAccessReturnFactory((expr) => new ReturnTsDsl(expr)); +registerLazyAccessReturnFactory((...args) => new ReturnTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/type/attr.ts b/packages/openapi-ts/src/ts-dsl/type/attr.ts index e4c58edff..ebf7a16b3 100644 --- a/packages/openapi-ts/src/ts-dsl/type/attr.ts +++ b/packages/openapi-ts/src/ts-dsl/type/attr.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type, @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 { TypeExprMixin } from '../mixins/type-expr'; export class TypeAttrTsDsl extends TsDsl { private _base?: string | MaybeTsDsl; @@ -43,3 +46,6 @@ export class TypeAttrTsDsl extends TsDsl { return ts.factory.createQualifiedName(left, right); } } + +export interface TypeAttrTsDsl extends TypeExprMixin {} +mixin(TypeAttrTsDsl, TypeExprMixin); diff --git a/packages/openapi-ts/src/ts-dsl/type/expr.ts b/packages/openapi-ts/src/ts-dsl/type/expr.ts index 8c62ab631..28ffdbdd9 100644 --- a/packages/openapi-ts/src/ts-dsl/type/expr.ts +++ b/packages/openapi-ts/src/ts-dsl/type/expr.ts @@ -1,9 +1,13 @@ -/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */ +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ import ts from 'typescript'; import { TypeTsDsl } from '../base'; import { mixin } from '../mixins/apply'; import { TypeArgsMixin } from '../mixins/type-args'; +import { + registerLazyAccessTypeExprFactory, + TypeExprMixin, +} from '../mixins/type-expr'; import { TypeAttrTsDsl } from './attr'; import { TypeIdxTsDsl } from './idx'; @@ -52,5 +56,10 @@ export class TypeExprTsDsl extends TypeTsDsl { } } -export interface TypeExprTsDsl extends TypeArgsMixin {} -mixin(TypeExprTsDsl, TypeArgsMixin); +export interface TypeExprTsDsl extends TypeArgsMixin, TypeExprMixin {} +mixin(TypeExprTsDsl, TypeArgsMixin, TypeExprMixin); + +registerLazyAccessTypeExprFactory( + (...args) => + new TypeExprTsDsl(...(args as ConstructorParameters)), +); diff --git a/packages/openapi-ts/src/ts-dsl/type/query.ts b/packages/openapi-ts/src/ts-dsl/type/query.ts index 85158c517..2685059f3 100644 --- a/packages/openapi-ts/src/ts-dsl/type/query.ts +++ b/packages/openapi-ts/src/ts-dsl/type/query.ts @@ -2,22 +2,20 @@ import ts from 'typescript'; import type { MaybeTsDsl } from '../base'; import { TypeTsDsl } from '../base'; +import { registerLazyAccessTypeQueryFactory } from '../mixins/type-expr'; export class TypeQueryTsDsl extends TypeTsDsl { - private expr: string | MaybeTsDsl; + private _expr: string | MaybeTsDsl; - constructor(expr: string | MaybeTsDsl) { + constructor(expr: string | MaybeTsDsl) { super(); - this.expr = expr; + this._expr = expr; } $render(): ts.TypeQueryNode { - const exprName = this.$node(this.expr); - if (!ts.isEntityName(exprName)) { - throw new Error( - 'TypeQueryTsDsl: expression must resolve to an EntityName', - ); - } - return ts.factory.createTypeQueryNode(exprName); + const expr = this.$node(this._expr); + return ts.factory.createTypeQueryNode(expr as unknown as ts.EntityName); } } + +registerLazyAccessTypeQueryFactory((...args) => new TypeQueryTsDsl(...args)); diff --git a/packages/openapi-ts/src/ts-dsl/typeof.ts b/packages/openapi-ts/src/ts-dsl/typeof.ts index f31ec1bbe..8b5b169a9 100644 --- a/packages/openapi-ts/src/ts-dsl/typeof.ts +++ b/packages/openapi-ts/src/ts-dsl/typeof.ts @@ -5,6 +5,7 @@ import type { MaybeTsDsl } from './base'; import { TsDsl } from './base'; import { mixin } from './mixins/apply'; import { OperatorMixin } from './mixins/operator'; +import { registerLazyAccessTypeOfExprFactory } from './mixins/type-expr'; export class TypeOfExprTsDsl extends TsDsl { private _expr: string | MaybeTsDsl; @@ -21,3 +22,5 @@ export class TypeOfExprTsDsl extends TsDsl { export interface TypeOfExprTsDsl extends OperatorMixin {} mixin(TypeOfExprTsDsl, OperatorMixin); + +registerLazyAccessTypeOfExprFactory((...args) => new TypeOfExprTsDsl(...args));