diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 575ec4c..22bdbc2 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -75,14 +75,17 @@ The main logic for code generation resides in the module implements a `DependencyCollector` class that:
+The module implements a unified `DependencyTraversal` class that exclusively uses Graphology for all dependency management:
+- **Graphology-Only Architecture**: Uses Graphology's DirectedGraph exclusively for all dependency tracking, eliminating Map-based and other tracking mechanisms
+- **Unified Data Management**: All dependency information is stored and managed through the Graphology graph structure with node attributes
+- **Topological Sorting**: Employs graphology-dag for robust dependency ordering with circular dependency detection and preference-based sorting
- **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
-- **Topological Sorting**: Performs topological sorting to ensure types are generated in the correct dependency order
- **Handles Multi-level Imports**: Supports complex scenarios with 3+ levels of nested imports (e.g., `TypeA` imports `TypeB` which imports `TypeC`)
+- **Graph-Based Caching**: Uses Graphology node attributes for caching type information and dependency relationships
+- **Export-Aware Ordering**: Provides specialized ordering logic for `exportEverything=false` scenarios to ensure proper dependency resolution
#### Key Features
@@ -93,19 +96,58 @@ The module provides comprehensive dependency management:
+
+- **Integrated AST Traversal**: Combines AST traversal logic with dependency collection for optimal performance
+- **Direct Graphology Usage**: Uses Graphology's DirectedGraph directly for dependency tracking without abstraction layers
+- **Unified Type Reference Extraction**: Consolidates type reference extraction logic within the main traversal module
+- **Export-Aware Processing**: Implements specialized logic for handling `exportEverything=false` scenarios with proper dependency ordering
+- **Performance Optimization**: Eliminates module boundaries and reduces function call overhead through unified architecture
+
+#### Graph-Based Dependency Resolution
+
+The unified module leverages Graphology's ecosystem for robust dependency management:
+
+- **DirectedGraph**: Uses Graphology's optimized graph data structure for dependency relationships
+- **Topological Sorting**: Employs `topologicalSort` from `graphology-dag` for dependency ordering with circular dependency detection
+- **Preference-Based Ordering**: Implements `getTopologicallySortedTypesWithPreference()` for export-aware type ordering
+- **Memory Efficiency**: Direct Graphology usage provides optimal memory management for large dependency graphs
+- **Type Safety**: Full TypeScript support through graphology-types package
+
+#### Simplified Architecture Benefits
+
+The Graphology-only approach provides several advantages:
+
+- **Simplified Architecture**: Eliminates multiple tracking mechanisms (Map-based dependencies, visitedFiles, various caches) in favor of a single graph-based solution
+- **Enhanced Performance**: Direct Graphology operations provide optimized graph algorithms and data structures
+- **Improved Maintainability**: Single dependency tracking mechanism reduces complexity and potential inconsistencies
+- **Better Memory Management**: Graphology's optimized memory handling for large dependency graphs
+- **Unified Data Model**: All dependency information stored consistently in graph nodes and edges
## Interface Inheritance Support
@@ -115,13 +157,13 @@ The codebase provides comprehensive support for TypeScript interface inheritance
The main codegen logic in implements sophisticated processing order management:
-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
+1. **Unified Dependency Analysis**: Uses with integrated graph-based architecture to analyze complex relationships between interfaces and type aliases
+2. **Direct Graph Processing**: Leverages Graphology's DirectedGraph and topological sorting for robust dependency ordering without abstraction layers
+3. **Export-Aware Processing**: Handles dependency ordering based on `exportEverything` flag:
+ - `exportEverything=true`: Prioritizes imported types for consistent ordering
+ - `exportEverything=false`: Ensures dependency-aware ordering while respecting local type preferences
+4. **Topological Sorting**: Uses `getTopologicallySortedTypesWithPreference()` to ensure types are processed in correct dependency order to prevent "type not found" errors
+5. **Circular Dependency Detection**: The graph-based algorithm detects and handles circular inheritance scenarios gracefully with detailed error reporting
### TypeBox Composite Generation
@@ -277,7 +319,25 @@ When implementing new type handlers or modifying existing ones, it is crucial to
## Performance Optimizations
-Several optimizations have been implemented to improve the performance of the code generation process, particularly for import resolution:
+Several optimizations have been implemented to improve the performance of the code generation process, particularly for import resolution and dependency management:
+
+### Unified Dependency Management with Graphology
+
+The project uses **Graphology** through a unified architecture for all dependency graph operations, providing:
+
+- **Production-Ready Graph Library**: Leverages Graphology's battle-tested graph data structures and algorithms
+- **Optimized Performance**: Benefits from Graphology's highly optimized internal implementations for graph operations
+- **Advanced Graph Algorithms**: Direct access to specialized algorithms through Graphology ecosystem (graphology-dag, graphology-traversal)
+- **Type Safety**: Full TypeScript support through graphology-types package
+- **Memory Efficiency**: Graphology's optimized memory management for large graphs
+- **Unified Architecture**: Single module eliminates abstraction layers and reduces complexity
+
+#### Core Architecture
+
+- **DependencyTraversal**: Uses Graphology's `DirectedGraph` exclusively for all dependency tracking, with no fallback to Map-based structures
+- **Integrated Topological Sorting**: Leverages `topologicalSort` from `graphology-dag` for ordering dependencies with export-aware preferences
+- **Graph-Based Data Storage**: All dependency information, visited files, and type metadata stored as Graphology node attributes
+- **Export-Aware Processing**: Implements specialized ordering logic for different export scenarios using graph-based algorithms
### TypeBox Type Handler Optimization
diff --git a/bun.lock b/bun.lock
index d9cf3c2..1a9b785 100644
--- a/bun.lock
+++ b/bun.lock
@@ -3,27 +3,33 @@
"workspaces": {
"": {
"name": "new-bun-project",
+ "dependencies": {
+ "graphology": "^0.26.0",
+ "graphology-dag": "^0.4.1",
+ "graphology-traversal": "^0.3.1",
+ },
"devDependencies": {
- "@eslint/js": "latest",
- "@prettier/sync": "latest",
- "@sinclair/typebox-codegen": "latest",
- "@types/bun": "latest",
- "@typescript-eslint/eslint-plugin": "latest",
- "@typescript-eslint/parser": "latest",
- "eslint": "latest",
- "eslint-config-prettier": "latest",
- "eslint-plugin-no-relative-import-paths": "latest",
- "eslint-plugin-prettier": "latest",
- "globals": "latest",
- "jiti": "latest",
- "prettier": "latest",
- "prettier-plugin-organize-imports": "latest",
- "ts-morph": "latest",
- "typescript-eslint": "latest",
- "wikibase-sdk": "latest",
+ "@eslint/js": "^9.34.0",
+ "@prettier/sync": "^0.6.1",
+ "@sinclair/typebox-codegen": "^0.11.1",
+ "@types/bun": "^1.2.21",
+ "@typescript-eslint/eslint-plugin": "^8.41.0",
+ "@typescript-eslint/parser": "^8.41.0",
+ "eslint": "^9.34.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-no-relative-import-paths": "^1.6.1",
+ "eslint-plugin-prettier": "^5.5.4",
+ "globals": "^16.3.0",
+ "graphology-types": "^0.24.8",
+ "jiti": "^2.5.1",
+ "prettier": "^3.6.2",
+ "prettier-plugin-organize-imports": "^4.2.0",
+ "ts-morph": "^26.0.0",
+ "typescript-eslint": "^8.41.0",
+ "wikibase-sdk": "^10.2.3",
},
"peerDependencies": {
- "typescript": "latest",
+ "typescript": "~5.9.2",
},
},
},
@@ -166,6 +172,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
@@ -194,6 +202,18 @@
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
+ "graphology": ["graphology@0.26.0", "", { "dependencies": { "events": "^3.3.0" }, "peerDependencies": { "graphology-types": ">=0.24.0" } }, "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg=="],
+
+ "graphology-dag": ["graphology-dag@0.4.1", "", { "dependencies": { "graphology-utils": "^2.4.1", "mnemonist": "^0.39.0" }, "peerDependencies": { "graphology-types": ">=0.19.0" } }, "sha512-3ch9oOAnHZDoT043vyg7ukmSkKJ505nFzaHaYOn0IF2PgGo5VtIavyVK4UpbIa4tli3hhGm1ZTdBsubTmaxu/w=="],
+
+ "graphology-indices": ["graphology-indices@0.17.0", "", { "dependencies": { "graphology-utils": "^2.4.2", "mnemonist": "^0.39.0" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ=="],
+
+ "graphology-traversal": ["graphology-traversal@0.3.1", "", { "dependencies": { "graphology-indices": "^0.17.0", "graphology-utils": "^2.0.0" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-lGLrLKEDKtNgAKgHVhVftKf3cb/nuWwuVPQZHXRnN90JWn0RSjco/s+NB2ARSlMapEMlbnPgv6j++427yTnU3Q=="],
+
+ "graphology-types": ["graphology-types@0.24.8", "", {}, "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q=="],
+
+ "graphology-utils": ["graphology-utils@2.5.2", "", { "peerDependencies": { "graphology-types": ">=0.23.0" } }, "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ=="],
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -236,10 +256,14 @@
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+ "mnemonist": ["mnemonist@0.39.8", "", { "dependencies": { "obliterator": "^2.0.1" } }, "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+ "obliterator": ["obliterator@2.0.5", "", {}, "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="],
+
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
diff --git a/package.json b/package.json
index b828327..7dd20ad 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.3.0",
+ "graphology-types": "^0.24.8",
"jiti": "^2.5.1",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
@@ -32,5 +33,10 @@
"typecheck": "tsc --noEmit",
"lint": "eslint"
},
- "type": "module"
+ "type": "module",
+ "dependencies": {
+ "graphology": "^0.26.0",
+ "graphology-dag": "^0.4.1",
+ "graphology-traversal": "^0.3.1"
+ }
}
diff --git a/src/traverse/ast-traversal.ts b/src/traverse/ast-traversal.ts
deleted file mode 100644
index 83213fe..0000000
--- a/src/traverse/ast-traversal.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-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
deleted file mode 100644
index 951ed0f..0000000
--- a/src/traverse/dependency-analyzer.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-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/traverse/dependency-collector.ts b/src/traverse/dependency-collector.ts
deleted file mode 100644
index b3b0668..0000000
--- a/src/traverse/dependency-collector.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import {
- DefaultFileResolver,
- type FileResolver,
-} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver'
-import {
- DefaultTypeReferenceExtractor,
- type TypeReferenceExtractor,
-} from '@daxserver/validation-schema-codegen/traverse/dependency-type'
-import { ImportDeclaration, SourceFile, TypeAliasDeclaration } from 'ts-morph'
-
-export interface TypeDependency {
- typeAlias: TypeAliasDeclaration
- sourceFile: SourceFile
- isImported: boolean
-}
-
-export class DependencyCollector {
- private dependencies = new Map()
- private visited = new Set()
- private visitedFiles = new Set()
- private static fileCache = new Map<
- string,
- { imports: ImportDeclaration[]; types: TypeAliasDeclaration[] }
- >()
- private fileResolver: FileResolver
- private typeReferenceExtractor: TypeReferenceExtractor
-
- constructor(
- fileResolver = new DefaultFileResolver(),
- typeReferenceExtractor = new DefaultTypeReferenceExtractor(),
- ) {
- this.fileResolver = fileResolver
- this.typeReferenceExtractor = typeReferenceExtractor
- }
-
- getDependencies(): Map {
- return new Map(this.dependencies)
- }
-
- getVisitedFiles(): Set {
- return new Set(this.visitedFiles)
- }
-
- collectFromImports(
- importDeclarations: ImportDeclaration[],
- exportEverything = true,
- ): TypeDependency[] {
- importDeclarations.forEach((importDeclaration) => {
- this.collectFromImport(importDeclaration)
- })
-
- if (!exportEverything) {
- this.filterUnusedImports()
- }
-
- return this.topologicalSort()
- }
-
- private collectFromImport(importDeclaration: ImportDeclaration): void {
- const importedSourceFile = this.fileResolver.getModuleSpecifierSourceFile(importDeclaration)
- if (!importedSourceFile) return
-
- const filePath = this.fileResolver.getFilePath(importedSourceFile)
- if (this.visitedFiles.has(filePath)) return
- this.visitedFiles.add(filePath)
-
- // Check cache for file data
- let fileData = DependencyCollector.fileCache.get(filePath)
-
- if (!fileData) {
- fileData = {
- imports: this.fileResolver.getImportDeclarations(importedSourceFile),
- types: this.fileResolver.getTypeAliases(importedSourceFile),
- }
- DependencyCollector.fileCache.set(filePath, fileData)
- }
-
- // First collect all imports from this file
- for (const nestedImport of fileData.imports) {
- this.collectFromImport(nestedImport)
- }
-
- // Then collect type aliases from this file - optimized batch processing
- const newTypes = new Map()
-
- for (const typeAlias of fileData.types) {
- const typeName = typeAlias.getName()
-
- if (!this.dependencies.has(typeName)) {
- newTypes.set(typeName, {
- typeAlias,
- sourceFile: importedSourceFile,
- isImported: true,
- })
- }
- }
-
- // Batch add new types to avoid repeated Map operations
- for (const [typeName, dependency] of newTypes) {
- this.dependencies.set(typeName, dependency)
- }
- }
-
- addLocalTypes(typeAliases: TypeAliasDeclaration[], sourceFile: SourceFile): void {
- // Batch process local types for better performance
- const newDependencies = new Map()
-
- for (const typeAlias of typeAliases) {
- const typeName = typeAlias.getName()
-
- if (!this.dependencies.has(typeName)) {
- newDependencies.set(typeName, {
- typeAlias,
- sourceFile,
- isImported: false,
- })
- }
- }
-
- // Batch add new dependencies
- for (const [typeName, dependency] of newDependencies) {
- this.dependencies.set(typeName, dependency)
- }
- }
-
- private filterUnusedImports(): void {
- const usedTypes = new Set()
-
- // Recursively find all types referenced by local types and their dependencies
- const findUsedTypes = (typeName: string): void => {
- if (usedTypes.has(typeName)) return
-
- const dependency = this.dependencies.get(typeName)
- if (!dependency) return
-
- usedTypes.add(typeName)
-
- const typeNode = dependency.typeAlias.getTypeNode()
- if (typeNode) {
- const referencedTypes = this.typeReferenceExtractor.extractTypeReferences(
- typeNode,
- this.dependencies,
- )
- for (const referencedType of referencedTypes) {
- findUsedTypes(referencedType)
- }
- }
- }
-
- // Start from local types (non-imported) and find all their dependencies
- for (const dependency of this.dependencies.values()) {
- if (!dependency.isImported) {
- findUsedTypes(dependency.typeAlias.getName())
- }
- }
-
- // Remove unused imported types
- for (const [typeName, dependency] of this.dependencies.entries()) {
- if (dependency.isImported && !usedTypes.has(typeName)) {
- this.dependencies.delete(typeName)
- }
- }
- }
-
- private topologicalSort(): TypeDependency[] {
- const result: TypeDependency[] = []
- const visiting = new Set()
- const dependencyKeys = new Set(this.dependencies.keys())
-
- const visit = (typeName: string): void => {
- if (this.visited.has(typeName) || visiting.has(typeName)) return
-
- visiting.add(typeName)
- const dependency = this.dependencies.get(typeName)
- if (!dependency) return
-
- // Find dependencies of this type by analyzing its type node
- const typeNode = dependency.typeAlias.getTypeNode()
- if (typeNode) {
- const referencedTypes = this.typeReferenceExtractor.extractTypeReferences(
- typeNode,
- this.dependencies,
- )
-
- // Use Set for O(1) lookup instead of Map.has() for each reference
- for (const referencedType of referencedTypes) {
- if (dependencyKeys.has(referencedType)) {
- visit(referencedType)
- }
- }
- }
-
- visiting.delete(typeName)
- this.visited.add(typeName)
- result.push(dependency)
- }
-
- // Visit all dependencies - use for...of for better performance
- for (const dependency of this.dependencies.values()) {
- visit(dependency.typeAlias.getName())
- }
-
- return result
- }
-
- static clearGlobalCache(): void {
- DependencyCollector.fileCache.clear()
- }
-}
diff --git a/src/traverse/dependency-traversal.ts b/src/traverse/dependency-traversal.ts
new file mode 100644
index 0000000..a814181
--- /dev/null
+++ b/src/traverse/dependency-traversal.ts
@@ -0,0 +1,636 @@
+import {
+ DefaultFileResolver,
+ type FileResolver,
+} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver'
+import { DirectedGraph } from 'graphology'
+import { topologicalSort } from 'graphology-dag'
+import {
+ ImportDeclaration,
+ InterfaceDeclaration,
+ Node,
+ SourceFile,
+ TypeAliasDeclaration,
+ TypeReferenceNode,
+} from 'ts-morph'
+
+export interface TypeDependency {
+ typeAlias: TypeAliasDeclaration
+ sourceFile: SourceFile
+ isImported: boolean
+}
+
+export interface TypeReferenceExtractor {
+ extractTypeReferences(typeNode: Node, dependencyGraph: DirectedGraph): string[]
+}
+
+export interface ProcessingOrderResult {
+ processInterfacesFirst: boolean
+ typeAliasesDependingOnInterfaces: string[]
+ interfacesDependingOnTypeAliases: string[]
+ optimalOrder: Array<{ name: string; type: 'interface' | 'typeAlias' }>
+}
+
+/**
+ * Unified dependency traversal class that combines AST traversal, dependency collection, and analysis
+ * Uses Graphology for efficient graph-based dependency management
+ */
+export class DependencyTraversal {
+ private dependencyGraph: DirectedGraph
+ private fileResolver: FileResolver
+ private typeReferenceExtractor: TypeReferenceExtractor
+
+ constructor(
+ fileResolver = new DefaultFileResolver(),
+ typeReferenceExtractor = new DefaultTypeReferenceExtractor(),
+ ) {
+ this.fileResolver = fileResolver
+ this.typeReferenceExtractor = typeReferenceExtractor
+ this.dependencyGraph = new DirectedGraph()
+ }
+
+ /**
+ * Extract interface names referenced by a type alias
+ */
+ extractInterfaceReferences(
+ typeAlias: TypeAliasDeclaration,
+ interfaces: Map,
+ ): string[] {
+ const typeNode = typeAlias.getTypeNode()
+ if (!typeNode) return []
+
+ const references: string[] = []
+ const visited = new Set()
+
+ const traverse = (node: Node): void => {
+ if (visited.has(node)) return
+ visited.add(node)
+
+ if (Node.isTypeReference(node)) {
+ const typeRefNode = node as TypeReferenceNode
+ const typeName = typeRefNode.getTypeName().getText()
+
+ if (interfaces.has(typeName)) {
+ references.push(typeName)
+ }
+ }
+
+ node.forEachChild(traverse)
+ }
+
+ traverse(typeNode)
+ return references
+ }
+
+ /**
+ * Extract type alias names referenced by an interface
+ */
+ extractTypeAliasReferences(
+ interfaceDecl: InterfaceDeclaration,
+ typeAliases: Map,
+ ): string[] {
+ const references: string[] = []
+ const visited = new Set()
+
+ const traverse = (node: Node): void => {
+ if (visited.has(node)) return
+ visited.add(node)
+
+ if (Node.isTypeReference(node)) {
+ const typeRefNode = node as TypeReferenceNode
+ const typeName = typeRefNode.getTypeName().getText()
+
+ if (typeAliases.has(typeName)) {
+ references.push(typeName)
+ }
+ return
+ }
+
+ node.forEachChild(traverse)
+ }
+
+ for (const typeParam of interfaceDecl.getTypeParameters()) {
+ const constraint = typeParam.getConstraint()
+ if (constraint) {
+ traverse(constraint)
+ }
+ }
+
+ for (const heritageClause of interfaceDecl.getHeritageClauses()) {
+ for (const typeNode of heritageClause.getTypeNodes()) {
+ traverse(typeNode)
+ }
+ }
+
+ return references
+ }
+
+ /**
+ * Extract type alias references from a declaration
+ */
+ extractTypeAliasReferencesFromDeclaration(typeAlias: TypeAliasDeclaration): string[] {
+ const references: string[] = []
+ const aliasName = typeAlias.getName()
+
+ if (!this.dependencyGraph.hasNode(aliasName)) {
+ this.dependencyGraph.addNode(aliasName, {
+ type: 'typeAlias',
+ declaration: typeAlias,
+ sourceFile: typeAlias.getSourceFile(),
+ isImported: false,
+ })
+ }
+
+ const typeNode = typeAlias.getTypeNode()
+ if (typeNode) {
+ const typeReferences = this.typeReferenceExtractor.extractTypeReferences(
+ typeNode,
+ this.dependencyGraph,
+ )
+ references.push(...typeReferences)
+ }
+
+ return references
+ }
+
+ /**
+ * Extract type references from a node
+ */
+ extractTypeReferences(typeNode: Node): string[] {
+ const references: string[] = []
+ const visited = new Set()
+
+ const traverse = (node: Node): void => {
+ if (visited.has(node)) return
+ visited.add(node)
+
+ if (Node.isTypeReference(node)) {
+ const typeRefNode = node as TypeReferenceNode
+ const typeName = typeRefNode.getTypeName().getText()
+
+ if (this.dependencyGraph.hasNode(typeName)) {
+ references.push(typeName)
+ }
+ return
+ }
+
+ node.forEachChild(traverse)
+ }
+
+ traverse(typeNode)
+
+ return references
+ }
+
+ /**
+ * Add local types to the dependency collection
+ */
+ addLocalTypes(typeAliases: TypeAliasDeclaration[], sourceFile: SourceFile): void {
+ for (const typeAlias of typeAliases) {
+ const typeName = typeAlias.getName()
+ if (!this.dependencyGraph.hasNode(typeName)) {
+ this.dependencyGraph.addNode(typeName, {
+ type: 'typeAlias',
+ declaration: typeAlias,
+ sourceFile,
+ isImported: false,
+ })
+ }
+ }
+ }
+
+ /**
+ * Collect dependencies from import declarations
+ */
+ collectFromImports(importDeclarations: ImportDeclaration[]): TypeDependency[] {
+ const collectedDependencies: TypeDependency[] = []
+
+ for (const importDecl of importDeclarations) {
+ const moduleSourceFile = this.fileResolver.getModuleSpecifierSourceFile(importDecl)
+ if (!moduleSourceFile) continue
+
+ const filePath = this.fileResolver.getFilePath(moduleSourceFile)
+ if (this.dependencyGraph.hasNode(filePath)) continue
+
+ this.dependencyGraph.addNode(filePath, {
+ type: 'file',
+ sourceFile: moduleSourceFile,
+ })
+
+ const imports = this.fileResolver.getImportDeclarations(moduleSourceFile)
+ const types = this.fileResolver.getTypeAliases(moduleSourceFile)
+
+ for (const typeAlias of types) {
+ const typeName = typeAlias.getName()
+ if (!this.dependencyGraph.hasNode(typeName)) {
+ this.dependencyGraph.addNode(typeName, {
+ type: 'typeAlias',
+ declaration: typeAlias,
+ sourceFile: moduleSourceFile,
+ isImported: true,
+ })
+ const dependency: TypeDependency = {
+ typeAlias,
+ sourceFile: moduleSourceFile,
+ isImported: true,
+ }
+ collectedDependencies.push(dependency)
+ }
+ }
+
+ const nestedDependencies = this.collectFromImports(imports)
+ collectedDependencies.push(...nestedDependencies)
+ }
+
+ return collectedDependencies
+ }
+
+ /**
+ * Analyze processing order for interfaces and type aliases
+ */
+ analyzeProcessingOrder(
+ typeAliases: TypeAliasDeclaration[],
+ interfaces: InterfaceDeclaration[],
+ ): ProcessingOrderResult {
+ this.dependencyGraph = new DirectedGraph()
+
+ const typeAliasMap = new Map()
+ const interfaceMap = new Map()
+
+ for (const typeAlias of typeAliases) {
+ typeAliasMap.set(typeAlias.getName(), typeAlias)
+ this.dependencyGraph.addNode(typeAlias.getName(), {
+ type: 'typeAlias',
+ declaration: typeAlias,
+ })
+ }
+
+ for (const interfaceDecl of interfaces) {
+ interfaceMap.set(interfaceDecl.getName(), interfaceDecl)
+ this.dependencyGraph.addNode(interfaceDecl.getName(), {
+ type: 'interface',
+ declaration: interfaceDecl,
+ })
+ }
+
+ const typeAliasesDependingOnInterfaces: string[] = []
+ const interfacesDependingOnTypeAliases: string[] = []
+
+ for (const typeAlias of typeAliases) {
+ const interfaceRefs = this.extractInterfaceReferences(typeAlias, interfaceMap)
+ if (interfaceRefs.length > 0) {
+ typeAliasesDependingOnInterfaces.push(typeAlias.getName())
+ for (const interfaceRef of interfaceRefs) {
+ this.dependencyGraph.addEdge(interfaceRef, typeAlias.getName(), {
+ type: 'REFERENCES',
+ direct: true,
+ context: 'type-dependency',
+ })
+ }
+ }
+ }
+
+ for (const interfaceDecl of interfaces) {
+ const typeAliasRefs = this.extractTypeAliasReferences(interfaceDecl, typeAliasMap)
+ if (typeAliasRefs.length > 0) {
+ interfacesDependingOnTypeAliases.push(interfaceDecl.getName())
+ for (const typeAliasRef of typeAliasRefs) {
+ this.dependencyGraph.addEdge(typeAliasRef, interfaceDecl.getName(), {
+ type: 'REFERENCES',
+ direct: true,
+ context: 'type-dependency',
+ })
+ }
+ }
+ }
+
+ const sortedNodes = topologicalSort(this.dependencyGraph)
+ const optimalOrder = sortedNodes.map((nodeId: string) => {
+ const nodeAttributes = this.dependencyGraph.getNodeAttributes(nodeId)
+ const nodeType = nodeAttributes.type as 'interface' | 'typeAlias'
+ return {
+ name: nodeId,
+ type: nodeType,
+ }
+ })
+
+ const processInterfacesFirst =
+ interfacesDependingOnTypeAliases.length === 0 && typeAliasesDependingOnInterfaces.length > 0
+
+ return {
+ processInterfacesFirst,
+ typeAliasesDependingOnInterfaces,
+ interfacesDependingOnTypeAliases,
+ optimalOrder,
+ }
+ }
+
+ /**
+ * Filter unused imports by removing imported types that are not referenced by local types
+ */
+ filterUnusedImports(): void {
+ const usedTypes = new Set()
+
+ // Recursively find all types referenced by local types and their dependencies
+ const findUsedTypes = (typeName: string): void => {
+ if (usedTypes.has(typeName)) return
+
+ if (!this.dependencyGraph.hasNode(typeName)) return
+ const nodeData = this.dependencyGraph.getNodeAttributes(typeName)
+ if (!nodeData || nodeData.type !== 'typeAlias') return
+
+ usedTypes.add(typeName)
+
+ const typeNode = nodeData.declaration.getTypeNode()
+ if (typeNode) {
+ const referencedTypes = this.extractTypeReferences(typeNode)
+ for (const referencedType of referencedTypes) {
+ findUsedTypes(referencedType)
+ }
+ }
+ }
+
+ // Start from local types (non-imported) and find all their dependencies
+ this.dependencyGraph.forEachNode((nodeName, nodeData) => {
+ if (nodeData.type === 'typeAlias' && !nodeData.isImported) {
+ findUsedTypes(nodeName)
+ }
+ })
+
+ // Remove unused imported types
+ const nodesToRemove: string[] = []
+ this.dependencyGraph.forEachNode((nodeName, nodeData) => {
+ if (nodeData.type === 'typeAlias' && nodeData.isImported && !usedTypes.has(nodeName)) {
+ nodesToRemove.push(nodeName)
+ }
+ })
+ for (const nodeName of nodesToRemove) {
+ this.dependencyGraph.dropNode(nodeName)
+ }
+ }
+
+ /**
+ * Get topologically sorted types with preference for imported types first
+ */
+ getTopologicallySortedTypes(exportEverything: boolean): TypeDependency[] {
+ const typeNodes = this.dependencyGraph.filterNodes(
+ (_, attributes) => attributes.type === 'typeAlias',
+ )
+ if (typeNodes.length === 0) return []
+
+ for (const typeName of typeNodes) {
+ const nodeData = this.dependencyGraph.getNodeAttributes(typeName)
+ if (!nodeData || nodeData.type !== 'typeAlias') continue
+
+ const typeNode = nodeData.declaration.getTypeNode()
+ if (typeNode) {
+ const typeReferences = this.extractTypeReferences(typeNode)
+ for (const ref of typeReferences) {
+ if (this.dependencyGraph.hasNode(ref) && !this.dependencyGraph.hasEdge(ref, typeName)) {
+ this.dependencyGraph.addEdge(ref, typeName, {
+ type: 'REFERENCES',
+ direct: true,
+ context: 'type-dependency',
+ })
+ }
+ }
+ }
+ }
+
+ const sortedNodes = topologicalSort(this.dependencyGraph)
+ const sortedDependencies = sortedNodes
+ .map((nodeId: string) => {
+ const nodeData = this.dependencyGraph.getNodeAttributes(nodeId)
+ if (nodeData && nodeData.type === 'typeAlias') {
+ return {
+ typeAlias: nodeData.declaration,
+ sourceFile: nodeData.sourceFile,
+ isImported: nodeData.isImported,
+ }
+ }
+ return undefined
+ })
+ .filter((dep): dep is TypeDependency => dep !== undefined)
+
+ if (!exportEverything) {
+ // Filter out unused imports when not exporting everything
+ this.filterUnusedImports()
+ // Re-get the sorted types after filtering by rebuilding the graph
+ const filteredSortedNodes = topologicalSort(this.dependencyGraph)
+ const filteredDependencies = filteredSortedNodes
+ .map((nodeId: string) => {
+ const nodeData = this.dependencyGraph.getNodeAttributes(nodeId)
+ if (nodeData && nodeData.type === 'typeAlias') {
+ return {
+ typeAlias: nodeData.declaration,
+ sourceFile: nodeData.sourceFile,
+ isImported: nodeData.isImported,
+ }
+ }
+ return undefined
+ })
+ .filter((dep): dep is TypeDependency => dep !== undefined)
+
+ // For exportEverything=false, still prioritize imported types while respecting dependencies
+ const processed = new Set()
+ const result: TypeDependency[] = []
+ const remaining = [...filteredDependencies]
+
+ while (remaining.length > 0) {
+ // Find all types with satisfied dependencies
+ const readyTypes = remaining.filter((dep) => this.allDependenciesProcessed(dep, processed))
+
+ if (readyTypes.length === 0) {
+ // If no types are ready, take the first one to avoid infinite loop
+ const typeToAdd = remaining.shift()
+ if (typeToAdd) {
+ result.push(typeToAdd)
+ processed.add(typeToAdd.typeAlias.getName())
+ }
+ continue
+ }
+
+ // Among ready types, prefer imported types first, then types with dependencies
+ const importedReady = readyTypes.filter((dep) => dep.isImported)
+ let typeToAdd: TypeDependency
+
+ if (importedReady.length > 0) {
+ typeToAdd = importedReady[0]!
+ } else {
+ // Among non-imported ready types, prefer those that have dependencies
+ const withDependencies = readyTypes.filter((dep) => {
+ const typeNode = dep.typeAlias.getTypeNode()
+ if (!typeNode) return false
+ const refs = this.extractTypeReferences(typeNode)
+ return refs.length > 0
+ })
+ typeToAdd = withDependencies.length > 0 ? withDependencies[0]! : readyTypes[0]!
+ }
+
+ result.push(typeToAdd)
+ processed.add(typeToAdd.typeAlias.getName())
+
+ // Remove the processed type from remaining
+ const index = remaining.indexOf(typeToAdd)
+ if (index > -1) {
+ remaining.splice(index, 1)
+ }
+ }
+
+ return result
+ }
+
+ // When exportEverything is true, we want to prioritize imported types but still respect dependencies
+ // Strategy: Go through the topologically sorted list and prefer imported types when there's a choice
+ const result: TypeDependency[] = []
+ const processed = new Set()
+ const remaining = [...sortedDependencies]
+
+ while (remaining.length > 0) {
+ let addedInThisRound = false
+
+ // Find all types that can be processed (all dependencies satisfied)
+ const readyTypes: { index: number; dep: TypeDependency }[] = []
+ for (let i = 0; i < remaining.length; i++) {
+ const dep = remaining[i]
+ if (dep && this.allDependenciesProcessed(dep, processed)) {
+ readyTypes.push({ index: i, dep })
+ }
+ }
+
+ if (readyTypes.length > 0) {
+ // Among ready types, prefer imported types first
+ const importedReady = readyTypes.filter((item) => item.dep.isImported)
+
+ let typeToAdd: { index: number; dep: TypeDependency } | undefined
+
+ if (importedReady.length > 0) {
+ // If there are imported types ready, pick the first one
+ typeToAdd = importedReady[0]
+ } else {
+ // Among local types, prefer those that have dependencies on already processed types
+ // This ensures types that depend on imported types come right after their dependencies
+ const localReady = readyTypes.filter((item) => !item.dep.isImported)
+ const withProcessedDeps = localReady.filter((item) => {
+ const typeNode = item.dep.typeAlias.getTypeNode()
+ if (!typeNode) return false
+ const refs = this.extractTypeReferences(typeNode)
+ return refs.some((ref) => processed.has(ref))
+ })
+
+ typeToAdd = withProcessedDeps.length > 0 ? withProcessedDeps[0] : localReady[0]
+ }
+
+ if (typeToAdd) {
+ result.push(typeToAdd.dep)
+ processed.add(typeToAdd.dep.typeAlias.getName())
+ remaining.splice(typeToAdd.index, 1)
+ addedInThisRound = true
+ }
+ }
+
+ // Safety check to prevent infinite loop
+ if (!addedInThisRound) {
+ // Add the first remaining item to break the loop
+ const dep = remaining.shift()!
+ result.push(dep)
+ processed.add(dep.typeAlias.getName())
+ }
+ }
+
+ return result
+ }
+
+ /**
+ * Check if all dependencies of a type have been processed
+ */
+ private allDependenciesProcessed(dependency: TypeDependency, processed: Set): boolean {
+ const typeNode = dependency.typeAlias.getTypeNode()
+ if (!typeNode) return true
+
+ const references = this.extractTypeReferences(typeNode)
+ for (const ref of references) {
+ if (!processed.has(ref)) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Get all collected dependencies
+ */
+ getDependencies(): Map {
+ const dependencies = new Map()
+ this.dependencyGraph.forEachNode((nodeName, nodeData) => {
+ if (nodeData.type === 'typeAlias') {
+ dependencies.set(nodeName, {
+ typeAlias: nodeData.declaration,
+ sourceFile: nodeData.sourceFile,
+ isImported: nodeData.isImported,
+ })
+ }
+ })
+
+ return dependencies
+ }
+
+ /**
+ * Get visited files
+ */
+ getVisitedFiles(): Set {
+ const visitedFiles = new Set()
+ this.dependencyGraph.forEachNode((nodeName, nodeData) => {
+ if (nodeData.type === 'file') {
+ visitedFiles.add(nodeName)
+ }
+ })
+
+ return visitedFiles
+ }
+
+ /**
+ * Get the dependency graph
+ */
+ getDependencyGraph(): DirectedGraph {
+ return this.dependencyGraph
+ }
+
+ /**
+ * Clear all caches and reset state
+ */
+ clearCache(): void {
+ this.dependencyGraph = new DirectedGraph()
+ }
+}
+
+/**
+ * Default type reference extractor implementation
+ */
+export class DefaultTypeReferenceExtractor implements TypeReferenceExtractor {
+ extractTypeReferences(typeNode: Node, dependencyGraph: DirectedGraph): string[] {
+ const references: string[] = []
+ const visited = new Set()
+
+ const traverse = (node: Node): void => {
+ if (visited.has(node)) return
+ visited.add(node)
+
+ if (Node.isTypeReference(node)) {
+ const typeRefNode = node as TypeReferenceNode
+ const typeName = typeRefNode.getTypeName().getText()
+
+ if (dependencyGraph.hasNode(typeName)) {
+ references.push(typeName)
+ }
+ return
+ }
+
+ node.forEachChild(traverse)
+ }
+
+ traverse(typeNode)
+
+ return references
+ }
+}
diff --git a/src/traverse/dependency-type.ts b/src/traverse/dependency-type.ts
deleted file mode 100644
index 0bc1fb4..0000000
--- a/src/traverse/dependency-type.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { TypeDependency } from '@daxserver/validation-schema-codegen/traverse/dependency-collector'
-import { Node, TypeReferenceNode } from 'ts-morph'
-
-export interface TypeReferenceExtractor {
- extractTypeReferences(typeNode: Node, dependencies: Map): string[]
-}
-
-export class DefaultTypeReferenceExtractor implements TypeReferenceExtractor {
- private static cache = new Map()
-
- extractTypeReferences(typeNode: Node, dependencies: Map): string[] {
- // Simple cache key using content only
- const cacheKey = typeNode.getText()
-
- const cached = DefaultTypeReferenceExtractor.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 (dependencies.has(typeName)) {
- references.push(typeName)
- }
- return // No need to traverse children of type references
- }
-
- // Use forEachChild for better performance instead of getChildren()
- node.forEachChild(traverse)
- }
-
- traverse(typeNode)
-
- // Cache the result
- DefaultTypeReferenceExtractor.cache.set(cacheKey, references)
- return references
- }
-
- clear(): void {
- DefaultTypeReferenceExtractor.cache.clear()
- }
-}
diff --git a/src/ts-morph-codegen.ts b/src/ts-morph-codegen.ts
index f6611ef..e2b8453 100644
--- a/src/ts-morph-codegen.ts
+++ b/src/ts-morph-codegen.ts
@@ -6,8 +6,7 @@ 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 { DependencyAnalyzer } from '@daxserver/validation-schema-codegen/traverse/dependency-analyzer'
-import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector'
+import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal'
import { getInterfaceProcessingOrder } from '@daxserver/validation-schema-codegen/utils/interface-processing-order'
import { Project, ts } from 'ts-morph'
@@ -68,8 +67,7 @@ export const generateCode = async ({
const enumParser = new EnumParser(parserOptions)
const interfaceParser = new InterfaceParser(parserOptions)
const functionDeclarationParser = new FunctionDeclarationParser(parserOptions)
- const dependencyCollector = new DependencyCollector()
- const dependencyAnalyzer = new DependencyAnalyzer()
+ const dependencyTraversal = new DependencyTraversal()
// Collect all dependencies in correct order
const importDeclarations = sourceFile.getImportDeclarations()
@@ -77,7 +75,10 @@ export const generateCode = async ({
const interfaces = sourceFile.getInterfaces()
// Analyze cross-dependencies between interfaces and type aliases
- const dependencyAnalysis = dependencyAnalyzer.analyzeProcessingOrder(localTypeAliases, interfaces)
+ const dependencyAnalysis = dependencyTraversal.analyzeProcessingOrder(
+ localTypeAliases,
+ interfaces,
+ )
// Handle different dependency scenarios:
// 1. If interfaces depend on type aliases, process those type aliases first
@@ -103,12 +104,17 @@ export const generateCode = async ({
}
// Always add local types first so they can be included in topological sort
- dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
+ dependencyTraversal.addLocalTypes(localTypeAliases, sourceFile)
- const orderedDependencies = dependencyCollector.collectFromImports(
- importDeclarations,
- exportEverything,
- )
+ // Collect from imports to resolve dependencies
+ dependencyTraversal.collectFromImports(importDeclarations)
+
+ // Filter unused imports if exportEverything is false
+ if (!exportEverything) {
+ dependencyTraversal.filterUnusedImports()
+ }
+
+ const orderedDependencies = dependencyTraversal.getTopologicallySortedTypes(exportEverything)
if (!hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) {
// Case 2: Only process type aliases that don't depend on interfaces
@@ -127,7 +133,7 @@ export const generateCode = async ({
(interfaceName) => {
const interfaceDecl = interfaces.find((i) => i.getName() === interfaceName)
if (!interfaceDecl) return false
- const typeAliasRefs = dependencyAnalyzer.extractTypeAliasReferences(
+ const typeAliasRefs = dependencyTraversal.extractTypeAliasReferences(
interfaceDecl,
new Map(localTypeAliases.map((ta) => [ta.getName(), ta])),
)
@@ -152,15 +158,6 @@ export const generateCode = async ({
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
@@ -182,6 +179,7 @@ export const generateCode = async ({
// Process remaining type aliases that depend on interfaces (Case 2 and Case 3)
if (hasTypeAliasesDependingOnInterfaces) {
+ // Process remaining type aliases (phase 2)
orderedDependencies.forEach((dependency) => {
const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes(
dependency.typeAlias.getName(),
diff --git a/tests/dependency-collector.integration.test.ts b/tests/dependency-collector.integration.test.ts
deleted file mode 100644
index 9132d3d..0000000
--- a/tests/dependency-collector.integration.test.ts
+++ /dev/null
@@ -1,429 +0,0 @@
-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'
-
-describe('DependencyCollector', () => {
- let project: Project
- let collector: DependencyCollector
-
- beforeEach(() => {
- project = new Project()
- collector = new DependencyCollector()
- DependencyCollector.clearGlobalCache()
- })
-
- describe('collectFromImports', () => {
- test('should collect dependencies from single import', () => {
- const externalFile = createSourceFile(
- project,
- `
- export type User = {
- id: string;
- name: string;
- };
- `,
- 'external.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { User } from "./external";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(1)
- expect(dependencies[0]!.typeAlias.getName()).toBe('User')
- expect(dependencies[0]!.isImported).toBe(true)
- expect(dependencies[0]!.sourceFile).toBe(externalFile)
- })
-
- test('should collect dependencies from multiple imports', () => {
- createSourceFile(
- project,
- `
- export type User = {
- id: string;
- name: string;
- };
- `,
- 'user.ts',
- )
-
- createSourceFile(
- project,
- `
- export type Product = {
- id: string;
- title: string;
- };
- `,
- 'product.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { User } from "./user";
- import { Product } from "./product";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(2)
- const typeNames = dependencies.map((d) => d.typeAlias.getName())
- expect(typeNames).toContain('User')
- expect(typeNames).toContain('Product')
- })
-
- test('should handle nested imports', () => {
- createSourceFile(
- project,
- `
- export type BaseType = {
- id: string;
- };
- `,
- 'base.ts',
- )
-
- createSourceFile(
- project,
- `
- import { BaseType } from "./base";
- export type User = BaseType & {
- name: string;
- };
- `,
- 'user.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { User } from "./user";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(2)
- const typeNames = dependencies.map((d) => d.typeAlias.getName())
- expect(typeNames).toContain('BaseType')
- expect(typeNames).toContain('User')
- })
-
- test('should handle missing module specifier source file', () => {
- const mainFile = createSourceFile(
- project,
- `
- import { NonExistent } from "./non-existent";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(0)
- })
-
- test('should not duplicate dependencies', () => {
- createSourceFile(
- project,
- `
- export type User = {
- id: string;
- name: string;
- };
- `,
- 'user.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { User } from "./user";
- import { User as UserAlias } from "./user";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(1)
- expect(dependencies[0]!.typeAlias.getName()).toBe('User')
- })
- })
-
- describe('addLocalTypes', () => {
- test('should add local type aliases', () => {
- const sourceFile = createSourceFile(
- project,
- `
- type LocalUser = {
- id: string;
- name: string;
- };
-
- type LocalProduct = {
- id: string;
- title: string;
- };
- `,
- )
-
- const typeAliases = sourceFile.getTypeAliases()
- collector.addLocalTypes(typeAliases, sourceFile)
-
- const dependencies = collector.collectFromImports([])
- expect(dependencies).toHaveLength(2)
-
- const typeNames = dependencies.map((d) => d.typeAlias.getName())
- expect(typeNames).toContain('LocalUser')
- expect(typeNames).toContain('LocalProduct')
-
- dependencies.forEach((dep) => {
- expect(dep!.isImported).toBe(false)
- expect(dep!.sourceFile).toBe(sourceFile)
- })
- })
-
- test('should not duplicate existing types', () => {
- const sourceFile = createSourceFile(
- project,
- `
- type User = {
- id: string;
- name: string;
- };
- `,
- )
-
- const typeAliases = sourceFile.getTypeAliases()
- collector.addLocalTypes(typeAliases, sourceFile)
- collector.addLocalTypes(typeAliases, sourceFile)
-
- const dependencies = collector.collectFromImports([])
- expect(dependencies).toHaveLength(1)
- expect(dependencies[0]!.typeAlias.getName()).toBe('User')
- })
- })
-
- describe('topological sorting', () => {
- test('should sort dependencies in correct order', () => {
- createSourceFile(
- project,
- `
- export type BaseType = {
- id: string;
- };
- `,
- 'base.ts',
- )
-
- createSourceFile(
- project,
- `
- import { BaseType } from "./base";
- export type User = BaseType & {
- name: string;
- };
- `,
- 'user.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { User } from "./user";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(2)
- expect(dependencies[0]!.typeAlias.getName()).toBe('BaseType')
- expect(dependencies[1]!.typeAlias.getName()).toBe('User')
- })
-
- test('should handle complex dependency chains', () => {
- createSourceFile(
- project,
- `
- export type A = {
- value: string;
- };
- `,
- 'a.ts',
- )
-
- createSourceFile(
- project,
- `
- import { A } from "./a";
- export type B = {
- a: A;
- name: string;
- };
- `,
- 'b.ts',
- )
-
- createSourceFile(
- project,
- `
- import { B } from "./b";
- export type C = {
- b: B;
- id: number;
- };
- `,
- 'c.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { C } from "./c";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(3)
- expect(dependencies[0]!.typeAlias.getName()).toBe('A')
- expect(dependencies[1]!.typeAlias.getName()).toBe('B')
- expect(dependencies[2]!.typeAlias.getName()).toBe('C')
- })
-
- test('should handle circular dependencies gracefully', () => {
- createSourceFile(
- project,
- `
- import { B } from "./b";
- export type A = {
- b?: B;
- value: string;
- };
- `,
- 'a.ts',
- )
-
- createSourceFile(
- project,
- `
- import { A } from "./a";
- export type B = {
- a?: A;
- name: string;
- };
- `,
- 'b.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { A } from "./a";
- import { B } from "./b";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(2)
- const typeNames = dependencies.map((d) => d.typeAlias.getName())
- expect(typeNames).toContain('A')
- expect(typeNames).toContain('B')
- })
-
- test('should handle types with no dependencies', () => {
- createSourceFile(
- project,
- `
- export type SimpleType = {
- id: string;
- name: string;
- };
- `,
- 'simple.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { SimpleType } from "./simple";
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(1)
- expect(dependencies[0]!.typeAlias.getName()).toBe('SimpleType')
- })
- })
-
- describe('mixed local and imported types', () => {
- test('should handle both local and imported types correctly', () => {
- createSourceFile(
- project,
- `
- export type ExternalType = {
- id: string;
- };
- `,
- 'external.ts',
- )
-
- const mainFile = createSourceFile(
- project,
- `
- import { ExternalType } from "./external";
-
- type LocalType = {
- external: ExternalType;
- local: string;
- };
- `,
- 'main.ts',
- )
-
- const importDeclarations = mainFile.getImportDeclarations()
- const typeAliases = mainFile.getTypeAliases()
-
- collector.addLocalTypes(typeAliases, mainFile)
- const dependencies = collector.collectFromImports(importDeclarations)
-
- expect(dependencies).toHaveLength(2)
-
- const externalDep = dependencies.find((d) => d.typeAlias.getName() === 'ExternalType')
- const localDep = dependencies.find((d) => d.typeAlias.getName() === 'LocalType')
-
- expect(externalDep!.isImported).toBe(true)
- expect(localDep!.isImported).toBe(false)
-
- expect(dependencies[0]!.typeAlias.getName()).toBe('ExternalType')
- expect(dependencies[1]!.typeAlias.getName()).toBe('LocalType')
- })
- })
-})
diff --git a/tests/dependency-collector.performance.test.ts b/tests/dependency-collector.performance.test.ts
deleted file mode 100644
index a702723..0000000
--- a/tests/dependency-collector.performance.test.ts
+++ /dev/null
@@ -1,341 +0,0 @@
-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'
-
-describe('DependencyCollector Performance Tests', () => {
- let project: Project
- let collector: DependencyCollector
-
- beforeEach(() => {
- project = new Project()
- collector = new DependencyCollector()
- })
-
- describe('large dependency chains', () => {
- test('should handle deep import chains efficiently', () => {
- const startTime = performance.now()
-
- // Create a chain of 50 files, each importing from the previous
- for (let i = 0; i < 50; i++) {
- const content =
- i === 0
- ? `export type Type${i} = { id: string; value: number; }`
- : `import { Type${i - 1} } from "./file${i - 1}";
-export type Type${i} = Type${i - 1} & { field${i}: string; };`
-
- createSourceFile(project, content, `file${i}.ts`)
- }
-
- const mainFile = createSourceFile(project, 'import { Type49 } from "./file49";', 'main.ts')
-
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- const endTime = performance.now()
- const executionTime = endTime - startTime
-
- expect(dependencies).toHaveLength(50)
- expect(executionTime).toBeLessThan(1000) // Should complete in under 1 second
- })
-
- test('should handle wide import trees efficiently', () => {
- const startTime = performance.now()
-
- // Create 100 independent files
- for (let i = 0; i < 100; i++) {
- createSourceFile(
- project,
- `export type WideType${i} = { id: string; value${i}: number; }`,
- `wide${i}.ts`,
- )
- }
-
- // Create main file that imports all 100 types
- const imports = Array.from(
- { length: 100 },
- (_, i) => `import { WideType${i} } from "./wide${i}";`,
- ).join('\n')
-
- const mainFile = createSourceFile(project, imports, 'main.ts')
- const importDeclarations = mainFile.getImportDeclarations()
- const dependencies = collector.collectFromImports(importDeclarations)
-
- const endTime = performance.now()
- const executionTime = endTime - startTime
-
- expect(dependencies).toHaveLength(100)
- expect(executionTime).toBeLessThan(2000) // Should complete in under 2 seconds
- })
- })
-
- describe('cache efficiency bottlenecks', () => {
- test('should demonstrate cache key generation overhead', () => {
- // Create types with complex nested structures that stress the cache key generation
- const complexType = `
- export type ComplexType = {
- nested: {
- deep: {
- structure: {
- with: {
- many: {
- levels: {
- and: {
- properties: string;
- numbers: number[];
- objects: { [key: string]: any };
- unions: string | number | boolean;
- intersections: { a: string } & { b: number };
- generics: Array