From 9ef28fe5596d8f90ada90f839d7f12239e09e27c Mon Sep 17 00:00:00 2001 From: DaxServer Date: Tue, 2 Sep 2025 18:55:19 +0200 Subject: [PATCH] feat(typebox): improve handling of large union types This commit introduces a new feature to handle large union types more efficiently in the TypeBox schema codegen. The key changes are: - Introduce a `shouldChunkUnion` function to check if a union type has more than a certain number of members (20 in this case). - Implement the `createChunkNodes` function to split large union types into smaller "chunk" types, each containing a subset of the original union members. - Update the `addLocalTypes` function in `local-type-collector.ts` to detect large unions and create the corresponding chunk types. - The chunk types are added to the `NodeGraph` and `ResolverStore` to be processed by the schema codegen. This change improves the performance and memory usage of the schema codegen when dealing with large union types, which can be common in complex validation schemas. --- src/handlers/typebox/base-type-handler.ts | 3 +- .../typebox/collection/array-type-handler.ts | 9 +- .../collection/collection-base-handler.ts | 13 +- .../collection/intersection-type-handler.ts | 5 +- .../typebox/collection/tuple-type-handler.ts | 6 +- .../typebox/collection/union-type-handler.ts | 6 +- src/handlers/typebox/date-type-handler.ts | 4 +- src/handlers/typebox/function-type-handler.ts | 13 +- .../typebox/indexed-access-type-handler.ts | 28 +- src/handlers/typebox/keyof-typeof-handler.ts | 12 +- src/handlers/typebox/literal-type-handler.ts | 16 +- .../typebox/object/interface-type-handler.ts | 17 +- .../object/object-like-base-handler.ts | 14 +- .../typebox/object/object-type-handler.ts | 6 +- .../typebox/reference/omit-type-handler.ts | 21 +- .../typebox/reference/partial-type-handler.ts | 15 +- .../typebox/reference/pick-type-handler.ts | 21 +- .../reference/readonly-type-handler.ts | 13 +- .../typebox/reference/record-type-handler.ts | 50 ++- .../reference/required-type-handler.ts | 9 +- src/handlers/typebox/simple-type-handler.ts | 4 +- .../typebox/template-literal-type-handler.ts | 16 +- .../template-literal-type-processor.ts | 28 +- src/handlers/typebox/type-query-handler.ts | 4 +- .../typebox/type-reference-handler.ts | 9 +- .../type/type-operator-base-handler.ts | 9 +- src/handlers/typebox/typeof-type-handler.ts | 4 +- src/index.ts | 9 +- src/parsers/base-parser.ts | 4 + src/parsers/parse-chunks.ts | 97 +++++ src/parsers/parse-enums.ts | 4 +- src/parsers/parse-function-declarations.ts | 13 +- src/parsers/parse-interfaces.ts | 8 +- src/parsers/parse-type-aliases.ts | 19 +- src/printer/typebox-printer.ts | 27 +- src/traverse/dependency-traversal.ts | 4 + src/traverse/local-type-collector.ts | 120 +++++- src/traverse/types.ts | 5 +- src/utils/add-static-type-alias.ts | 20 - src/utils/generic-type-utils.ts | 34 +- src/utils/key-extraction-utils.ts | 10 +- src/utils/typebox-call.ts | 13 +- src/utils/typebox-codegen-utils.ts | 12 - .../deep-instantiation-integration.test.ts | 361 ++++++++++++++++++ tsconfig.build.json | 12 +- 45 files changed, 916 insertions(+), 211 deletions(-) create mode 100644 src/parsers/parse-chunks.ts delete mode 100644 src/utils/add-static-type-alias.ts delete mode 100644 src/utils/typebox-codegen-utils.ts create mode 100644 tests/integration/deep-instantiation-integration.test.ts diff --git a/src/handlers/typebox/base-type-handler.ts b/src/handlers/typebox/base-type-handler.ts index c9eaf39..3228da8 100644 --- a/src/handlers/typebox/base-type-handler.ts +++ b/src/handlers/typebox/base-type-handler.ts @@ -1,6 +1,7 @@ +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { Node, ts } from 'ts-morph' export abstract class BaseTypeHandler { abstract canHandle(node: Node): boolean - abstract handle(node: Node): ts.Expression + abstract handle(node: Node, context: TypeBoxContext): ts.Expression } diff --git a/src/handlers/typebox/collection/array-type-handler.ts b/src/handlers/typebox/collection/array-type-handler.ts index cf6f41c..843a6e7 100644 --- a/src/handlers/typebox/collection/array-type-handler.ts +++ b/src/handlers/typebox/collection/array-type-handler.ts @@ -1,6 +1,7 @@ import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { ArrayTypeNode, Node, ts } from 'ts-morph' export class ArrayTypeHandler extends CollectionBaseHandler { @@ -8,9 +9,9 @@ export class ArrayTypeHandler extends CollectionBaseHandler { return Node.isArrayTypeNode(node) } - handle(node: ArrayTypeNode): ts.Expression { - const typeboxType = getTypeBoxType(node.getElementTypeNode()) + handle(node: ArrayTypeNode, context: TypeBoxContext): ts.Expression { + const typeboxType = getTypeBoxType(node.getElementTypeNode(), context) - return makeTypeCall('Array', [typeboxType]) + return GenericTypeUtils.makeTypeCall('Array', [typeboxType]) } } diff --git a/src/handlers/typebox/collection/collection-base-handler.ts b/src/handlers/typebox/collection/collection-base-handler.ts index ad5c621..cb9ac99 100644 --- a/src/handlers/typebox/collection/collection-base-handler.ts +++ b/src/handlers/typebox/collection/collection-base-handler.ts @@ -1,13 +1,18 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, ts } from 'ts-morph' export abstract class CollectionBaseHandler extends BaseTypeHandler { - protected processTypeCollection(nodes: Node[], typeBoxFunction: string): ts.Expression { - const typeBoxTypes = nodes.map((node) => getTypeBoxType(node)) + protected processTypeCollection( + nodes: Node[], + typeBoxFunction: string, + context: TypeBoxContext, + ): ts.Expression { + const typeBoxTypes = nodes.map((node) => getTypeBoxType(node, context)) const arrayLiteral = ts.factory.createArrayLiteralExpression(typeBoxTypes) - return makeTypeCall(typeBoxFunction, [arrayLiteral]) + return GenericTypeUtils.makeTypeCall(typeBoxFunction, [arrayLiteral]) } } diff --git a/src/handlers/typebox/collection/intersection-type-handler.ts b/src/handlers/typebox/collection/intersection-type-handler.ts index ac23ce0..be28890 100644 --- a/src/handlers/typebox/collection/intersection-type-handler.ts +++ b/src/handlers/typebox/collection/intersection-type-handler.ts @@ -1,4 +1,5 @@ import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { IntersectionTypeNode, Node, ts } from 'ts-morph' export class IntersectionTypeHandler extends CollectionBaseHandler { @@ -6,7 +7,7 @@ export class IntersectionTypeHandler extends CollectionBaseHandler { return Node.isIntersectionTypeNode(node) } - handle(node: IntersectionTypeNode): ts.Expression { - return this.processTypeCollection(node.getTypeNodes(), 'Intersect') + handle(node: IntersectionTypeNode, context: TypeBoxContext): ts.Expression { + return this.processTypeCollection(node.getTypeNodes(), 'Intersect', context) } } diff --git a/src/handlers/typebox/collection/tuple-type-handler.ts b/src/handlers/typebox/collection/tuple-type-handler.ts index ac981ca..4f15164 100644 --- a/src/handlers/typebox/collection/tuple-type-handler.ts +++ b/src/handlers/typebox/collection/tuple-type-handler.ts @@ -1,4 +1,5 @@ import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { Node, ts, TupleTypeNode } from 'ts-morph' export class TupleTypeHandler extends CollectionBaseHandler { @@ -6,7 +7,8 @@ export class TupleTypeHandler extends CollectionBaseHandler { return Node.isTupleTypeNode(node) } - handle(node: TupleTypeNode): ts.Expression { - return this.processTypeCollection(node.getElements(), 'Tuple') + handle(node: TupleTypeNode, context: TypeBoxContext): ts.Expression { + const elements = node.getElements() + return this.processTypeCollection(elements, 'Tuple', context) } } diff --git a/src/handlers/typebox/collection/union-type-handler.ts b/src/handlers/typebox/collection/union-type-handler.ts index d070c42..c8ae592 100644 --- a/src/handlers/typebox/collection/union-type-handler.ts +++ b/src/handlers/typebox/collection/union-type-handler.ts @@ -1,4 +1,5 @@ import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { Node, ts, UnionTypeNode } from 'ts-morph' export class UnionTypeHandler extends CollectionBaseHandler { @@ -6,7 +7,8 @@ export class UnionTypeHandler extends CollectionBaseHandler { return Node.isUnionTypeNode(node) } - handle(node: UnionTypeNode): ts.Expression { - return this.processTypeCollection(node.getTypeNodes(), 'Union') + handle(node: UnionTypeNode, context: TypeBoxContext): ts.Expression { + const types = node.getTypeNodes() + return this.processTypeCollection(types, 'Union', context) } } diff --git a/src/handlers/typebox/date-type-handler.ts b/src/handlers/typebox/date-type-handler.ts index 94f11ff..fa7f677 100644 --- a/src/handlers/typebox/date-type-handler.ts +++ b/src/handlers/typebox/date-type-handler.ts @@ -1,5 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, ts, TypeReferenceNode } from 'ts-morph' export class DateTypeHandler extends BaseTypeHandler { @@ -10,6 +10,6 @@ export class DateTypeHandler extends BaseTypeHandler { } handle(): ts.Expression { - return makeTypeCall('Date') + return GenericTypeUtils.makeTypeCall('Date') } } diff --git a/src/handlers/typebox/function-type-handler.ts b/src/handlers/typebox/function-type-handler.ts index 99f4da1..42fb683 100644 --- a/src/handlers/typebox/function-type-handler.ts +++ b/src/handlers/typebox/function-type-handler.ts @@ -1,6 +1,7 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { FunctionTypeNode, Node, ts } from 'ts-morph' export class FunctionTypeHandler extends BaseTypeHandler { @@ -8,28 +9,28 @@ export class FunctionTypeHandler extends BaseTypeHandler { return Node.isFunctionTypeNode(node) } - handle(node: FunctionTypeNode): ts.Expression { + handle(node: FunctionTypeNode, context: TypeBoxContext): ts.Expression { const parameters = node.getParameters() const returnType = node.getReturnTypeNode() // Convert parameters to TypeBox types const parameterTypes = parameters.map((param) => { const paramTypeNode = param.getTypeNode() - const paramType = getTypeBoxType(paramTypeNode) + const paramType = getTypeBoxType(paramTypeNode, context) // Check if parameter is optional if (param.hasQuestionToken()) { - return makeTypeCall('Optional', [paramType]) + return GenericTypeUtils.makeTypeCall('Optional', [paramType]) } return paramType }) // Convert return type to TypeBox type - const returnTypeBox = getTypeBoxType(returnType) + const returnTypeBox = getTypeBoxType(returnType, context) // Create TypeBox Function call with parameters array and return type - return makeTypeCall('Function', [ + return GenericTypeUtils.makeTypeCall('Function', [ ts.factory.createArrayLiteralExpression(parameterTypes), returnTypeBox, ]) diff --git a/src/handlers/typebox/indexed-access-type-handler.ts b/src/handlers/typebox/indexed-access-type-handler.ts index e11669a..bc773dd 100644 --- a/src/handlers/typebox/indexed-access-type-handler.ts +++ b/src/handlers/typebox/indexed-access-type-handler.ts @@ -1,6 +1,7 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { IndexedAccessTypeNode, Node, ts, TypeNode } from 'ts-morph' export class IndexedAccessTypeHandler extends BaseTypeHandler { @@ -8,7 +9,7 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler { return node.isKind(ts.SyntaxKind.IndexedAccessType) } - handle(node: IndexedAccessTypeNode): ts.Expression { + handle(node: IndexedAccessTypeNode, context: TypeBoxContext): ts.Expression { const objectType = node.getObjectTypeNode() const indexType = node.getIndexTypeNode() @@ -17,18 +18,19 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler { objectType.isKind(ts.SyntaxKind.TypeQuery) && indexType.isKind(ts.SyntaxKind.NumberKeyword) ) { - return this.handleTypeofArrayAccess(objectType, node) + return this.handleTypeofArrayAccess(objectType, node, context) } - const typeboxObjectType = getTypeBoxType(objectType) - const typeboxIndexType = getTypeBoxType(indexType) + const typeboxObjectType = getTypeBoxType(objectType, context) + const typeboxIndexType = getTypeBoxType(indexType, context) - return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) + return GenericTypeUtils.makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) } private handleTypeofArrayAccess( typeQuery: Node, indexedAccessType: IndexedAccessTypeNode, + context: TypeBoxContext, ): ts.Expression { const typeQueryNode = typeQuery.asKindOrThrow(ts.SyntaxKind.TypeQuery) const exprName = typeQueryNode.getExprName() @@ -54,10 +56,10 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler { } // Fallback to default Index behavior - const typeboxObjectType = getTypeBoxType(typeQuery) - const typeboxIndexType = getTypeBoxType(indexedAccessType.getIndexTypeNode()) + const typeboxObjectType = getTypeBoxType(typeQuery, context) + const typeboxIndexType = getTypeBoxType(indexedAccessType.getIndexTypeNode(), context) - return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) + return GenericTypeUtils.makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) } private extractTupleUnion(typeNode: TypeNode | undefined): ts.Expression | null { @@ -86,14 +88,18 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler { if (literal.isKind(ts.SyntaxKind.StringLiteral)) { const stringLiteral = literal.asKindOrThrow(ts.SyntaxKind.StringLiteral) const value = stringLiteral.getLiteralValue() - literalTypes.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(value)])) + literalTypes.push( + GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createStringLiteral(value)]), + ) } } } // Return union of literal types if we found any if (literalTypes.length > 0) { - return makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literalTypes)]) + return GenericTypeUtils.makeTypeCall('Union', [ + ts.factory.createArrayLiteralExpression(literalTypes), + ]) } } diff --git a/src/handlers/typebox/keyof-typeof-handler.ts b/src/handlers/typebox/keyof-typeof-handler.ts index e0be21f..cde3b03 100644 --- a/src/handlers/typebox/keyof-typeof-handler.ts +++ b/src/handlers/typebox/keyof-typeof-handler.ts @@ -1,5 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, ts, TypeOperatorTypeNode, VariableDeclaration } from 'ts-morph' export class KeyOfTypeofHandler extends BaseTypeHandler { @@ -13,13 +13,13 @@ export class KeyOfTypeofHandler extends BaseTypeHandler { handle(node: TypeOperatorTypeNode): ts.Expression { const typeQuery = node.getTypeNode() - if (!Node.isTypeQuery(typeQuery)) return makeTypeCall('Any') + if (!Node.isTypeQuery(typeQuery)) return GenericTypeUtils.makeTypeCall('Any') const exprName = typeQuery.getExprName() - if (!Node.isIdentifier(exprName)) return makeTypeCall('Any') + if (!Node.isIdentifier(exprName)) return GenericTypeUtils.makeTypeCall('Any') const keys = this.getObjectKeys(exprName) - return keys.length > 0 ? this.createUnion(keys) : makeTypeCall('Any') + return keys.length > 0 ? this.createUnion(keys) : GenericTypeUtils.makeTypeCall('Any') } private getObjectKeys(node: Node): string[] { @@ -80,11 +80,11 @@ export class KeyOfTypeofHandler extends BaseTypeHandler { ? ts.factory.createNumericLiteral(num) : ts.factory.createStringLiteral(key) - return makeTypeCall('Literal', [literal]) + return GenericTypeUtils.makeTypeCall('Literal', [literal]) }) return literals.length === 1 ? literals[0]! - : makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literals)]) + : GenericTypeUtils.makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literals)]) } } diff --git a/src/handlers/typebox/literal-type-handler.ts b/src/handlers/typebox/literal-type-handler.ts index a556e0a..776a9b3 100644 --- a/src/handlers/typebox/literal-type-handler.ts +++ b/src/handlers/typebox/literal-type-handler.ts @@ -1,5 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { LiteralTypeNode, Node, SyntaxKind, ts } from 'ts-morph' export class LiteralTypeHandler extends BaseTypeHandler { @@ -12,19 +12,21 @@ export class LiteralTypeHandler extends BaseTypeHandler { switch (literal.getKind()) { case SyntaxKind.StringLiteral: - return makeTypeCall('Literal', [ + return GenericTypeUtils.makeTypeCall('Literal', [ ts.factory.createStringLiteral(literal.getText().slice(1, -1)), ]) case SyntaxKind.NumericLiteral: - return makeTypeCall('Literal', [ts.factory.createNumericLiteral(literal.getText())]) + return GenericTypeUtils.makeTypeCall('Literal', [ + ts.factory.createNumericLiteral(literal.getText()), + ]) case SyntaxKind.TrueKeyword: - return makeTypeCall('Literal', [ts.factory.createTrue()]) + return GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createTrue()]) case SyntaxKind.FalseKeyword: - return makeTypeCall('Literal', [ts.factory.createFalse()]) + return GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createFalse()]) case SyntaxKind.NullKeyword: - return makeTypeCall('Null') + return GenericTypeUtils.makeTypeCall('Null') default: - return makeTypeCall('Any') + return GenericTypeUtils.makeTypeCall('Any') } } } diff --git a/src/handlers/typebox/object/interface-type-handler.ts b/src/handlers/typebox/object/interface-type-handler.ts index 0e59caa..39670fa 100644 --- a/src/handlers/typebox/object/interface-type-handler.ts +++ b/src/handlers/typebox/object/interface-type-handler.ts @@ -1,5 +1,6 @@ import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { HeritageClause, InterfaceDeclaration, Node, ts } from 'ts-morph' export class InterfaceTypeHandler extends ObjectLikeBaseHandler { @@ -7,9 +8,11 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { return Node.isInterfaceDeclaration(node) } - handle(node: InterfaceDeclaration): ts.Expression { + handle(node: InterfaceDeclaration, context: TypeBoxContext): ts.Expression { const heritageClauses = node.getHeritageClauses() - const baseObjectType = this.createObjectType(this.processProperties(node.getProperties())) + const baseObjectType = this.createObjectType( + this.processProperties(node.getProperties(), context), + ) if (heritageClauses.length === 0) return baseObjectType @@ -20,7 +23,7 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { const allTypes = [...extendedTypes, baseObjectType] const expression = ts.factory.createArrayLiteralExpression(allTypes, true) - return makeTypeCall('Composite', [expression]) + return GenericTypeUtils.makeTypeCall('Composite', [expression]) } private parseGenericTypeCall(typeText: string): ts.Expression | null { @@ -42,11 +45,11 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { // Convert common TypeScript types to TypeBox calls switch (typeArg) { case 'number': - return makeTypeCall('Number') + return GenericTypeUtils.makeTypeCall('Number') case 'string': - return makeTypeCall('String') + return GenericTypeUtils.makeTypeCall('String') case 'boolean': - return makeTypeCall('Boolean') + return GenericTypeUtils.makeTypeCall('Boolean') default: // For other types, assume it's a reference return ts.factory.createIdentifier(typeArg) diff --git a/src/handlers/typebox/object/object-like-base-handler.ts b/src/handlers/typebox/object/object-like-base-handler.ts index bba91b3..f46737c 100644 --- a/src/handlers/typebox/object/object-like-base-handler.ts +++ b/src/handlers/typebox/object/object-like-base-handler.ts @@ -1,11 +1,15 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { isValidIdentifier } from '@daxserver/validation-schema-codegen/utils/identifier-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, PropertySignature, ts } from 'ts-morph' export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { - protected processProperties(properties: PropertySignature[]): ts.PropertyAssignment[] { + protected processProperties( + properties: PropertySignature[], + context?: TypeBoxContext, + ): ts.PropertyAssignment[] { const propertyAssignments: ts.PropertyAssignment[] = [] for (const prop of properties) { @@ -13,7 +17,7 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { if (!propTypeNode) continue const outputNameNode = this.extractPropertyNameInfo(prop) - const valueExpr = getTypeBoxType(propTypeNode) + const valueExpr = getTypeBoxType(propTypeNode, context) const isAlreadyOptional = ts.isCallExpression(valueExpr) && ts.isPropertyAccessExpression(valueExpr.expression) && @@ -21,7 +25,7 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { const maybeOptional = prop.hasQuestionToken() && !isAlreadyOptional - ? makeTypeCall('Optional', [valueExpr]) + ? GenericTypeUtils.makeTypeCall('Optional', [valueExpr]) : valueExpr propertyAssignments.push(ts.factory.createPropertyAssignment(outputNameNode, maybeOptional)) @@ -33,7 +37,7 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { protected createObjectType(properties: ts.PropertyAssignment[]): ts.Expression { const objectLiteral = ts.factory.createObjectLiteralExpression(properties, true) - return makeTypeCall('Object', [objectLiteral]) + return GenericTypeUtils.makeTypeCall('Object', [objectLiteral]) } private extractPropertyNameInfo(prop: PropertySignature): ts.PropertyName { diff --git a/src/handlers/typebox/object/object-type-handler.ts b/src/handlers/typebox/object/object-type-handler.ts index 0c3ea21..eab39e2 100644 --- a/src/handlers/typebox/object/object-type-handler.ts +++ b/src/handlers/typebox/object/object-type-handler.ts @@ -1,4 +1,5 @@ import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { Node, ts, TypeLiteralNode } from 'ts-morph' export class ObjectTypeHandler extends ObjectLikeBaseHandler { @@ -6,7 +7,8 @@ export class ObjectTypeHandler extends ObjectLikeBaseHandler { return Node.isTypeLiteral(node) } - handle(node: TypeLiteralNode): ts.Expression { - return this.createObjectType(this.processProperties(node.getProperties())) + handle(node: TypeLiteralNode, context: TypeBoxContext): ts.Expression { + const properties = node.getProperties() + return this.createObjectType(this.processProperties(properties, context)) } } diff --git a/src/handlers/typebox/reference/omit-type-handler.ts b/src/handlers/typebox/reference/omit-type-handler.ts index f56bc7d..e1010d1 100644 --- a/src/handlers/typebox/reference/omit-type-handler.ts +++ b/src/handlers/typebox/reference/omit-type-handler.ts @@ -1,26 +1,29 @@ import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { createTypeBoxKeys, extractStringKeys, } from '@daxserver/validation-schema-codegen/utils/key-extraction-utils' -import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { + getTypeBoxType, + type TypeBoxContext, +} from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { ts, TypeReferenceNode } from 'ts-morph' export class OmitTypeHandler extends TypeReferenceBaseHandler { - protected supportedTypeNames = ['Omit'] - protected expectedArgumentCount = 2 + protected readonly supportedTypeNames = ['Omit'] + protected readonly expectedArgumentCount = 2 - handle(node: Node): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const typeRef = this.validateTypeReference(node) const [objectType, keysType] = this.extractTypeArguments(typeRef) - if (!keysType) return makeTypeCall('Any') + if (!keysType) return GenericTypeUtils.makeTypeCall('Any') - const typeboxObjectType = getTypeBoxType(objectType) + const typeboxObjectType = getTypeBoxType(objectType, context) const omitKeys = extractStringKeys(keysType) const typeboxKeys = createTypeBoxKeys(omitKeys) - return makeTypeCall('Omit', [typeboxObjectType, typeboxKeys]) + return GenericTypeUtils.makeTypeCall('Omit', [typeboxObjectType, typeboxKeys]) } } diff --git a/src/handlers/typebox/reference/partial-type-handler.ts b/src/handlers/typebox/reference/partial-type-handler.ts index ad38392..070e145 100644 --- a/src/handlers/typebox/reference/partial-type-handler.ts +++ b/src/handlers/typebox/reference/partial-type-handler.ts @@ -1,18 +1,21 @@ import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' -import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import { + getTypeBoxType, + type TypeBoxContext, +} from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { ts, TypeReferenceNode } from 'ts-morph' export class PartialTypeHandler extends TypeReferenceBaseHandler { protected readonly supportedTypeNames = ['Partial'] protected readonly expectedArgumentCount = 1 - handle(node: Node): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const typeRef = this.validateTypeReference(node) const [innerType] = this.extractTypeArguments(typeRef) - const typeBoxType = getTypeBoxType(innerType) + const typeBoxType = getTypeBoxType(innerType, context) - return makeTypeCall('Partial', [typeBoxType]) + return GenericTypeUtils.makeTypeCall('Partial', [typeBoxType]) } } diff --git a/src/handlers/typebox/reference/pick-type-handler.ts b/src/handlers/typebox/reference/pick-type-handler.ts index b71f586..89e2c7f 100644 --- a/src/handlers/typebox/reference/pick-type-handler.ts +++ b/src/handlers/typebox/reference/pick-type-handler.ts @@ -1,26 +1,29 @@ import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { createTypeBoxKeys, extractStringKeys, } from '@daxserver/validation-schema-codegen/utils/key-extraction-utils' -import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { + getTypeBoxType, + type TypeBoxContext, +} from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { ts, TypeReferenceNode } from 'ts-morph' export class PickTypeHandler extends TypeReferenceBaseHandler { - protected supportedTypeNames = ['Pick'] - protected expectedArgumentCount = 2 + protected readonly supportedTypeNames = ['Pick'] + protected readonly expectedArgumentCount = 2 - handle(node: Node): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const typeRef = this.validateTypeReference(node) const [objectType, keysType] = this.extractTypeArguments(typeRef) - if (!keysType) return makeTypeCall('Any') + if (!keysType) return GenericTypeUtils.makeTypeCall('Any') - const typeboxObjectType = getTypeBoxType(objectType) + const typeboxObjectType = getTypeBoxType(objectType, context) const pickKeys = extractStringKeys(keysType) const typeboxKeys = createTypeBoxKeys(pickKeys) - return makeTypeCall('Pick', [typeboxObjectType, typeboxKeys]) + return GenericTypeUtils.makeTypeCall('Pick', [typeboxObjectType, typeboxKeys]) } } diff --git a/src/handlers/typebox/reference/readonly-type-handler.ts b/src/handlers/typebox/reference/readonly-type-handler.ts index e5b237b..4cb0356 100644 --- a/src/handlers/typebox/reference/readonly-type-handler.ts +++ b/src/handlers/typebox/reference/readonly-type-handler.ts @@ -1,16 +1,19 @@ import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' -import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import { + getTypeBoxType, + type TypeBoxContext, +} from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { ts, TypeReferenceNode } from 'ts-morph' export class ReadonlyTypeHandler extends TypeReferenceBaseHandler { protected readonly supportedTypeNames = ['Readonly'] protected readonly expectedArgumentCount = 1 - handle(node: Node): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const typeRef = this.validateTypeReference(node) const [innerType] = this.extractTypeArguments(typeRef) - return makeTypeCall('Readonly', [getTypeBoxType(innerType)]) + return GenericTypeUtils.makeTypeCall('Readonly', [getTypeBoxType(innerType, context)]) } } diff --git a/src/handlers/typebox/reference/record-type-handler.ts b/src/handlers/typebox/reference/record-type-handler.ts index b814b40..a429a55 100644 --- a/src/handlers/typebox/reference/record-type-handler.ts +++ b/src/handlers/typebox/reference/record-type-handler.ts @@ -1,19 +1,57 @@ import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { Node, ts, TypeReferenceNode } from 'ts-morph' export class RecordTypeHandler extends TypeReferenceBaseHandler { protected readonly supportedTypeNames = ['Record'] protected readonly expectedArgumentCount = 2 - handle(node: Node): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const typeRef = this.validateTypeReference(node) const [keyType, valueType] = this.extractTypeArguments(typeRef) - const typeBoxKeyType = getTypeBoxType(keyType) - const typeBoxValueType = getTypeBoxType(valueType) + // Check if keyType is a type reference that might be chunked + if (Node.isTypeReference(keyType)) { + const typeName = keyType.getTypeName() + if (Node.isIdentifier(typeName)) { + const originalTypeName = typeName.getText() + const qualifiedName = resolverStore.resolveQualifiedName(originalTypeName) - return makeTypeCall('Record', [typeBoxKeyType, typeBoxValueType]) + // Check if the node exists in the graph before accessing it + if (qualifiedName && context.nodeGraph.hasNode(qualifiedName)) { + const referencedNode = context.nodeGraph.getNode(qualifiedName) + + // Check if this is a chunked union type + if ( + referencedNode && + referencedNode.chunkReferences && + referencedNode.chunkReferences.length > 0 + ) { + // Generate Type.Intersect of Type.Record for each chunk + const chunkRecords = referencedNode.chunkReferences.map( + (chunkQualifiedName: string) => { + const chunkNode = context.nodeGraph.getNode(chunkQualifiedName) + const chunkOriginalName = chunkNode?.originalName || chunkQualifiedName + const chunkIdentifier = ts.factory.createIdentifier(chunkOriginalName) + const typeBoxValueType = getTypeBoxType(valueType, context) + return GenericTypeUtils.makeTypeCall('Record', [chunkIdentifier, typeBoxValueType]) + }, + ) + + return GenericTypeUtils.makeTypeCall('Intersect', [ + ts.factory.createArrayLiteralExpression(chunkRecords), + ]) + } + } + } + } + + const typeBoxKeyType = getTypeBoxType(keyType, context) + const typeBoxValueType = getTypeBoxType(valueType, context) + + return GenericTypeUtils.makeTypeCall('Record', [typeBoxKeyType, typeBoxValueType]) } } diff --git a/src/handlers/typebox/reference/required-type-handler.ts b/src/handlers/typebox/reference/required-type-handler.ts index 17e2529..f7d3409 100644 --- a/src/handlers/typebox/reference/required-type-handler.ts +++ b/src/handlers/typebox/reference/required-type-handler.ts @@ -1,16 +1,17 @@ import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { ts, TypeReferenceNode } from 'ts-morph' export class RequiredTypeHandler extends TypeReferenceBaseHandler { protected readonly supportedTypeNames = ['Required'] protected readonly expectedArgumentCount = 1 - handle(node: Node): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const typeRef = this.validateTypeReference(node) const [innerType] = this.extractTypeArguments(typeRef) - return makeTypeCall('Required', [getTypeBoxType(innerType)]) + return GenericTypeUtils.makeTypeCall('Required', [getTypeBoxType(innerType, context)]) } } diff --git a/src/handlers/typebox/simple-type-handler.ts b/src/handlers/typebox/simple-type-handler.ts index 403e896..09d6407 100644 --- a/src/handlers/typebox/simple-type-handler.ts +++ b/src/handlers/typebox/simple-type-handler.ts @@ -1,6 +1,6 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { isAnySyntaxKind } from '@daxserver/validation-schema-codegen/utils/node-type-utils' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, SyntaxKind, ts } from 'ts-morph' export const TypeBoxType = 'Type' @@ -34,6 +34,6 @@ export class SimpleTypeHandler extends BaseTypeHandler { } handle(node: Node): ts.Expression { - return makeTypeCall(kindToTypeBox[node.getKind() as SimpleKind]) + return GenericTypeUtils.makeTypeCall(kindToTypeBox[node.getKind() as SimpleKind]) } } diff --git a/src/handlers/typebox/template-literal-type-handler.ts b/src/handlers/typebox/template-literal-type-handler.ts index e3857e1..e22437f 100644 --- a/src/handlers/typebox/template-literal-type-handler.ts +++ b/src/handlers/typebox/template-literal-type-handler.ts @@ -1,6 +1,6 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' import { TemplateLiteralTypeProcessor } from '@daxserver/validation-schema-codegen/handlers/typebox/template-literal-type-processor' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, TemplateLiteralTypeNode, ts } from 'ts-morph' export class TemplateLiteralTypeHandler extends BaseTypeHandler { @@ -16,7 +16,9 @@ export class TemplateLiteralTypeHandler extends BaseTypeHandler { const headCompilerNode = head.compilerNode as ts.TemplateHead const headText = headCompilerNode.text if (headText) { - parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)])) + parts.push( + GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]), + ) } // Process template spans (substitutions + following literal parts) @@ -32,16 +34,20 @@ export class TemplateLiteralTypeHandler extends BaseTypeHandler { // Add the literal part after the substitution const literalText = compilerNode.literal.text if (literalText) { - parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)])) + parts.push( + GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]), + ) } } // If no parts were found, fallback to a simple string if (parts.length === 0) { - return makeTypeCall('String') + return GenericTypeUtils.makeTypeCall('String') } // Return TemplateLiteral with array of parts - return makeTypeCall('TemplateLiteral', [ts.factory.createArrayLiteralExpression(parts)]) + return GenericTypeUtils.makeTypeCall('TemplateLiteral', [ + ts.factory.createArrayLiteralExpression(parts), + ]) } } diff --git a/src/handlers/typebox/template-literal-type-processor.ts b/src/handlers/typebox/template-literal-type-processor.ts index fbf4cca..8577138 100644 --- a/src/handlers/typebox/template-literal-type-processor.ts +++ b/src/handlers/typebox/template-literal-type-processor.ts @@ -1,4 +1,4 @@ -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { ts } from 'ts-morph' /** @@ -11,15 +11,15 @@ export class TemplateLiteralTypeProcessor { static processType(node: ts.TypeNode): ts.Expression { switch (node.kind) { case ts.SyntaxKind.StringKeyword: - return makeTypeCall('String') + return GenericTypeUtils.makeTypeCall('String') case ts.SyntaxKind.NumberKeyword: - return makeTypeCall('Number') + return GenericTypeUtils.makeTypeCall('Number') case ts.SyntaxKind.LiteralType: return this.processLiteralType(node as ts.LiteralTypeNode) case ts.SyntaxKind.UnionType: return this.processUnionType(node as ts.UnionTypeNode) default: - return makeTypeCall('String') + return GenericTypeUtils.makeTypeCall('String') } } @@ -28,15 +28,19 @@ export class TemplateLiteralTypeProcessor { */ private static processLiteralType(literalType: ts.LiteralTypeNode): ts.Expression { if (ts.isStringLiteral(literalType.literal)) { - return makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]) + return GenericTypeUtils.makeTypeCall('Literal', [ + ts.factory.createStringLiteral(literalType.literal.text), + ]) } if (ts.isNumericLiteral(literalType.literal)) { - return makeTypeCall('Literal', [ts.factory.createNumericLiteral(literalType.literal.text)]) + return GenericTypeUtils.makeTypeCall('Literal', [ + ts.factory.createNumericLiteral(literalType.literal.text), + ]) } // Fallback for other literals (boolean, etc.) - return makeTypeCall('String') + return GenericTypeUtils.makeTypeCall('String') } /** @@ -47,12 +51,16 @@ export class TemplateLiteralTypeProcessor { if (t.kind === ts.SyntaxKind.LiteralType) { const literalType = t as ts.LiteralTypeNode if (ts.isStringLiteral(literalType.literal)) { - return makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]) + return GenericTypeUtils.makeTypeCall('Literal', [ + ts.factory.createStringLiteral(literalType.literal.text), + ]) } } - return makeTypeCall('String') // fallback + return GenericTypeUtils.makeTypeCall('String') // fallback }) - return makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(unionParts)]) + return GenericTypeUtils.makeTypeCall('Union', [ + ts.factory.createArrayLiteralExpression(unionParts), + ]) } } diff --git a/src/handlers/typebox/type-query-handler.ts b/src/handlers/typebox/type-query-handler.ts index ed7e6f7..30907d7 100644 --- a/src/handlers/typebox/type-query-handler.ts +++ b/src/handlers/typebox/type-query-handler.ts @@ -1,5 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, ts, TypeQueryNode } from 'ts-morph' export class TypeQueryHandler extends BaseTypeHandler { @@ -24,6 +24,6 @@ export class TypeQueryHandler extends BaseTypeHandler { return ts.factory.createIdentifier(fullName.replace('.', '_')) } - return makeTypeCall('Any') + return GenericTypeUtils.makeTypeCall('Any') } } diff --git a/src/handlers/typebox/type-reference-handler.ts b/src/handlers/typebox/type-reference-handler.ts index 1ec1db8..af8375c 100644 --- a/src/handlers/typebox/type-reference-handler.ts +++ b/src/handlers/typebox/type-reference-handler.ts @@ -1,7 +1,8 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, ts, TypeReferenceNode } from 'ts-morph' export class TypeReferenceHandler extends BaseTypeHandler { @@ -9,7 +10,7 @@ export class TypeReferenceHandler extends BaseTypeHandler { return Node.isTypeReference(node) } - handle(node: TypeReferenceNode): ts.Expression { + handle(node: TypeReferenceNode, context: TypeBoxContext): ts.Expression { const referencedType = node.getTypeName() const typeArguments = node.getTypeArguments() @@ -21,7 +22,7 @@ export class TypeReferenceHandler extends BaseTypeHandler { // If there are type arguments, create a function call if (typeArguments.length > 0) { - const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg)) + const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg, context)) return ts.factory.createCallExpression( ts.factory.createIdentifier(typeName), @@ -34,6 +35,6 @@ export class TypeReferenceHandler extends BaseTypeHandler { return ts.factory.createIdentifier(typeName) } - return makeTypeCall('Any') + return GenericTypeUtils.makeTypeCall('Any') } } diff --git a/src/handlers/typebox/type/type-operator-base-handler.ts b/src/handlers/typebox/type/type-operator-base-handler.ts index 9badf93..88922f4 100644 --- a/src/handlers/typebox/type/type-operator-base-handler.ts +++ b/src/handlers/typebox/type/type-operator-base-handler.ts @@ -1,7 +1,8 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { isTypeOperatorWithOperator } from '@daxserver/validation-schema-codegen/utils/node-type-utils' +import type { TypeBoxContext } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph' /** @@ -16,10 +17,10 @@ export abstract class TypeOperatorBaseHandler extends BaseTypeHandler { return isTypeOperatorWithOperator(node, this.operatorKind) } - handle(node: TypeOperatorTypeNode): ts.Expression { + handle(node: TypeOperatorTypeNode, context: TypeBoxContext): ts.Expression { const innerType = node.getTypeNode() - const typeboxType = getTypeBoxType(innerType) + const typeboxType = getTypeBoxType(innerType, context) - return makeTypeCall(this.typeBoxMethod, [typeboxType]) + return GenericTypeUtils.makeTypeCall(this.typeBoxMethod, [typeboxType]) } } diff --git a/src/handlers/typebox/typeof-type-handler.ts b/src/handlers/typebox/typeof-type-handler.ts index 16a32eb..53a6601 100644 --- a/src/handlers/typebox/typeof-type-handler.ts +++ b/src/handlers/typebox/typeof-type-handler.ts @@ -1,5 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, ts } from 'ts-morph' export class TypeofTypeHandler extends BaseTypeHandler { @@ -10,6 +10,6 @@ export class TypeofTypeHandler extends BaseTypeHandler { handle(): ts.Expression { // TypeQuery represents 'typeof' expressions in TypeScript // For TypeBox, we'll return a String type as typeof returns string literals - return makeTypeCall('String') + return GenericTypeUtils.makeTypeCall('String') } } diff --git a/src/index.ts b/src/index.ts index 7881e14..13916a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,10 +45,15 @@ const createOutputFile = (hasGenericInterfaces: boolean) => { return newSourceFile } -const printSortedNodes = (sortedTraversedNodes: TraversedNode[], newSourceFile: SourceFile) => { +const printSortedNodes = ( + sortedTraversedNodes: TraversedNode[], + newSourceFile: SourceFile, + dependencyTraversal: DependencyTraversal, +) => { const printer = new TypeBoxPrinter({ newSourceFile, printer: ts.createPrinter(), + nodeGraph: dependencyTraversal.getNodeGraph(), }) // Process nodes in topological order @@ -94,7 +99,7 @@ export const generateCode = (options: InputOptions): string => { const newSourceFile = createOutputFile(hasGenericInterfaces) // Print sorted nodes to output - const result = printSortedNodes(traversedNodes, newSourceFile) + const result = printSortedNodes(traversedNodes, newSourceFile, dependencyTraversal) return result } diff --git a/src/parsers/base-parser.ts b/src/parsers/base-parser.ts index c477365..b72f977 100644 --- a/src/parsers/base-parser.ts +++ b/src/parsers/base-parser.ts @@ -1,20 +1,24 @@ +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' import { Node, SourceFile, ts } from 'ts-morph' export interface BaseParserOptions { newSourceFile: SourceFile printer: ts.Printer processedTypes: Set + nodeGraph: NodeGraph } export abstract class BaseParser { protected newSourceFile: SourceFile protected printer: ts.Printer protected processedTypes: Set + protected nodeGraph: NodeGraph constructor(options: BaseParserOptions) { this.newSourceFile = options.newSourceFile this.printer = options.printer this.processedTypes = options.processedTypes + this.nodeGraph = options.nodeGraph } abstract parse(node: Node): void diff --git a/src/parsers/parse-chunks.ts b/src/parsers/parse-chunks.ts new file mode 100644 index 0000000..4729152 --- /dev/null +++ b/src/parsers/parse-chunks.ts @@ -0,0 +1,97 @@ +import { + BaseParser, + type BaseParserOptions, +} from '@daxserver/validation-schema-codegen/parsers/base-parser' +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { SourceFile, ts } from 'ts-morph' + +export interface ChunkParserOptions extends BaseParserOptions { + newSourceFile: SourceFile + printer: ts.Printer + processedTypes: Set +} + +export class ChunkParser extends BaseParser { + constructor(options: ChunkParserOptions) { + super(options) + } + + parse(): void { + // This method is required by BaseParser but not used for chunk parsing + throw new Error('Use parseChunk(traversedNode, aliasName) instead') + } + + parseChunk(traversedNode: TraversedNode, aliasName?: string): void { + const typeName = aliasName || traversedNode.originalName + + if (this.processedTypes.has(typeName)) return + this.processedTypes.add(typeName) + + this.parseChunkNode(traversedNode, typeName) + } + + private parseChunkNode(traversedNode: TraversedNode, typeName: string): void { + // If this is an individual chunk node (type === 'chunk'), process it as a regular union + if (traversedNode.type === 'chunk') { + const typeboxType = this.printer.printNode( + ts.EmitHint.Expression, + getTypeBoxType(traversedNode.node, { nodeGraph: this.nodeGraph }), + this.newSourceFile.compilerNode, + ) + + // Chunk nodes should only be const variables, not exported and without static type aliases + GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, typeName, typeboxType, false) + + return + } + + // If this is a main type with chunk references, create union of chunk identifiers + if (!traversedNode.chunkReferences || traversedNode.chunkReferences.length === 0) { + // Fallback to empty union if no chunk references + const unionCall = GenericTypeUtils.makeTypeCall('Union', []) + const typeboxType = this.printer.printNode( + ts.EmitHint.Expression, + unionCall, + this.newSourceFile.compilerNode, + ) + + GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, typeName, typeboxType) + GenericTypeUtils.addStaticTypeAlias( + this.newSourceFile, + typeName, + this.newSourceFile.compilerNode, + this.printer, + ) + + return + } + + // Convert chunk references to identifier references + const chunkIdentifiers = traversedNode.chunkReferences.map((qualifiedName) => { + const chunkNode = this.nodeGraph.getNode(qualifiedName) + if (!chunkNode) { + throw new Error(`Chunk node not found: ${qualifiedName}`) + } + // Use the original name of the chunk as an identifier + return ts.factory.createIdentifier(chunkNode.originalName) + }) + const arrayLiteral = ts.factory.createArrayLiteralExpression(chunkIdentifiers) + const unionCall = GenericTypeUtils.makeTypeCall('Union', [arrayLiteral]) + + const typeboxType = this.printer.printNode( + ts.EmitHint.Expression, + unionCall, + this.newSourceFile.compilerNode, + ) + + GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, typeName, typeboxType) + GenericTypeUtils.addStaticTypeAlias( + this.newSourceFile, + typeName, + this.newSourceFile.compilerNode, + this.printer, + ) + } +} diff --git a/src/parsers/parse-enums.ts b/src/parsers/parse-enums.ts index 72313e4..ae8c846 100644 --- a/src/parsers/parse-enums.ts +++ b/src/parsers/parse-enums.ts @@ -1,5 +1,5 @@ import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser' -import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { EnumDeclaration, VariableDeclarationKind } from 'ts-morph' export class EnumParser extends BaseParser { @@ -30,7 +30,7 @@ export class EnumParser extends BaseParser { ], }) - addStaticTypeAlias( + GenericTypeUtils.addStaticTypeAlias( this.newSourceFile, schemaName, this.newSourceFile.compilerNode, diff --git a/src/parsers/parse-function-declarations.ts b/src/parsers/parse-function-declarations.ts index 6d9654d..8ea208d 100644 --- a/src/parsers/parse-function-declarations.ts +++ b/src/parsers/parse-function-declarations.ts @@ -1,7 +1,6 @@ import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser' -import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph' export class FunctionDeclarationParser extends BaseParser { @@ -21,7 +20,9 @@ export class FunctionDeclarationParser extends BaseParser { // Convert parameters to TypeBox types const parameterTypes = parameters.map((param) => { const paramTypeNode = param.getTypeNode() - const paramType = paramTypeNode ? getTypeBoxType(paramTypeNode) : makeTypeCall('Any') + const paramType = paramTypeNode + ? getTypeBoxType(paramTypeNode, { nodeGraph: this.nodeGraph }) + : GenericTypeUtils.makeTypeCall('Any') // Check if parameter is optional or required if (!param.hasQuestionToken()) { @@ -40,7 +41,9 @@ export class FunctionDeclarationParser extends BaseParser { }) // Convert return type to TypeBox type - const returnTypeBox = returnType ? getTypeBoxType(returnType) : makeTypeCall('Any') + const returnTypeBox = returnType + ? getTypeBoxType(returnType, { nodeGraph: this.nodeGraph }) + : GenericTypeUtils.makeTypeCall('Any') // Create TypeBox Function call with parameters array and return type const typeboxExpression = ts.factory.createCallExpression( @@ -69,7 +72,7 @@ export class FunctionDeclarationParser extends BaseParser { ], }) - addStaticTypeAlias( + GenericTypeUtils.addStaticTypeAlias( this.newSourceFile, functionName, this.newSourceFile.compilerNode, diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts index fd2e4dd..bff16d0 100644 --- a/src/parsers/parse-interfaces.ts +++ b/src/parsers/parse-interfaces.ts @@ -1,5 +1,4 @@ import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser' -import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias' import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { InterfaceDeclaration, ts } from 'ts-morph' @@ -23,7 +22,7 @@ export class InterfaceParser extends BaseParser { private parseRegularInterface(interfaceDecl: InterfaceDeclaration, interfaceName: string): void { // Generate TypeBox type definition - const typeboxTypeNode = getTypeBoxType(interfaceDecl) + const typeboxTypeNode = getTypeBoxType(interfaceDecl, { nodeGraph: this.nodeGraph }) const typeboxType = this.printer.printNode( ts.EmitHint.Expression, typeboxTypeNode, @@ -31,8 +30,7 @@ export class InterfaceParser extends BaseParser { ) GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, interfaceName, typeboxType) - - addStaticTypeAlias( + GenericTypeUtils.addStaticTypeAlias( this.newSourceFile, interfaceName, this.newSourceFile.compilerNode, @@ -44,7 +42,7 @@ export class InterfaceParser extends BaseParser { const typeParameters = interfaceDecl.getTypeParameters() // Generate TypeBox function definition using the same flow as type aliases - const typeboxTypeNode = getTypeBoxType(interfaceDecl) + const typeboxTypeNode = getTypeBoxType(interfaceDecl, { nodeGraph: this.nodeGraph }) // Create the function expression using shared utilities (mirrors type-alias flow) const functionExpression = GenericTypeUtils.createGenericArrowFunction( diff --git a/src/parsers/parse-type-aliases.ts b/src/parsers/parse-type-aliases.ts index 6db4657..f298600 100644 --- a/src/parsers/parse-type-aliases.ts +++ b/src/parsers/parse-type-aliases.ts @@ -1,8 +1,7 @@ import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser' -import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias' import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' + import { ts, TypeAliasDeclaration } from 'ts-morph' export class TypeAliasParser extends BaseParser { @@ -24,7 +23,9 @@ export class TypeAliasParser extends BaseParser { private parseRegularTypeAlias(typeAlias: TypeAliasDeclaration, typeName: string): void { const typeNode = typeAlias.getTypeNode() - const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any') + const typeboxTypeNode = typeNode + ? getTypeBoxType(typeNode, { nodeGraph: this.nodeGraph }) + : GenericTypeUtils.makeTypeCall('Any') const typeboxType = this.printer.printNode( ts.EmitHint.Expression, typeboxTypeNode, @@ -32,8 +33,12 @@ export class TypeAliasParser extends BaseParser { ) GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, typeName, typeboxType) - - addStaticTypeAlias(this.newSourceFile, typeName, this.newSourceFile.compilerNode, this.printer) + GenericTypeUtils.addStaticTypeAlias( + this.newSourceFile, + typeName, + this.newSourceFile.compilerNode, + this.printer, + ) } private parseGenericTypeAlias(typeAlias: TypeAliasDeclaration, typeName: string): void { @@ -41,7 +46,9 @@ export class TypeAliasParser extends BaseParser { // Generate TypeBox function definition const typeNode = typeAlias.getTypeNode() - const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any') + const typeboxTypeNode = typeNode + ? getTypeBoxType(typeNode, { nodeGraph: this.nodeGraph }) + : GenericTypeUtils.makeTypeCall('Any') // Create the function expression using shared utilities const functionExpression = GenericTypeUtils.createGenericArrowFunction( diff --git a/src/printer/typebox-printer.ts b/src/printer/typebox-printer.ts index 23f530f..bfc99f0 100644 --- a/src/printer/typebox-printer.ts +++ b/src/printer/typebox-printer.ts @@ -1,3 +1,4 @@ +import { ChunkParser } from '@daxserver/validation-schema-codegen/parsers/parse-chunks' import { EnumParser } from '@daxserver/validation-schema-codegen/parsers/parse-enums' import { FunctionDeclarationParser } from '@daxserver/validation-schema-codegen/parsers/parse-function-declarations' import { InterfaceParser } from '@daxserver/validation-schema-codegen/parsers/parse-interfaces' @@ -5,39 +6,63 @@ import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/pa import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' import { Node, SourceFile, ts } from 'ts-morph' +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' + export interface PrinterOptions { newSourceFile: SourceFile printer: ts.Printer + nodeGraph: NodeGraph } export class TypeBoxPrinter { private readonly newSourceFile: SourceFile private readonly printer: ts.Printer + private readonly nodeGraph: NodeGraph private readonly processedTypes = new Set() private readonly typeAliasParser: TypeAliasParser private readonly interfaceParser: InterfaceParser private readonly enumParser: EnumParser private readonly functionParser: FunctionDeclarationParser + private readonly chunkParser: ChunkParser constructor(options: PrinterOptions) { this.newSourceFile = options.newSourceFile this.printer = options.printer + this.nodeGraph = options.nodeGraph // Initialize parsers with the same configuration const parserOptions = { newSourceFile: this.newSourceFile, printer: this.printer, processedTypes: this.processedTypes, + nodeGraph: this.nodeGraph, } this.typeAliasParser = new TypeAliasParser(parserOptions) this.interfaceParser = new InterfaceParser(parserOptions) this.enumParser = new EnumParser(parserOptions) this.functionParser = new FunctionDeclarationParser(parserOptions) + this.chunkParser = new ChunkParser(parserOptions) } printNode(traversedNode: TraversedNode): void { - const { node, aliasName } = traversedNode + const { node, aliasName, type } = traversedNode + + // Handle chunk nodes first + if (type === 'chunk') { + this.chunkParser.parseChunk(traversedNode, aliasName) + return + } + + // Handle type aliases with chunk references using chunk parser + if ( + type === 'typeAlias' && + traversedNode.chunkReferences && + traversedNode.chunkReferences.length > 0 + ) { + this.chunkParser.parseChunk(traversedNode, aliasName) + return + } switch (true) { case Node.isTypeAliasDeclaration(node): diff --git a/src/traverse/dependency-traversal.ts b/src/traverse/dependency-traversal.ts index 9d7cf37..806d797 100644 --- a/src/traverse/dependency-traversal.ts +++ b/src/traverse/dependency-traversal.ts @@ -68,4 +68,8 @@ export class DependencyTraversal { async visualizeGraph(options: VisualizationOptions = {}): Promise { return GraphVisualizer.generateVisualization(this.nodeGraph, options) } + + getNodeGraph(): NodeGraph { + return this.nodeGraph + } } diff --git a/src/traverse/local-type-collector.ts b/src/traverse/local-type-collector.ts index 701bb30..b880f53 100644 --- a/src/traverse/local-type-collector.ts +++ b/src/traverse/local-type-collector.ts @@ -1,6 +1,82 @@ import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' -import { SourceFile } from 'ts-morph' +import { Node, SourceFile } from 'ts-morph' + +const CHUNK_SIZE = 20 + +const shouldChunkUnion = (typeNode: Node): boolean => { + if (!Node.isUnionTypeNode(typeNode)) { + return false + } + return typeNode.getTypeNodes().length >= CHUNK_SIZE +} + +const createChunkNodes = ( + node: Node, + parentTypeName: string, + nodeGraph: NodeGraph, + maincodeNodeIds: Set, + requiredNodeIds: Set, + sourceFile: SourceFile, +): string[] => { + if (!Node.isUnionTypeNode(node)) { + return [] + } + + const typeNodes = node.getTypeNodes() + const chunks: Node[][] = [] + + // Create chunks of 20 items each + for (let i = 0; i < typeNodes.length; i += CHUNK_SIZE) { + chunks.push(typeNodes.slice(i, i + CHUNK_SIZE)) + } + + const chunkReferences: string[] = [] + + // Create chunk nodes + for (let i = 0; i < chunks.length; i++) { + const chunkName = `${parentTypeName}_Chunk${i + 1}` + const chunkQualifiedName = resolverStore.generateQualifiedName(chunkName, sourceFile) + + chunkReferences.push(chunkQualifiedName) + maincodeNodeIds.add(chunkQualifiedName) + requiredNodeIds.add(chunkQualifiedName) + + // Create a new union node with only the chunk's type nodes + const chunkTypeNodes = chunks[i]! + + // Create a synthetic union node for this chunk + const project = node.getProject() + const tempSourceFile = project.createSourceFile( + `__temp_chunk_${i}.ts`, + `type TempChunk = ${chunkTypeNodes.map((node) => node.getText()).join(' | ')}`, + ) + const tempTypeAlias = tempSourceFile.getTypeAliases()[0]! + const chunkTypeNode = tempTypeAlias.getTypeNode()! + + const chunkTraversedNode: TraversedNode = { + node: chunkTypeNode, // Use the chunk-specific union node + type: 'chunk', + originalName: chunkName, + qualifiedName: chunkQualifiedName, + isImported: false, + isMainCode: true, + isChunk: true, + chunkReferences: [], // Chunk nodes don't need references to other chunks + } + + nodeGraph.addTypeNode(chunkQualifiedName, chunkTraversedNode) + + // Add to ResolverStore + resolverStore.addTypeMapping({ + originalName: chunkName, + sourceFile, + }) + } + + return chunkReferences +} export const addLocalTypes = ( sourceFile: SourceFile, @@ -42,14 +118,40 @@ export const addLocalTypes = ( const qualifiedName = resolverStore.generateQualifiedName(typeName, typeAlias.getSourceFile()) maincodeNodeIds.add(qualifiedName) requiredNodeIds.add(qualifiedName) - nodeGraph.addTypeNode(qualifiedName, { - node: typeAlias, - type: 'typeAlias', - originalName: typeName, - qualifiedName, - isImported: false, - isMainCode: true, - }) + + // Check if this type alias contains a large union that needs chunking + const typeNode = typeAlias.getTypeNode() + if (typeNode && shouldChunkUnion(typeNode)) { + // Create chunk nodes for large union + const chunkReferences = createChunkNodes( + typeNode, + typeName, + nodeGraph, + maincodeNodeIds, + requiredNodeIds, + sourceFile, + ) + + // Add the main type as a regular type alias that will be handled by chunk parser + nodeGraph.addTypeNode(qualifiedName, { + node: typeAlias, + type: 'typeAlias', + originalName: typeName, + qualifiedName, + isImported: false, + isMainCode: true, + chunkReferences: chunkReferences, + }) + } else { + nodeGraph.addTypeNode(qualifiedName, { + node: typeAlias, + type: 'typeAlias', + originalName: typeName, + qualifiedName, + isImported: false, + isMainCode: true, + }) + } // Add to ResolverStore during traversal resolverStore.addTypeMapping({ diff --git a/src/traverse/types.ts b/src/traverse/types.ts index 596e568..f6db874 100644 --- a/src/traverse/types.ts +++ b/src/traverse/types.ts @@ -2,10 +2,13 @@ import type { Node } from 'ts-morph' export interface TraversedNode { node: Node - type: 'interface' | 'typeAlias' | 'enum' | 'function' + type: 'interface' | 'typeAlias' | 'enum' | 'function' | 'chunk' originalName: string qualifiedName: string isImported: boolean isMainCode: boolean aliasName?: string // The alias name used in import statements (e.g., 'UserType' for 'import { User as UserType }') + // Chunk-specific properties + isChunk?: boolean + chunkReferences?: string[] } diff --git a/src/utils/add-static-type-alias.ts b/src/utils/add-static-type-alias.ts deleted file mode 100644 index 27432ac..0000000 --- a/src/utils/add-static-type-alias.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SourceFile, ts } from 'ts-morph' - -export const addStaticTypeAlias = ( - newSourceFile: SourceFile, - name: string, - compilerNode: ts.SourceFile, - printer: ts.Printer, -) => { - const staticTypeNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Static'), [ - ts.factory.createTypeQueryNode(ts.factory.createIdentifier(name)), - ]) - - const staticType = printer.printNode(ts.EmitHint.Unspecified, staticTypeNode, compilerNode) - - newSourceFile.addTypeAlias({ - isExported: true, - name, - type: staticType, - }) -} diff --git a/src/utils/generic-type-utils.ts b/src/utils/generic-type-utils.ts index 0547981..61626d8 100644 --- a/src/utils/generic-type-utils.ts +++ b/src/utils/generic-type-utils.ts @@ -11,9 +11,10 @@ export class GenericTypeUtils { newSourceFile: SourceFile, name: string, initializer: string, + isExported: boolean = true, ): void { newSourceFile.addVariableStatement({ - isExported: true, + isExported, declarationKind: VariableDeclarationKind.Const, declarations: [ { @@ -146,4 +147,35 @@ export class GenericTypeUtils { type: staticType, }) } + + static addStaticTypeAlias = ( + newSourceFile: SourceFile, + name: string, + compilerNode: ts.SourceFile, + printer: ts.Printer, + ) => { + const staticTypeNode = ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('Static'), + [ts.factory.createTypeQueryNode(ts.factory.createIdentifier(name))], + ) + + const staticType = printer.printNode(ts.EmitHint.Unspecified, staticTypeNode, compilerNode) + + newSourceFile.addTypeAlias({ + isExported: true, + name, + type: staticType, + }) + } + + static makeTypeCall(method: string, args: ts.Expression[] = []) { + return ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('Type'), + ts.factory.createIdentifier(method), + ), + undefined, + args, + ) + } } diff --git a/src/utils/key-extraction-utils.ts b/src/utils/key-extraction-utils.ts index 94d896f..acca77f 100644 --- a/src/utils/key-extraction-utils.ts +++ b/src/utils/key-extraction-utils.ts @@ -1,4 +1,4 @@ -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, ts } from 'ts-morph' /** @@ -33,12 +33,14 @@ export const extractStringKeys = (keysType: Node): string[] => { */ export const createTypeBoxKeys = (keys: string[]): ts.Expression => { if (keys.length === 1) { - return makeTypeCall('Literal', [ts.factory.createStringLiteral(keys[0]!)]) + return GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createStringLiteral(keys[0]!)]) } - return makeTypeCall('Union', [ + return GenericTypeUtils.makeTypeCall('Union', [ ts.factory.createArrayLiteralExpression( - keys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])), + keys.map((k) => + GenericTypeUtils.makeTypeCall('Literal', [ts.factory.createStringLiteral(k)]), + ), ), ]) } diff --git a/src/utils/typebox-call.ts b/src/utils/typebox-call.ts index d4e558d..34eb957 100644 --- a/src/utils/typebox-call.ts +++ b/src/utils/typebox-call.ts @@ -1,15 +1,20 @@ import { TypeBoxTypeHandlers } from '@daxserver/validation-schema-codegen/handlers/typebox/typebox-type-handlers' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils' import { Node, ts } from 'ts-morph' let handlers: TypeBoxTypeHandlers | null = null -export const getTypeBoxType = (node?: Node): ts.Expression => { - if (!node) return makeTypeCall('Any') +export interface TypeBoxContext { + nodeGraph: NodeGraph +} + +export const getTypeBoxType = (node?: Node, context?: TypeBoxContext): ts.Expression => { + if (!node || !context) return GenericTypeUtils.makeTypeCall('Any') if (!handlers) { handlers = new TypeBoxTypeHandlers() } - return handlers.getHandler(node).handle(node) + return handlers.getHandler(node).handle(node, context) } diff --git a/src/utils/typebox-codegen-utils.ts b/src/utils/typebox-codegen-utils.ts deleted file mode 100644 index 38c65e9..0000000 --- a/src/utils/typebox-codegen-utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ts } from 'ts-morph' - -export const makeTypeCall = (method: string, args: ts.Expression[] = []) => { - return ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('Type'), - ts.factory.createIdentifier(method), - ), - undefined, - args, - ) -} diff --git a/tests/integration/deep-instantiation-integration.test.ts b/tests/integration/deep-instantiation-integration.test.ts new file mode 100644 index 0000000..59e3f66 --- /dev/null +++ b/tests/integration/deep-instantiation-integration.test.ts @@ -0,0 +1,361 @@ +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Deep Instantiation Integration', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + test('should generate code without deep Instantiation', () => { + const sourceFile = createSourceFile( + project, + ` + type A = 'aa' | 'ab' | 'ac' | 'ad' | 'ae' | 'af' | 'ag' | 'ah' | 'ai' | 'aj' + type B = Record + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier( + ` + export const A = Type.Union([ + Type.Literal('aa'), + Type.Literal('ab'), + Type.Literal('ac'), + Type.Literal('ad'), + Type.Literal('ae'), + Type.Literal('af'), + Type.Literal('ag'), + Type.Literal('ah'), + Type.Literal('ai'), + Type.Literal('aj'), + ]); + + export type A = Static + + export const B = Type.Record(A, Type.String()) + + export type B = Static + `, + ), + ) + }) + + test('should generate code with deep instantiation 1', () => { + const sourceFile = createSourceFile( + project, + ` + type A = 'aa' | 'ab' | 'ac' | 'ad' | 'ae' | 'af' | 'ag' | 'ah' | 'ai' | 'aj' + | 'ak' | 'al' | 'am' | 'an' | 'ao' | 'ap' | 'aq' | 'ar' | 'as' | 'at' + | 'au' | 'av' | 'aw' | 'ax' | 'ay' | 'az' | 'ba' | 'bb' | 'bc' | 'bd' + | 'be' | 'bf' | 'bg' | 'bh' | 'bi' | 'bj' | 'bk' | 'bl' | 'bm' | 'bn' + | 'bo' | 'bp' | 'bq' | 'br' | 'bs' | 'bt' | 'bu' | 'bv' | 'bw' | 'bx' + | 'by' | 'bz' | 'ca' | 'cb' | 'cc' | 'cd' | 'ce' | 'cf' | 'cg' | 'ch' + | 'ci' | 'cj' | 'ck' | 'cl' | 'cm' | 'cn' | 'co' | 'cp' | 'cq' | 'cr' + | 'cs' | 'ct' | 'cu' | 'cv' | 'cw' | 'cx' | 'cy' | 'cz' + + type B = Partial> + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier( + ` + const A_Chunk1 = Type.Union([ + Type.Literal('aa'), + Type.Literal('ab'), + Type.Literal('ac'), + Type.Literal('ad'), + Type.Literal('ae'), + Type.Literal('af'), + Type.Literal('ag'), + Type.Literal('ah'), + Type.Literal('ai'), + Type.Literal('aj'), + Type.Literal('ak'), + Type.Literal('al'), + Type.Literal('am'), + Type.Literal('an'), + Type.Literal('ao'), + Type.Literal('ap'), + Type.Literal('aq'), + Type.Literal('ar'), + Type.Literal('as'), + Type.Literal('at'), + ]); + const A_Chunk2 = Type.Union([ + Type.Literal('au'), + Type.Literal('av'), + Type.Literal('aw'), + Type.Literal('ax'), + Type.Literal('ay'), + Type.Literal('az'), + Type.Literal('ba'), + Type.Literal('bb'), + Type.Literal('bc'), + Type.Literal('bd'), + Type.Literal('be'), + Type.Literal('bf'), + Type.Literal('bg'), + Type.Literal('bh'), + Type.Literal('bi'), + Type.Literal('bj'), + Type.Literal('bk'), + Type.Literal('bl'), + Type.Literal('bm'), + Type.Literal('bn'), + ]); + const A_Chunk3 = Type.Union([ + Type.Literal('bo'), + Type.Literal('bp'), + Type.Literal('bq'), + Type.Literal('br'), + Type.Literal('bs'), + Type.Literal('bt'), + Type.Literal('bu'), + Type.Literal('bv'), + Type.Literal('bw'), + Type.Literal('bx'), + Type.Literal('by'), + Type.Literal('bz'), + Type.Literal('ca'), + Type.Literal('cb'), + Type.Literal('cc'), + Type.Literal('cd'), + Type.Literal('ce'), + Type.Literal('cf'), + Type.Literal('cg'), + Type.Literal('ch'), + ]); + const A_Chunk4 = Type.Union([ + Type.Literal('ci'), + Type.Literal('cj'), + Type.Literal('ck'), + Type.Literal('cl'), + Type.Literal('cm'), + Type.Literal('cn'), + Type.Literal('co'), + Type.Literal('cp'), + Type.Literal('cq'), + Type.Literal('cr'), + Type.Literal('cs'), + Type.Literal('ct'), + Type.Literal('cu'), + Type.Literal('cv'), + Type.Literal('cw'), + Type.Literal('cx'), + Type.Literal('cy'), + Type.Literal('cz'), + ]); + export const A = Type.Union([ + A_Chunk1, + A_Chunk2, + A_Chunk3, + A_Chunk4, + ]); + + export type A = Static; + + export const B = Type.Partial( + Type.Intersect([ + Type.Record(A_Chunk1, Type.String()), + Type.Record(A_Chunk2, Type.String()), + Type.Record(A_Chunk3, Type.String()), + Type.Record(A_Chunk4, Type.String()), + ]), + ); + + export type B = Static; + `, + ), + ) + }) + + test('should generate code with deep instantiation 2', () => { + const sourceFile = createSourceFile( + project, + ` + type A = 'aa' | 'ab' | 'ac' | 'ad' | 'ae' | 'af' | 'ag' | 'ah' | 'ai' | 'aj' + | 'ak' | 'al' | 'am' | 'an' | 'ao' | 'ap' | 'aq' | 'ar' | 'as' | 'at' + | 'au' | 'av' | 'aw' | 'ax' | 'ay' | 'az' | 'ba' | 'bb' | 'bc' | 'bd' + | 'be' | 'bf' | 'bg' | 'bh' | 'bi' | 'bj' | 'bk' | 'bl' | 'bm' | 'bn' + | 'bo' | 'bp' | 'bq' | 'br' | 'bs' | 'bt' | 'bu' | 'bv' | 'bw' | 'bx' + | 'by' | 'bz' | 'ca' | 'cb' | 'cc' | 'cd' | 'ce' | 'cf' | 'cg' | 'ch' + | 'ci' | 'cj' | 'ck' | 'cl' | 'cm' | 'cn' | 'co' | 'cp' | 'cq' | 'cr' + | 'cs' | 'ct' | 'cu' | 'cv' | 'cw' | 'cx' | 'cy' | 'cz' | number + + type B = Record + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier( + ` + const A_Chunk1 = Type.Union([ + Type.Literal('aa'), + Type.Literal('ab'), + Type.Literal('ac'), + Type.Literal('ad'), + Type.Literal('ae'), + Type.Literal('af'), + Type.Literal('ag'), + Type.Literal('ah'), + Type.Literal('ai'), + Type.Literal('aj'), + Type.Literal('ak'), + Type.Literal('al'), + Type.Literal('am'), + Type.Literal('an'), + Type.Literal('ao'), + Type.Literal('ap'), + Type.Literal('aq'), + Type.Literal('ar'), + Type.Literal('as'), + Type.Literal('at'), + ]); + const A_Chunk2 = Type.Union([ + Type.Literal('au'), + Type.Literal('av'), + Type.Literal('aw'), + Type.Literal('ax'), + Type.Literal('ay'), + Type.Literal('az'), + Type.Literal('ba'), + Type.Literal('bb'), + Type.Literal('bc'), + Type.Literal('bd'), + Type.Literal('be'), + Type.Literal('bf'), + Type.Literal('bg'), + Type.Literal('bh'), + Type.Literal('bi'), + Type.Literal('bj'), + Type.Literal('bk'), + Type.Literal('bl'), + Type.Literal('bm'), + Type.Literal('bn'), + ]); + const A_Chunk3 = Type.Union([ + Type.Literal('bo'), + Type.Literal('bp'), + Type.Literal('bq'), + Type.Literal('br'), + Type.Literal('bs'), + Type.Literal('bt'), + Type.Literal('bu'), + Type.Literal('bv'), + Type.Literal('bw'), + Type.Literal('bx'), + Type.Literal('by'), + Type.Literal('bz'), + Type.Literal('ca'), + Type.Literal('cb'), + Type.Literal('cc'), + Type.Literal('cd'), + Type.Literal('ce'), + Type.Literal('cf'), + Type.Literal('cg'), + Type.Literal('ch'), + ]); + const A_Chunk4 = Type.Union([ + Type.Literal('ci'), + Type.Literal('cj'), + Type.Literal('ck'), + Type.Literal('cl'), + Type.Literal('cm'), + Type.Literal('cn'), + Type.Literal('co'), + Type.Literal('cp'), + Type.Literal('cq'), + Type.Literal('cr'), + Type.Literal('cs'), + Type.Literal('ct'), + Type.Literal('cu'), + Type.Literal('cv'), + Type.Literal('cw'), + Type.Literal('cx'), + Type.Literal('cy'), + Type.Literal('cz'), + Type.Number(), + ]); + export const A = Type.Union([ + A_Chunk1, + A_Chunk2, + A_Chunk3, + A_Chunk4, + ]); + + export type A = Static; + + export const B = Type.Intersect([ + Type.Record(A_Chunk1, Type.String()), + Type.Record(A_Chunk2, Type.String()), + Type.Record(A_Chunk3, Type.String()), + Type.Record(A_Chunk4, Type.String()), + ]); + + export type B = Static; + `, + ), + ) + }) + + test('should generate code with deep instantiation 3', () => { + const sourceFile = createSourceFile( + project, + ` + type A = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 + | string + + type B = Record + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier( + ` + const A_Chunk1 = Type.Union([ + Type.Literal(1), + Type.Literal(2), + Type.Literal(3), + Type.Literal(4), + Type.Literal(5), + Type.Literal(6), + Type.Literal(7), + Type.Literal(8), + Type.Literal(9), + Type.Literal(10), + Type.Literal(11), + Type.Literal(12), + Type.Literal(13), + Type.Literal(14), + Type.Literal(15), + Type.Literal(16), + Type.Literal(17), + Type.Literal(18), + Type.Literal(19), + Type.Literal(20), + ]); + const A_Chunk2 = Type.String(); + export const A = Type.Union([ + A_Chunk1, + A_Chunk2, + ]); + + export type A = Static; + + export const B = Type.Intersect([ + Type.Record(A_Chunk1, Type.String()), + Type.Record(A_Chunk2, Type.String()), + ]); + + export type B = Static; + `, + ), + ) + }) +}) diff --git a/tsconfig.build.json b/tsconfig.build.json index 71db777..6f58b8b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -13,14 +13,6 @@ "allowImportingTsExtensions": false, "verbatimModuleSyntax": false }, - "include": [ - "src/**/*" - ], - "exclude": [ - "tests/**/*", - "**/*.test.ts", - "**/*.spec.ts", - "dist", - "node_modules" - ] + "include": ["src/**/*"], + "exclude": ["tests/**/*", "**/*.test.ts", "**/*.spec.ts", "dist", "node_modules"] }