diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bda861c..575ec4c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -33,7 +33,7 @@ - [TypeBoxTypeHandlers Optimizations](#typeboxtypehandlers-optimizations) - [Performance Testing](#performance-testing) - [Process Overview](#process-overview) -- [Test-Driven Development (TDD) Approach](#test-driven-development-tdd-approach) +- [Test-Driven Development](#test-driven-development) - [TDD Cycle](#tdd-cycle) - [Running Tests](#running-tests) - [TDD Workflow for New Features](#tdd-workflow-for-new-features) @@ -77,7 +77,7 @@ The code generation process includes sophisticated import resolution and depende #### DependencyCollector -The module implements a `DependencyCollector` class that: +The module implements a `DependencyCollector` class that: - **Traverses Import Chains**: Recursively follows import declarations to collect all type dependencies from external files - **Builds Dependency Graph**: Creates a comprehensive map of type dependencies, tracking which types depend on which other types @@ -113,11 +113,15 @@ The codebase provides comprehensive support for TypeScript interface inheritance ### Dependency-Ordered Processing -Interfaces are processed in dependency order using a topological sort algorithm implemented in : +The main codegen logic in implements sophisticated processing order management: -1. **Dependency Analysis**: The `getInterfaceProcessingOrder` function analyzes all interfaces to identify inheritance relationships -2. **Topological Sorting**: Interfaces are sorted to ensure base interfaces are processed before extended interfaces -3. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully +1. **Dependency Analysis**: Uses `InterfaceTypeDependencyAnalyzer` to analyze complex relationships between interfaces and type aliases +2. **Conditional Processing**: Handles three scenarios: + - Interfaces depending on type aliases only + - Type aliases depending on interfaces only + - Both dependencies present (three-phase processing) +3. **Topological Sorting**: Ensures types are processed in correct dependency order to prevent "type not found" errors +4. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully ### TypeBox Composite Generation @@ -126,11 +130,13 @@ Interface inheritance is implemented using TypeBox's `Type.Composite` functional - **Base Interface Reference**: Extended interfaces reference their base interfaces by name as identifiers - **Property Combination**: The `InterfaceTypeHandler` generates `Type.Composite([BaseInterface, Type.Object({...})])` for extended interfaces - **Type Safety**: Generated code maintains full TypeScript type safety through proper static type aliases +- **Generic Type Parameter Handling**: Uses `TSchema` as the constraint for TypeBox compatibility instead of preserving original TypeScript constraints ### Implementation Details - **Heritage Clause Processing**: The processes `extends` clauses by extracting referenced type names - **Identifier Generation**: Base interface references are converted to TypeScript identifiers rather than attempting recursive type resolution +- **TypeBox Constraint Normalization**: Generic type parameters use `TSchema` constraints for TypeBox schema compatibility - **Error Prevention**: The dependency ordering prevents "No handler found for type" errors that occur when extended interfaces are processed before their base interfaces ## Input Handling System @@ -341,9 +347,9 @@ To ensure the dependency collection system performs efficiently under various sc 5. **Static Type Generation**: Alongside each TypeBox schema, a TypeScript `type` alias is generated using `Static` to provide compile-time type safety and seamless integration with existing TypeScript code. 6. **Output**: A new TypeScript file (as a string) containing the generated TypeBox schemas and static type aliases, ready to be written to disk or integrated into your application. -## Test-Driven Development (TDD) Approach +## Test-Driven Development -This project follows a Test-Driven Development methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases: +This project follows a Test-Driven Development (TDD) methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases: ### TDD Cycle diff --git a/src/handlers/typebox/object/interface-type-handler.ts b/src/handlers/typebox/object/interface-type-handler.ts index 91eae19..8603c57 100644 --- a/src/handlers/typebox/object/interface-type-handler.ts +++ b/src/handlers/typebox/object/interface-type-handler.ts @@ -69,10 +69,13 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { const functionTypeParams = typeParameters.map((typeParam) => { const paramName = typeParam.getName() + // Use TSchema as the constraint for TypeBox compatibility + const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined) + return ts.factory.createTypeParameterDeclaration( undefined, ts.factory.createIdentifier(paramName), - ts.factory.createTypeReferenceNode('TSchema', undefined), + constraintNode, undefined, ) }) diff --git a/src/handlers/typebox/type-reference-handler.ts b/src/handlers/typebox/type-reference-handler.ts index e264b24..91d8a11 100644 --- a/src/handlers/typebox/type-reference-handler.ts +++ b/src/handlers/typebox/type-reference-handler.ts @@ -1,4 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-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, TypeReferenceNode } from 'ts-morph' @@ -9,9 +10,23 @@ export class TypeReferenceHandler extends BaseTypeHandler { handle(node: TypeReferenceNode): ts.Expression { const referencedType = node.getTypeName() + const typeArguments = node.getTypeArguments() if (Node.isIdentifier(referencedType)) { const typeName = referencedType.getText() + + // If there are type arguments, create a function call + if (typeArguments.length > 0) { + const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg)) + + return ts.factory.createCallExpression( + ts.factory.createIdentifier(typeName), + undefined, + typeBoxArgs, + ) + } + + // No type arguments, just return the identifier return ts.factory.createIdentifier(typeName) } diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts index a6360a3..d2a2a9c 100644 --- a/src/parsers/parse-interfaces.ts +++ b/src/parsers/parse-interfaces.ts @@ -107,10 +107,13 @@ export class InterfaceParser extends BaseParser { // Create type parameters for the type alias const typeParamDeclarations = typeParameters.map((typeParam) => { const paramName = typeParam.getName() + // Use TSchema as the constraint for TypeBox compatibility + const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined) + return ts.factory.createTypeParameterDeclaration( undefined, ts.factory.createIdentifier(paramName), - ts.factory.createTypeReferenceNode('TSchema', undefined), + constraintNode, undefined, ) }) diff --git a/src/traverse/ast-traversal.ts b/src/traverse/ast-traversal.ts new file mode 100644 index 0000000..83213fe --- /dev/null +++ b/src/traverse/ast-traversal.ts @@ -0,0 +1,111 @@ +import { InterfaceDeclaration, Node, TypeAliasDeclaration, TypeReferenceNode } from 'ts-morph' + +/** + * AST traversal patterns used in dependency analysis + */ +export class ASTTraversal { + private static cache = new Map() + + /** + * Extract interface names referenced by a type alias + */ + static extractInterfaceReferences( + typeAlias: TypeAliasDeclaration, + interfaces: Map, + ): string[] { + const typeNode = typeAlias.getTypeNode() + if (!typeNode) return [] + + const cacheKey = `interface_refs_${typeNode.getText()}` + const cached = ASTTraversal.cache.get(cacheKey) + if (cached) return cached + + const references: string[] = [] + const visited = new Set() + + const traverse = (node: Node): void => { + if (visited.has(node)) return + visited.add(node) + + // Handle type references + if (Node.isTypeReference(node)) { + const typeRefNode = node as TypeReferenceNode + const typeName = typeRefNode.getTypeName().getText() + + if (interfaces.has(typeName)) { + references.push(typeName) + } + // Continue traversing to handle type arguments in generic instantiations + } + + // Use forEachChild for better performance + node.forEachChild(traverse) + } + + traverse(typeNode) + + // Cache the result + ASTTraversal.cache.set(cacheKey, references) + return references + } + + /** + * Extract type alias names referenced by an interface (e.g., in type parameter constraints) + */ + static extractTypeAliasReferences( + interfaceDecl: InterfaceDeclaration, + typeAliases: Map, + ): string[] { + const cacheKey = `type_alias_refs_${interfaceDecl.getName()}_${interfaceDecl.getText()}` + const cached = ASTTraversal.cache.get(cacheKey) + if (cached) return cached + + const references: string[] = [] + const visited = new Set() + + const traverse = (node: Node): void => { + if (visited.has(node)) return + visited.add(node) + + // Handle type references + if (Node.isTypeReference(node)) { + const typeRefNode = node as TypeReferenceNode + const typeName = typeRefNode.getTypeName().getText() + + if (typeAliases.has(typeName)) { + references.push(typeName) + } + return // No need to traverse children of type references + } + + // Use forEachChild for better performance + node.forEachChild(traverse) + } + + // Check type parameters for constraints + for (const typeParam of interfaceDecl.getTypeParameters()) { + const constraint = typeParam.getConstraint() + if (constraint) { + traverse(constraint) + } + } + + // Check heritage clauses + for (const heritageClause of interfaceDecl.getHeritageClauses()) { + for (const typeNode of heritageClause.getTypeNodes()) { + traverse(typeNode) + } + } + + // Cache the result + ASTTraversal.cache.set(cacheKey, references) + return references + } + + /** + * Clear the internal cache + */ + static clearCache(): void { + ASTTraversal.cache.clear() + } +} diff --git a/src/traverse/dependency-analyzer.ts b/src/traverse/dependency-analyzer.ts new file mode 100644 index 0000000..951ed0f --- /dev/null +++ b/src/traverse/dependency-analyzer.ts @@ -0,0 +1,140 @@ +import { ASTTraversal } from '@daxserver/validation-schema-codegen/traverse/ast-traversal' +import { InterfaceDeclaration, TypeAliasDeclaration } from 'ts-morph' + +/** + * Dependency analyzer for determining processing order + */ +export class DependencyAnalyzer { + /** + * Extract interface names referenced by a type alias + */ + extractInterfaceReferences( + typeAlias: TypeAliasDeclaration, + interfaces: Map, + ): string[] { + return ASTTraversal.extractInterfaceReferences(typeAlias, interfaces) + } + + /** + * Extract type alias names referenced by an interface + */ + extractTypeAliasReferences( + interfaceDecl: InterfaceDeclaration, + typeAliases: Map, + ): string[] { + return ASTTraversal.extractTypeAliasReferences(interfaceDecl, typeAliases) + } + + /** + * Check if any type aliases reference interfaces + */ + hasInterfaceReferences( + typeAliases: TypeAliasDeclaration[], + interfaces: InterfaceDeclaration[], + ): boolean { + const interfaceMap = new Map() + for (const iface of interfaces) { + interfaceMap.set(iface.getName(), iface) + } + + for (const typeAlias of typeAliases) { + const references = this.extractInterfaceReferences(typeAlias, interfaceMap) + if (references.length > 0) { + return true + } + } + + return false + } + + /** + * Get type aliases that reference interfaces, ordered by their dependencies + */ + getTypeAliasesReferencingInterfaces( + typeAliases: TypeAliasDeclaration[], + interfaces: InterfaceDeclaration[], + ): { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] { + const interfaceMap = new Map() + for (const iface of interfaces) { + interfaceMap.set(iface.getName(), iface) + } + + const result: { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] = [] + + for (const typeAlias of typeAliases) { + const references = this.extractInterfaceReferences(typeAlias, interfaceMap) + if (references.length > 0) { + result.push({ + typeAlias, + referencedInterfaces: references, + }) + } + } + + return result + } + + /** + * Determine the correct processing order for interfaces and type aliases + * Returns an object indicating which should be processed first + */ + analyzeProcessingOrder( + typeAliases: TypeAliasDeclaration[], + interfaces: InterfaceDeclaration[], + ): { + processInterfacesFirst: boolean + typeAliasesDependingOnInterfaces: string[] + interfacesDependingOnTypeAliases: string[] + } { + const typeAliasMap = new Map() + const interfaceMap = new Map() + + for (const typeAlias of typeAliases) { + typeAliasMap.set(typeAlias.getName(), typeAlias) + } + + for (const interfaceDecl of interfaces) { + interfaceMap.set(interfaceDecl.getName(), interfaceDecl) + } + + const typeAliasesDependingOnInterfaces: string[] = [] + const interfacesDependingOnTypeAliases: string[] = [] + + // Check type aliases that depend on interfaces + for (const typeAlias of typeAliases) { + const interfaceRefs = this.extractInterfaceReferences(typeAlias, interfaceMap) + if (interfaceRefs.length > 0) { + typeAliasesDependingOnInterfaces.push(typeAlias.getName()) + } + } + + // Check interfaces that depend on type aliases + for (const interfaceDecl of interfaces) { + const typeAliasRefs = this.extractTypeAliasReferences(interfaceDecl, typeAliasMap) + if (typeAliasRefs.length > 0) { + interfacesDependingOnTypeAliases.push(interfaceDecl.getName()) + } + } + + // Determine processing order: + // If interfaces depend on type aliases, process type aliases first + // If only type aliases depend on interfaces, process interfaces first + // If both have dependencies, process type aliases that interfaces depend on first, + // then interfaces, then type aliases that depend on interfaces + const processInterfacesFirst = + interfacesDependingOnTypeAliases.length === 0 && typeAliasesDependingOnInterfaces.length > 0 + + return { + processInterfacesFirst, + typeAliasesDependingOnInterfaces, + interfacesDependingOnTypeAliases, + } + } + + /** + * Clear internal caches + */ + clearCache(): void { + ASTTraversal.clearCache() + } +} diff --git a/src/utils/dependency-collector.ts b/src/traverse/dependency-collector.ts similarity index 97% rename from src/utils/dependency-collector.ts rename to src/traverse/dependency-collector.ts index 24648ba..b3b0668 100644 --- a/src/utils/dependency-collector.ts +++ b/src/traverse/dependency-collector.ts @@ -1,11 +1,11 @@ import { DefaultFileResolver, type FileResolver, -} from '@daxserver/validation-schema-codegen/utils/dependency-file-resolver' +} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver' import { DefaultTypeReferenceExtractor, type TypeReferenceExtractor, -} from '@daxserver/validation-schema-codegen/utils/dependency-type' +} from '@daxserver/validation-schema-codegen/traverse/dependency-type' import { ImportDeclaration, SourceFile, TypeAliasDeclaration } from 'ts-morph' export interface TypeDependency { diff --git a/src/utils/dependency-file-resolver.ts b/src/traverse/dependency-file-resolver.ts similarity index 100% rename from src/utils/dependency-file-resolver.ts rename to src/traverse/dependency-file-resolver.ts diff --git a/src/utils/dependency-type.ts b/src/traverse/dependency-type.ts similarity index 97% rename from src/utils/dependency-type.ts rename to src/traverse/dependency-type.ts index 29ef00b..0bc1fb4 100644 --- a/src/utils/dependency-type.ts +++ b/src/traverse/dependency-type.ts @@ -1,4 +1,4 @@ -import type { TypeDependency } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import type { TypeDependency } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { Node, TypeReferenceNode } from 'ts-morph' export interface TypeReferenceExtractor { diff --git a/src/ts-morph-codegen.ts b/src/ts-morph-codegen.ts index 8105a5c..f6611ef 100644 --- a/src/ts-morph-codegen.ts +++ b/src/ts-morph-codegen.ts @@ -6,7 +6,8 @@ import { EnumParser } from '@daxserver/validation-schema-codegen/parsers/parse-e import { FunctionDeclarationParser } from '@daxserver/validation-schema-codegen/parsers/parse-function-declarations' import { InterfaceParser } from '@daxserver/validation-schema-codegen/parsers/parse-interfaces' import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases' -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyAnalyzer } from '@daxserver/validation-schema-codegen/traverse/dependency-analyzer' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { getInterfaceProcessingOrder } from '@daxserver/validation-schema-codegen/utils/interface-processing-order' import { Project, ts } from 'ts-morph' @@ -68,10 +69,38 @@ export const generateCode = async ({ const interfaceParser = new InterfaceParser(parserOptions) const functionDeclarationParser = new FunctionDeclarationParser(parserOptions) const dependencyCollector = new DependencyCollector() + const dependencyAnalyzer = new DependencyAnalyzer() // Collect all dependencies in correct order const importDeclarations = sourceFile.getImportDeclarations() const localTypeAliases = sourceFile.getTypeAliases() + const interfaces = sourceFile.getInterfaces() + + // Analyze cross-dependencies between interfaces and type aliases + const dependencyAnalysis = dependencyAnalyzer.analyzeProcessingOrder(localTypeAliases, interfaces) + + // Handle different dependency scenarios: + // 1. If interfaces depend on type aliases, process those type aliases first + // 2. If only type aliases depend on interfaces, process interfaces first + // 3. If both scenarios exist, process in order: type aliases interfaces depend on -> interfaces -> type aliases that depend on interfaces + + const hasInterfacesDependingOnTypeAliases = + dependencyAnalysis.interfacesDependingOnTypeAliases.length > 0 + const hasTypeAliasesDependingOnInterfaces = + dependencyAnalysis.typeAliasesDependingOnInterfaces.length > 0 + + if (hasInterfacesDependingOnTypeAliases && !hasTypeAliasesDependingOnInterfaces) { + // Case 1: Only interfaces depend on type aliases - process type aliases first (normal order) + // This will be handled by the normal dependency collection below + } else if (!hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 2: Only type aliases depend on interfaces - process interfaces first + getInterfaceProcessingOrder(interfaces).forEach((i) => { + interfaceParser.parse(i) + }) + } else if (hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 3: Both dependencies exist - process type aliases that interfaces depend on first + // This will be handled by the normal dependency collection below, then interfaces, then remaining type aliases + } // Always add local types first so they can be included in topological sort dependencyCollector.addLocalTypes(localTypeAliases, sourceFile) @@ -81,20 +110,57 @@ export const generateCode = async ({ exportEverything, ) - // Process all dependencies (both imported and local) in topological order - orderedDependencies.forEach((dependency) => { - if (!processedTypes.has(dependency.typeAlias.getName())) { - typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) - } - }) - - // Process any remaining local types that weren't included in the dependency graph - if (exportEverything) { - localTypeAliases.forEach((typeAlias) => { - if (!processedTypes.has(typeAlias.getName())) { - typeAliasParser.parseWithImportFlag(typeAlias, false) + if (!hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 2: Only process type aliases that don't depend on interfaces + orderedDependencies.forEach((dependency) => { + const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes( + dependency.typeAlias.getName(), + ) + if (!dependsOnInterface && !processedTypes.has(dependency.typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) } }) + } else if (hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 3: Process only type aliases that interfaces depend on (phase 1) + orderedDependencies.forEach((dependency) => { + const interfaceDependsOnThis = dependencyAnalysis.interfacesDependingOnTypeAliases.some( + (interfaceName) => { + const interfaceDecl = interfaces.find((i) => i.getName() === interfaceName) + if (!interfaceDecl) return false + const typeAliasRefs = dependencyAnalyzer.extractTypeAliasReferences( + interfaceDecl, + new Map(localTypeAliases.map((ta) => [ta.getName(), ta])), + ) + return typeAliasRefs.includes(dependency.typeAlias.getName()) + }, + ) + const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes( + dependency.typeAlias.getName(), + ) + if ( + interfaceDependsOnThis && + !dependsOnInterface && + !processedTypes.has(dependency.typeAlias.getName()) + ) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) + } + }) + } else { + // Case 1: Process all dependencies (both imported and local) in topological order + orderedDependencies.forEach((dependency) => { + if (!processedTypes.has(dependency.typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) + } + }) + + // Process any remaining local types that weren't included in the dependency graph + if (exportEverything) { + localTypeAliases.forEach((typeAlias) => { + if (!processedTypes.has(typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(typeAlias, false) + } + }) + } } // Process enums @@ -103,9 +169,37 @@ export const generateCode = async ({ }) // Process interfaces in dependency order - getInterfaceProcessingOrder(sourceFile.getInterfaces()).forEach((i) => { - interfaceParser.parse(i) - }) + if ( + hasInterfacesDependingOnTypeAliases || + (!hasInterfacesDependingOnTypeAliases && !hasTypeAliasesDependingOnInterfaces) + ) { + // Case 1 and Case 3: Process interfaces after type aliases they depend on + getInterfaceProcessingOrder(interfaces).forEach((i) => { + interfaceParser.parse(i) + }) + } + // Case 2: Interfaces were already processed above + + // Process remaining type aliases that depend on interfaces (Case 2 and Case 3) + if (hasTypeAliasesDependingOnInterfaces) { + orderedDependencies.forEach((dependency) => { + const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes( + dependency.typeAlias.getName(), + ) + if (dependsOnInterface && !processedTypes.has(dependency.typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) + } + }) + + // Process any remaining local types that weren't included in the dependency graph + if (exportEverything) { + localTypeAliases.forEach((typeAlias) => { + if (!processedTypes.has(typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(typeAlias, false) + } + }) + } + } // Process function declarations sourceFile.getFunctions().forEach((f) => { diff --git a/tests/dependency-collector.integration.test.ts b/tests/dependency-collector.integration.test.ts index 1a8bcab..9132d3d 100644 --- a/tests/dependency-collector.integration.test.ts +++ b/tests/dependency-collector.integration.test.ts @@ -1,4 +1,4 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { createSourceFile } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' diff --git a/tests/dependency-collector.performance.test.ts b/tests/dependency-collector.performance.test.ts index 44e8099..a702723 100644 --- a/tests/dependency-collector.performance.test.ts +++ b/tests/dependency-collector.performance.test.ts @@ -1,4 +1,4 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { createSourceFile } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' diff --git a/tests/dependency-collector.unit.test.ts b/tests/dependency-collector.unit.test.ts index 963146f..a25e525 100644 --- a/tests/dependency-collector.unit.test.ts +++ b/tests/dependency-collector.unit.test.ts @@ -1,12 +1,12 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { DefaultFileResolver, type FileResolver, -} from '@daxserver/validation-schema-codegen/utils/dependency-file-resolver' +} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver' import { DefaultTypeReferenceExtractor, type TypeReferenceExtractor, -} from '@daxserver/validation-schema-codegen/utils/dependency-type' +} from '@daxserver/validation-schema-codegen/traverse/dependency-type' import { describe, expect, mock, test } from 'bun:test' import type { ImportDeclaration, SourceFile, TypeAliasDeclaration, TypeNode } from 'ts-morph' diff --git a/tests/handlers/typebox/interfaces.test.ts b/tests/handlers/typebox/interfaces.test.ts index 2be4b04..a8aa1aa 100644 --- a/tests/handlers/typebox/interfaces.test.ts +++ b/tests/handlers/typebox/interfaces.test.ts @@ -201,34 +201,105 @@ describe('Interfaces', () => { ) }) - test('generic types', () => { - const sourceFile = createSourceFile( - project, - ` - interface A { a: T } - interface B extends A { b: number } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier( + describe('generic types', () => { + test('generic types', () => { + const sourceFile = createSourceFile( + project, ` - const A = (T: T) => Type.Object({ - a: T - }); + interface A { a: T } + interface B extends A { b: number } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier( + ` + const A = (T: T) => Type.Object({ + a: T + }); + + type A = Static>>; + + const B = Type.Composite([A(Type.Number()), Type.Object({ + b: Type.Number() + })]); + + type B = Static; + `, + true, + true, + ), + ) + }) + + test('generic types extension', () => { + const sourceFile = createSourceFile( + project, + ` + interface A { a: T } + interface B extends A { b: T } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier( + ` + const A = (T: T) => Type.Object({ + a: T + }); + + type A = Static>>; + + const B = (T: T) => Type.Composite([A(T), Type.Object({ + b: T + })]); + + type B = Static>>; + `, + true, + true, + ), + ) + }) + + test('generic types with extended type', () => { + const sourceFile = createSourceFile( + project, + ` + declare const A: readonly ["a", "b"] + type A = typeof A[number] + interface B { a: T } + type C = B<'a'> + type D = B<'b'> + `, + ) - type A = Static>>; + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier( + ` + const A = Type.Union([Type.Literal('a'), Type.Literal('b')]) - const B = Type.Composite([A(Type.Number()), Type.Object({ - b: Type.Number() - })]); + type A = Static - type B = Static; - `, - true, - true, - ), - ) + const B = (T: T) => Type.Object({ + a: T + }) + + type B = Static>> + + const C = B(Type.Literal('a')) + + type C = Static + + const D = B(Type.Literal('b')) + + type D = Static + `, + true, + true, + ), + ) + }) }) }) }) diff --git a/tests/import-resolution.test.ts b/tests/import-resolution.test.ts index 173e343..a055bd8 100644 --- a/tests/import-resolution.test.ts +++ b/tests/import-resolution.test.ts @@ -1,4 +1,4 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph'