From fd1d1094893fa7012713f2d4780bb9d6e4b0b874 Mon Sep 17 00:00:00 2001 From: DaxServer Date: Thu, 28 Aug 2025 12:35:17 +0200 Subject: [PATCH 1/2] feat(graph): rework dependency traversal with graphology DAG --- ARCHITECTURE.md | 491 ++++------ package.json | 4 +- src/index.ts | 84 ++ src/parsers/base-parser.ts | 12 +- src/parsers/parse-enums.ts | 26 +- src/parsers/parse-function-declarations.ts | 12 +- src/parsers/parse-interfaces.ts | 34 +- src/parsers/parse-type-aliases.ts | 15 +- src/printer/typebox-printer.ts | 61 ++ src/traverse/dependency-file-resolver.ts | 26 - src/traverse/dependency-graph.ts | 8 + src/traverse/dependency-traversal.ts | 752 +++++---------- src/traverse/file-graph.ts | 20 + src/traverse/node-graph.ts | 57 ++ src/traverse/types.ts | 19 + src/ts-morph-codegen.ts | 208 ----- src/utils/add-static-type-alias.ts | 3 +- src/utils/generate-qualified-name.ts | 25 + tests/export-everything.test.ts | 168 ---- tests/handlers/typebox/advanced-types.test.ts | 51 -- .../{array-types.test.ts => arrays.test.ts} | 32 +- tests/handlers/typebox/enum-types.test.ts | 107 --- tests/handlers/typebox/enums.test.ts | 115 +++ ...nction-types.test.ts => functions.test.ts} | 36 +- tests/handlers/typebox/interfaces.test.ts | 96 +- .../{object-types.test.ts => objects.test.ts} | 8 +- .../handlers/typebox/primitive-types.test.ts | 42 +- .../typebox/template-literal-types.test.ts | 95 -- .../typebox/template-literals.test.ts | 183 ++++ tests/handlers/typebox/typeof.test.ts | 91 ++ tests/handlers/typebox/utility-types.test.ts | 37 +- tests/import-resolution.test.ts | 72 +- .../dependency-collector.integration.test.ts | 860 +++++++++--------- .../dependency-collector.unit.test.ts | 178 ---- tests/traverse/dependency-ordering.test.ts | 184 ++-- tests/utils.ts | 4 +- tsconfig.json | 1 + 37 files changed, 1757 insertions(+), 2460 deletions(-) create mode 100644 src/index.ts create mode 100644 src/printer/typebox-printer.ts delete mode 100644 src/traverse/dependency-file-resolver.ts create mode 100644 src/traverse/dependency-graph.ts create mode 100644 src/traverse/file-graph.ts create mode 100644 src/traverse/node-graph.ts create mode 100644 src/traverse/types.ts delete mode 100644 src/ts-morph-codegen.ts create mode 100644 src/utils/generate-qualified-name.ts delete mode 100644 tests/export-everything.test.ts delete mode 100644 tests/handlers/typebox/advanced-types.test.ts rename tests/handlers/typebox/{array-types.test.ts => arrays.test.ts} (85%) delete mode 100644 tests/handlers/typebox/enum-types.test.ts create mode 100644 tests/handlers/typebox/enums.test.ts rename tests/handlers/typebox/{function-types.test.ts => functions.test.ts} (79%) rename tests/handlers/typebox/{object-types.test.ts => objects.test.ts} (88%) delete mode 100644 tests/handlers/typebox/template-literal-types.test.ts create mode 100644 tests/handlers/typebox/template-literals.test.ts create mode 100644 tests/handlers/typebox/typeof.test.ts delete mode 100644 tests/traverse/dependency-collector.unit.test.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 22bdbc2..24d185f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,191 +1,132 @@ -# TypeBox Code Generation Documentation +# Architecture + +## Table of Contents - [Overview](#overview) -- [Core Component](#core-component) - - [Function Flow](#function-flow) - - [Import Resolution and Dependency Management](#import-resolution-and-dependency-management) - - [DependencyCollector](#dependencycollector) - - [Key Features](#key-features) - - [Implementation Details](#implementation-details) -- [Interface Inheritance Support](#interface-inheritance-support) - - [Dependency-Ordered Processing](#dependency-ordered-processing) - - [TypeBox Composite Generation](#typebox-composite-generation) - - [Implementation Details](#implementation-details-1) -- [Input Handling System](#input-handling-system) - - [InputOptions Interface](#inputoptions-interface) - - [Input Processing Features](#input-processing-features) - - [Usage Patterns](#usage-patterns) -- [Basic Usage](#basic-usage) - - [With Export Everything](#with-export-everything) - - [Using File Path](#using-file-path) -- [Utility Functions and Modules](#utility-functions-and-modules) - - [Handlers Directory](#handlers-directory) - - [Parsers Directory](#parsers-directory) - - [Performance Considerations](#performance-considerations) -- [Performance Optimizations](#performance-optimizations) - - [TypeBox Type Handler Optimization](#typebox-type-handler-optimization) - - [Parser Instance Reuse](#parser-instance-reuse) - - [Prettier Optimization](#prettier-optimization) - - [Import Resolution Performance Optimizations](#import-resolution-performance-optimizations) - - [ImportParser Optimizations](#importparser-optimizations) - - [DependencyCollector Optimizations](#dependencycollector-optimizations) - - [TypeReferenceExtractor Optimizations](#typereferenceextractor-optimizations) - - [TypeBoxTypeHandlers Optimizations](#typeboxtypehandlers-optimizations) - - [Performance Testing](#performance-testing) +- [Core Components](#core-components) + - [Code Generation Flow](#code-generation-flow) + - [TypeBox Printer](#typebox-printer) + - [Dependency Management](#dependency-management) + - [Parser System](#parser-system) + - [Handler System](#handler-system) + - [Import Resolution](#import-resolution) + - [Input Handling](#input-handling) + - [Interface Inheritance](#interface-inheritance) + - [Utility Functions](#utility-functions) - [Process Overview](#process-overview) -- [Test-Driven Development](#test-driven-development) - - [TDD Cycle](#tdd-cycle) - - [Running Tests](#running-tests) - - [TDD Workflow for New Features](#tdd-workflow-for-new-features) - - [Test Organization](#test-organization) - - [Best Practices](#best-practices) -- [Documentation Guidelines](#documentation-guidelines) - -This document describes the process and key components involved in generating TypeBox schemas from TypeScript source files using `ts-morph`. +- [Basic Usage](#basic-usage) +- [Testing](#testing) ## Overview -The primary goal of this codebase is to automate the creation of TypeBox schemas and their corresponding static types from existing TypeScript declarations including, but not limited to, `enum`, `type alias`, `interface`, and `function` declarations. This allows for robust runtime validation and type inference based on a single source of truth. - -## Core Component - -The main logic for code generation resides in the file. Its primary function, `generateCode`, takes a `GenerateCodeOptions` object as input and returns a string containing the generated TypeBox code. The input can be either a file path or source code string, with support for relative imports when using existing project contexts. - -### Function Flow - -1. **Input Processing**: The module processes the input options to create a `SourceFile` object. This supports both file paths and source code strings, with proper validation for relative imports and path resolution. - -2. **Initialization**: A new in-memory `SourceFile` (`output.ts`) is created to build the generated code. Essential imports for TypeBox (`Type`, `Static`) are added as separate import declarations for better compatibility. - -3. **Parser Instantiation**: Instances of `ImportParser`, `EnumParser`, `TypeAliasParser`, and `FunctionDeclarationParser` are created, each responsible for handling specific types of declarations. - -4. **Import Processing**: The `ImportParser` is instantiated and processes all import declarations in the input `sourceFile` to resolve imported types from external files. This includes locating corresponding source files for relative module specifiers and processing type aliases from imported files. - -5. **Enum Processing**: The `EnumParser` is instantiated and iterates through all `enum` declarations in the input `sourceFile`. For each enum, its original declaration is copied, a TypeBox `Type.Enum` schema is generated, and a corresponding static type alias is added. - -6. **Type Alias Processing**: The `TypeAliasParser` is instantiated and iterates through all `type alias` declarations in the input `sourceFile`. For each type alias, its underlying type node is converted into a TypeBox-compatible type representation, a TypeBox schema is generated, and a corresponding static type alias is added. - -7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order to handle inheritance properly. For each interface, its properties and methods are converted into TypeBox object schemas. Interfaces that extend other interfaces are generated using `Type.Intersect` to combine the base interface with additional properties, ensuring proper inheritance semantics. - -8. **Function Declaration Processing**: The `FunctionDeclarationParser` is instantiated and iterates through all function declarations in the input `sourceFile`. For each function, its parameters, optional parameters, and return type are converted into TypeBox function schemas with corresponding static type aliases. - -9. **Output**: Finally, the full text content of the newly generated `output.ts` source file (which now contains all the TypeBox schemas and static types) is returned as a string. +`@daxserver/validation-schema-codegen` is a comprehensive codegen tool for validation schemas that transforms TypeScript type definitions into TypeBox validation schemas. This project serves as an enhanced alternative to `@sinclair/typebox-codegen`, providing superior type handling capabilities and advanced dependency management through graph-based analysis. -### Import Resolution and Dependency Management +### Supported TypeScript Constructs -The code generation process includes sophisticated import resolution and dependency management to handle complex type hierarchies and multi-level import chains: +- **Type Definitions**: Type aliases, interfaces, enums, and function declarations +- **Generic Types**: Generic interfaces and type parameters with proper constraint handling +- **Complex Types**: Union and intersection types, nested object structures, template literal types +- **Utility Types**: Built-in support for Pick, Omit, Partial, Required, Record, and other TypeScript utility types +- **Advanced Features**: Conditional types, mapped types, keyof operators, indexed access types +- **Import Resolution**: Cross-file type dependencies with qualified naming and circular dependency handling -#### DependencyTraversal +## Core Components -The module implements a unified `DependencyTraversal` class that exclusively uses Graphology for all dependency management: +The main logic for code generation resides in the file. Its primary function, `generateCode`, takes a `GenerateCodeOptions` object as input and returns a string containing the generated TypeBox code. The input can be either a file path or source code string, with support for relative imports when using existing project contexts. -- **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 -- **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 +### Code Generation Flow -#### Key Features +The `generateCode` function in orchestrates the entire code generation process: -1. **Dependency Order Resolution**: Types are processed in dependency order, ensuring that imported types are defined before types that reference them -2. **Export Keyword Handling**: Imported types are generated without `export` keywords, while local types maintain their original export status -3. **Circular Dependency Detection**: The topological sort algorithm detects and handles circular dependencies gracefully -4. **Type Reference Extraction**: Analyzes TypeScript AST nodes to identify type references and build accurate dependency relationships +1. **Input Processing**: Creates a `SourceFile` from input using `createSourceFileFromInput` +2. **Generic Interface Detection**: Checks for generic interfaces to determine required TypeBox imports +3. **Output File Creation**: Creates a new output file with necessary `@sinclair/typebox` imports using `createOutputFile` +4. **Dependency Traversal**: Uses `DependencyTraversal` to analyze and sort all type dependencies +5. **Code Generation**: Processes sorted nodes using `TypeBoxPrinter` in `printSortedNodes` +6. **Output Formatting**: Returns the formatted TypeBox schema code -#### Implementation Details - -The import resolution process works in three phases using the unified architecture: +### TypeBox Printer -1. **Collection Phase**: - - `DependencyTraversal.collectFromImports()` traverses all import declarations using integrated AST traversal - - `DependencyTraversal.addLocalTypes()` adds local type aliases with unified type reference extraction - - Dependencies are tracked using Graphology's DirectedGraph with nodes and edges representing type relationships +The class orchestrates the parsing and printing of TypeScript nodes into TypeBox schemas: -2. **Analysis Phase**: - - `DependencyTraversal.getTopologicallySortedTypesWithPreference()` performs dependency ordering with export-aware preferences - - Uses graphology-dag for circular dependency detection and topological sorting - - Handles complex dependency scenarios including imported vs. local type ordering based on `exportEverything` flag +- **Unified Processing**: Handles type aliases, interfaces, enums, and function declarations through a single interface +- **Parser Coordination**: Utilizes dedicated parsers (`TypeAliasParser`, `InterfaceParser`, `EnumParser`, `FunctionDeclarationParser`) +- **Type Tracking**: Maintains a `processedTypes` set to avoid redundant processing +- **Output Management**: Coordinates with the output `SourceFile` and TypeScript printer -3. **Generation Phase**: - - `DependencyTraversal.getTopologicallySortedTypes()` returns types in dependency order - - `TypeAliasParser.parseWithImportFlag()` generates code with appropriate export handling - - Types are processed sequentially in the sorted order +### Dependency Management -This unified approach ensures robust handling of complex import scenarios, circular dependencies, and generates code that compiles without dependency errors while maintaining optimal performance through reduced module boundaries. +The system uses a sophisticated graph-based dependency management approach centered around the `DependencyTraversal` class: -### Unified Dependency Management +#### Architecture Overview -The dependency management system is built on a unified architecture that integrates all dependency-related functionality: +The dependency system consists of three main components: -#### DependencyTraversal Integration +1. **DependencyTraversal**: - Main orchestrator +2. **FileGraph**: - Tracks file-level dependencies +3. **NodeGraph**: - Manages type-level dependencies -The module provides comprehensive dependency management: +#### Process Flow -- **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 +1. **Local Type Collection**: Adds all types from the main source file +2. **Import Processing**: Recursively processes import declarations +3. **Dependency Extraction**: Analyzes type references to build dependency graph +4. **Topological Sorting**: Returns nodes in proper dependency order -#### Graph-Based Dependency Resolution +### Parser System -The unified module leverages Graphology's ecosystem for robust dependency management: +The parser system is built around a base class architecture in : -- **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 +#### Base Parser -#### Simplified Architecture Benefits +The provides: +- Common interface for all parsers +- Shared access to output `SourceFile`, TypeScript printer, and processed types tracking +- Abstract `parse` method for implementation by specific parsers -The Graphology-only approach provides several advantages: +#### Specialized Parsers -- **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 +1. **InterfaceParser**: - Handles both regular and generic interfaces +2. **TypeAliasParser**: - Processes type alias declarations +3. **EnumParser**: - Handles enum declarations +4. **FunctionParser**: - Processes function declarations +5. **ImportParser**: - Handles import resolution -## Interface Inheritance Support - -The codebase provides comprehensive support for TypeScript interface inheritance through a sophisticated dependency resolution and code generation system: +### Handler System -### Dependency-Ordered Processing +The handler system in provides TypeBox-specific type conversion through a hierarchical architecture: -The main codegen logic in implements sophisticated processing order management: +#### Handler Categories -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 +1. **Base Handlers**: Foundation classes including and specialized base classes +2. **Collection Handlers**: , , , +3. **Object Handlers**: , +4. **Reference Handlers**: , , , , +5. **Simple Handlers**: , +6. **Advanced Handlers**: , , , +7. **Function Handlers**: +8. **Type Query Handlers**: , +9. **Access Handlers**: , -### TypeBox Composite Generation +#### Handler Management -Interface inheritance is implemented using TypeBox's `Type.Composite` functionality: +The class orchestrates all handlers through: +- **Handler Caching**: Caches handler instances for performance optimization +- **Fallback System**: Provides fallback handlers for complex cases -- **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 +### Import Resolution -### Implementation Details +The import resolution system handles complex import scenarios: -- **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 +- **Multi-level Imports**: Supports nested import chains of any depth +- **Circular Import Detection**: Prevents infinite loops during traversal +- **Qualified Naming**: Resolves type name collisions across files -## Input Handling System +### Input Handling The module provides flexible input processing capabilities for the code generation system. It supports multiple input methods and handles various edge cases related to file resolution and import validation. -### InputOptions Interface +#### InputOptions Interface The `InputOptions` interface defines the available input parameters: @@ -198,7 +139,7 @@ export interface InputOptions { } ``` -### Input Processing Features +#### Input Processing Features 1. **Dual Input Support**: Accepts either file paths or source code strings 2. **Path Resolution**: Handles both absolute and relative file paths with proper validation @@ -206,210 +147,111 @@ export interface InputOptions { 4. **Project Context Sharing**: Supports passing existing `ts-morph` Project instances to maintain import resolution context 5. **Error Handling**: Provides clear error messages for invalid inputs and unresolvable paths -### Usage Patterns +#### Usage Patterns - **File Path Input**: Automatically resolves and loads TypeScript files from disk - **Source Code Input**: Processes TypeScript code directly from strings with validation - **Project Context**: Enables proper relative import resolution when working with in-memory source files -## Basic Usage - -```typescript -const result = await generateCode({ - sourceCode: sourceFile.getFullText(), - callerFile: sourceFile.getFilePath(), -}) -``` - -### With Export Everything - -```typescript -const result = await generateCode({ - sourceCode: sourceFile.getFullText(), - exportEverything: true, - callerFile: sourceFile.getFilePath(), -}) -``` - -### Using File Path - -```typescript -const result = await generateCode({ - filePath: './types.ts', -}) -``` - -## Utility Functions and Modules - -- : Contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. `getTypeBoxType` takes a `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema. -- : Generates and adds the `export type [TypeName] = Static` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time. -- : Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation. -- : Provides shared utilities for extracting string literal keys from union or literal types, used by Pick and Omit type handlers to avoid code duplication. -- : Contains common Node type checking utilities for `canHandle` methods, including functions for checking SyntaxKind, TypeOperator patterns, and TypeReference patterns. - -### Handlers Directory - -This directory contains a collection of specialized handler modules, each responsible for converting a specific type of TypeScript AST node into its corresponding TypeBox schema. The handlers follow a hierarchical architecture with specialized base classes to reduce code duplication and ensure consistent behavior. - -#### Base Handler Classes +### Interface Inheritance -- : The root abstract base class that defines the common interface for all type handlers. Provides the `canHandle` and `handle` methods, along with utility functions like `makeTypeCall` for creating TypeBox expressions. -- : Specialized base class for utility type handlers that work with TypeScript type references. Provides `validateTypeReference` and `extractTypeArguments` methods for consistent handling of generic utility types like `Partial`, `Pick`, etc. -- : Base class for handlers that process object-like structures (objects and interfaces). Provides `processProperties`, `extractProperties`, and `createObjectType` methods for consistent property handling and TypeBox object creation. -- : Base class for handlers that work with collections of types (arrays, tuples, unions, intersections). Provides `processTypeCollection`, `processSingleType`, and `validateNonEmptyCollection` methods for consistent type collection processing. -- : Base class for TypeScript type operator handlers (keyof, readonly). Provides common functionality for checking operator types using `isTypeOperatorWithOperator` utility and processing inner types. Subclasses define `operatorKind`, `typeBoxMethod`, and `createTypeBoxCall` to customize behavior for specific operators. - -#### Type Handler Implementations - -**Utility Type Handlers** (extend `TypeReferenceBaseHandler`): - -- : Handles TypeScript `Partial` utility types. -- : Handles TypeScript `Pick` utility types. -- : Handles TypeScript `Omit` utility types. -- : Handles TypeScript `Required` utility types. -- : Handles TypeScript `Record` utility types. - -**Object-Like Type Handlers** (extend `ObjectLikeBaseHandler`): - -- : Handles TypeScript object types and type literals. -- : Handles TypeScript interface declarations, including support for interface inheritance using `Type.Composite` to combine base interfaces with extended properties. Supports generic interfaces with type parameters, generating parameterized functions that accept TypeBox schemas as arguments. Handles generic type calls in heritage clauses, converting expressions like `A` to `A(Type.Number())` for proper TypeBox composition. - -**Collection Type Handlers** (extend `CollectionBaseHandler`): - -- : Handles TypeScript array types (e.g., `string[]`, `Array`). -- : Handles TypeScript tuple types. -- : Handles TypeScript union types (e.g., `string | number`). -- : Handles TypeScript intersection types (e.g., `TypeA & TypeB`). - -**Type Operator Handlers** (extend `TypeOperatorBaseHandler`): - -- : Handles TypeScript `keyof` type operator for extracting object keys. -- : Handles TypeScript `readonly` type modifier for creating immutable types. - -**Standalone Type Handlers** (extend `BaseTypeHandler`): - -- : Handles basic TypeScript types like `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `void`. -- : Handles TypeScript literal types (e.g., `'hello'`, `123`, `true`). -- : Handles TypeScript function types and function declarations, including parameter types, optional parameters, and return types. -- : Handles TypeScript template literal types (e.g., `` `hello-${string}` ``). Parses template literals into components, handling literal text, embedded types (string, number, unions), and string/numeric literals. -- : Handles TypeScript `typeof` expressions for extracting types from values. -- : Fallback handler for other TypeScript type operators not covered by specific handlers. -- : Handles references to other types (e.g., `MyType`). -- : Handles TypeScript indexed access types (e.g., `Type[Key]`). -- : A generic handler for TypeBox types. - -**Handler Orchestration**: - -- : Orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered. Uses optimized lookup mechanisms for performance with O(1) syntax kind-based lookups and type reference name mappings. Includes specialized handlers for type operators (KeyOfTypeHandler, TypeofTypeHandler, ReadonlyTypeHandler) and maintains fallback handlers for edge cases requiring custom logic. - -### Parsers Directory - -This directory contains a collection of parser classes, each extending the `BaseParser` abstract class. These classes are responsible for parsing specific TypeScript declarations (imports, enums, type aliases) and transforming them into TypeBox schemas and static types. This modular design ensures a clear separation of concerns and facilitates the addition of new parser functionalities. - -- : Defines the abstract `BaseParser` class, providing a common interface and shared properties for all parser implementations. -- : Implements the `EnumParser` class, responsible for processing TypeScript `enum` declarations. -- : Implements the `ImportParser` class, responsible for resolving and processing TypeScript import declarations. -- : Implements the `TypeAliasParser` class, responsible for processing TypeScript `type alias` declarations. -- : Implements the `FunctionDeclarationParser` class, responsible for processing TypeScript function declarations and converting them to TypeBox function schemas. -- : Implements the `InterfaceParser` class, responsible for processing TypeScript interface declarations with support for inheritance through dependency ordering and `Type.Composite` generation. Handles generic interfaces by generating parameterized functions with type parameters that accept TypeBox schemas as arguments. - -### Performance Considerations - -When implementing new type handlers or modifying existing ones, it is crucial to consider performance. Operations that involve converting complex TypeScript AST nodes to text, such as `type.getText()`, can be computationally expensive, especially when performed frequently or on large type structures. Directly checking specific properties of AST nodes (e.g., `typeName.getText()`) is significantly more performant than relying on full text representations of types. This principle should be applied throughout the codebase to ensure optimal performance. - -## Performance Optimizations - -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 +The codebase provides comprehensive support for TypeScript interface inheritance through a sophisticated dependency resolution and code generation system: -#### Core Architecture +#### Dependency-Ordered Processing -- **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 +1. **Separate Graph Analysis**: Uses with specialized FileGraph and NodeGraph classes to analyze complex relationships between interfaces and type aliases +2. **Specialized Graph Processing**: Leverages NodeGraph's topological sorting for robust type dependency ordering with domain-specific optimizations +3. **Type-Focused Processing**: NodeGraph handles type node dependencies independently from file dependencies for cleaner separation of concerns +4. **Topological Sorting**: Uses `NodeGraph.getNodesToPrint()` to ensure types are processed in correct dependency order to prevent "type not found" errors +5. **Circular Dependency Detection**: The NodeGraph provides built-in circular dependency detection specifically for type relationships with detailed error reporting -### TypeBox Type Handler Optimization +#### TypeBox Composite Generation -- **O(1) Handler Lookup**: The `TypeBoxTypeHandlers` class has been optimized from O(n) to O(1) lookup performance using specialized `Map` data structures: - - **SyntaxKind-based Lookup**: Direct mapping of TypeScript `SyntaxKind` values to handlers for primitive types, arrays, tuples, unions, intersections, and other structural types - - **Type Reference Name Lookup**: Dedicated mapping for utility types (`Record`, `Partial`, `Pick`, `Omit`, `Required`) based on type reference names - - **Enhanced TypeReference Handling**: All `TypeReference` nodes that are not specific utility types now default to `TypeReferenceHandler`, significantly reducing reliance on O(n) fallback searches - - **Minimal Fallback Mechanism**: Maintains backward compatibility with a reduced fallback array for edge cases that require custom `canHandle` logic -- **Handler Caching**: Results are cached based on `typeNode.kind` and `typeNode.getText()` to avoid repeated lookups for identical type nodes -- **Singleton Pattern**: The `getTypeBoxType` function reuses a single `TypeBoxTypeHandlers` instance instead of creating new instances on every call +Interface inheritance is implemented using TypeBox's `Type.Composite` functionality: -### Parser Instance Reuse +- **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 -- **Shared Printer**: A single `ts.createPrinter()` instance is now shared across all parsers (`ImportParser`, `EnumParser`, `TypeAliasParser`) to reduce object creation overhead. -- **TypeAliasParser Caching**: `ImportParser` now reuses a single `TypeAliasParser` instance instead of creating new instances for each import, reducing instantiation overhead. +#### Implementation Details -### Prettier Optimization +- **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 -- **Configuration Caching**: Prettier options and import strings are now cached as constants to avoid repeated object creation and string concatenation during formatting operations. +### Utility Functions -### Import Resolution Performance Optimizations +The directory provides essential utilities for the TypeBox code generation process: -#### ImportParser Optimizations +#### Core Utility Modules -- **Visited Files Tracking**: Implements `visitedFiles` set to prevent infinite recursion and duplicate processing of the same source files during import chain traversal -- **File Content Caching**: Uses `fileCache` to store parsed import declarations and type aliases, eliminating redundant file parsing operations -- **Reset Mechanism**: Provides `reset()` method to clear internal caches, ensuring memory efficiency between processing sessions +- : Contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. `getTypeBoxType` takes a `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema. +- : Generates and adds the `export type [TypeName] = Static` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time. +- : Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation. +- : Provides shared utilities for extracting string literal keys from union or literal types, used by Pick and Omit type handlers to avoid code duplication. +- : Contains common Node type checking utilities for `canHandle` methods, including functions for checking SyntaxKind, TypeOperator patterns, and TypeReference patterns. +- : Processes literal type nodes within template literals, handling the conversion of embedded literal values into TypeBox expressions for template literal type generation. -#### DependencyCollector Optimizations +#### Template Literal Processing -- **Unified File Caching**: Consolidates file data into single cache entries containing both imports and type aliases, reducing memory overhead and cache key complexity -- **Direct Map Operations**: Eliminates intermediate `Map` creation during dependency collection, performing direct operations on the main dependencies map for better performance -- **Early Exit Optimization**: Implements early exit strategies in batch processing loops to avoid unnecessary iterations +- **Template Literal Type Processor**: Handles the complex parsing and conversion of TypeScript template literal types into TypeBox `TemplateLiteral` expressions +- **Literal Node Processing**: Converts various literal types (string, number, boolean) within template contexts +- **Pattern Recognition**: Identifies and processes template literal patterns with embedded type expressions -#### TypeReferenceExtractor Optimizations +#### Key Extraction System -- **Stable Cache Keys**: Uses node text and kind for reliable cache keys that maintain consistency across test runs -- **Set-based Dependency Lookup**: Converts dependency keys to `Set` for O(1) lookup performance instead of O(n) `Map.has()` operations -- **Efficient Node Traversal**: Uses `forEachChild()` instead of `getChildren()` for better AST traversal performance -- **Cache Management**: Provides `clearCache()` method for memory management between processing sessions +- **String Key Extraction**: Extracts string literal keys from union types and object type literals for use in utility type handlers +- **TypeBox Expression Generation**: Converts extracted keys into appropriate TypeBox array expressions +- **Shared Utilities**: Provides reusable key extraction logic for Pick, Omit, and other utility type handlers to avoid code duplication -#### TypeBoxTypeHandlers Optimizations +## Process Overview -- **Stable Cache Keys**: Uses node text and kind for reliable cache keys that maintain consistency across test runs -- **Optimized Handler Lookup**: Prioritizes syntax kind handlers (most common case) before type reference handlers for better average-case performance -- **Cache Management**: Provides public `clearCache()` method for external cache management +1. **Input**: A TypeScript source file containing `enum`, `type alias`, `interface`, and `function` declarations. +2. **Parsing**: `ts-morph` parses the input TypeScript file into an Abstract Syntax Tree (AST). +3. **Centralized Dependency Traversal**: The `DependencyTraversal.startTraversal()` method orchestrates the complete dependency collection and ordering process: + - Collects local types from the main source file + - Recursively traverses import chains to gather all dependencies + - Establishes dependency relationships between all types + - Returns topologically sorted nodes for processing +4. **Sequential Node Processing**: The sorted nodes are processed sequentially using specialized parsers, ensuring dependencies are handled before dependent types. +5. **TypeBox Schema Generation**: For each node, the corresponding TypeBox schema is constructed using appropriate `Type` methods (e.g., `Type.Enum`, `Type.Object`, `Type.Function`, etc.). This process involves sophisticated mapping of TypeScript types to their TypeBox equivalents. +6. **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. +7. **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. -These optimizations collectively improve import resolution performance by: +## Basic Usage -- Reducing redundant file I/O operations through comprehensive caching -- Optimizing data structure access patterns for better algorithmic complexity -- Preventing infinite recursion and duplicate processing scenarios -- Using stable cache keys that maintain consistency across test runs +```typescript +const result = await generateCode({ + sourceCode: sourceFile.getFullText(), + callerFile: sourceFile.getFilePath(), +}) +``` -The optimizations maintain full backward compatibility and test reliability while improving performance for complex import chains and large codebases. +### Using File Path -### Performance Testing +```typescript +const result = await generateCode({ + filePath: './types.ts', +}) +``` -To ensure the dependency collection system performs efficiently under various scenarios, comprehensive performance tests have been implemented in . These tests specifically target potential bottlenecks in dependency collection and import processing. +## Testing -## Process Overview +### Test Structure -1. **Input**: A TypeScript source file containing `enum`, `type alias`, `interface`, and `function` declarations. -2. **Parsing**: `ts-morph` parses the input TypeScript file into an Abstract Syntax Tree (AST). -3. **Traversal and Transformation**: The `generateCode` function traverses the AST, identifying and processing various declaration types including enums, type aliases, interfaces, and function declarations. -4. **TypeBox Schema Generation**: For each identified declaration, the corresponding TypeBox schema is constructed using appropriate `Type` methods (e.g., `Type.Enum`, `Type.Object`, `Type.Function`, etc.). This process involves sophisticated mapping of TypeScript types to their TypeBox equivalents. -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. +- **Unit Tests**: Individual component testing using Bun's test framework +- **Integration Tests**: End-to-end testing of the complete generation pipeline +- **Type Safety Tests**: Validation of generated TypeBox schemas +- **Edge Case Coverage**: Testing of complex scenarios and error conditions -## Test-Driven Development +### Quality Tools -This project follows a Test-Driven Development (TDD) methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases: +- **TypeScript Compiler**: Type checking with `bun tsc` +- **ESLint**: Code quality and style enforcement +- **Prettier**: Consistent code formatting +- **Bun Test**: Fast and reliable test execution ### TDD Cycle @@ -442,13 +284,6 @@ When implementing new type handlers or features: 4. **Verify Implementation**: Run tests again to ensure they pass 5. **Integration Testing**: Run the full test suite with `bun test` to ensure no regressions -### Test Organization - -Tests are organized into several categories: - -- **Unit Tests** (`tests/handlers/`): Test individual type handlers and parsers -- **Performance Tests**: Validate performance characteristics of complex operations - ### Best Practices - Write tests before implementing functionality @@ -461,7 +296,3 @@ Tests are organized into several categories: - Run any specific tests with path like `bun test tests/handlers/typebox/function-types.test.ts` - Run any specific test cases using command like `bun test tests/handlers/typebox/function-types.test.ts -t function types` - If tests keep failing, take help from tsc, lint commands to detect for any issues - -## Documentation Guidelines - -Whenever changes are made to the codebase, it is crucial to update the relevant sections of this documentation to reflect those changes accurately. This ensures the documentation remains a reliable and up-to-date resource for understanding the project. diff --git a/package.json b/package.json index 7dd20ad..4e3b711 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@daxserver/validation-schema-codegen", "version": "0.1.0", - "main": "src/ts-morph-codegen.ts", - "module": "src/ts-morph-codegen.ts", + "main": "src/index.ts", + "module": "src/index.ts", "devDependencies": { "@eslint/js": "^9.34.0", "@prettier/sync": "^0.6.1", diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ea0c0bf --- /dev/null +++ b/src/index.ts @@ -0,0 +1,84 @@ +import { + createSourceFileFromInput, + type InputOptions, +} from '@daxserver/validation-schema-codegen/input-handler' +import { TypeBoxPrinter } from '@daxserver/validation-schema-codegen/printer/typebox-printer' +import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { Project, SourceFile, ts } from 'ts-morph' + +const createOutputFile = (hasGenericInterfaces: boolean) => { + const newSourceFile = new Project().createSourceFile('output.ts', '', { + overwrite: true, + }) + + // Add imports + const namedImports = [ + 'Type', + { + name: 'Static', + isTypeOnly: true, + }, + ] + + if (hasGenericInterfaces) { + namedImports.push({ + name: 'TSchema', + isTypeOnly: true, + }) + } + + newSourceFile.addImportDeclaration({ + moduleSpecifier: '@sinclair/typebox', + namedImports, + }) + + return newSourceFile +} + +const printSortedNodes = ( + sortedTraversedNodes: TraversedNode[], + newSourceFile: SourceFile, +) => { + const printer = new TypeBoxPrinter({ + newSourceFile, + printer: ts.createPrinter(), + }) + + // Process nodes in topological order + for (const traversedNode of sortedTraversedNodes) { + printer.printNode(traversedNode) + } + + return newSourceFile.getFullText() +} + +export const generateCode = async ({ + sourceCode, + filePath, + ...options +}: InputOptions): Promise => { + // Create source file from input + const sourceFile = createSourceFileFromInput({ + sourceCode, + filePath, + ...options, + }) + + // Check if any interfaces have generic type parameters + const hasGenericInterfaces = sourceFile + .getInterfaces() + .some((i) => i.getTypeParameters().length > 0) + + // Create output file with proper imports + const newSourceFile = createOutputFile(hasGenericInterfaces) + + // Create dependency traversal and start traversal + const dependencyTraversal = new DependencyTraversal() + const traversedNodes = dependencyTraversal.startTraversal(sourceFile) + + // Print sorted nodes to output + const result = printSortedNodes(traversedNodes, newSourceFile) + + return result +} diff --git a/src/parsers/base-parser.ts b/src/parsers/base-parser.ts index 9f3fbb3..c477365 100644 --- a/src/parsers/base-parser.ts +++ b/src/parsers/base-parser.ts @@ -1,30 +1,20 @@ -import { ExportGetableNode, Node, SourceFile, ts } from 'ts-morph' +import { Node, SourceFile, ts } from 'ts-morph' export interface BaseParserOptions { newSourceFile: SourceFile printer: ts.Printer processedTypes: Set - exportEverything?: boolean } export abstract class BaseParser { protected newSourceFile: SourceFile protected printer: ts.Printer protected processedTypes: Set - protected exportEverything: boolean constructor(options: BaseParserOptions) { this.newSourceFile = options.newSourceFile this.printer = options.printer this.processedTypes = options.processedTypes - this.exportEverything = options.exportEverything ?? false - } - - protected getIsExported(node: ExportGetableNode, isImported: boolean = false): boolean { - if (this.exportEverything) { - return true - } - return isImported ? false : node.hasExportKeyword() } abstract parse(node: Node): void diff --git a/src/parsers/parse-enums.ts b/src/parsers/parse-enums.ts index aa5b5a8..fcb4cae 100644 --- a/src/parsers/parse-enums.ts +++ b/src/parsers/parse-enums.ts @@ -4,28 +4,36 @@ import { EnumDeclaration, VariableDeclarationKind } from 'ts-morph' export class EnumParser extends BaseParser { parse(enumDeclaration: EnumDeclaration): void { - const typeName = enumDeclaration.getName() - const enumText = enumDeclaration.getText() - const isExported = this.getIsExported(enumDeclaration) - this.newSourceFile.addStatements(isExported ? `export ${enumText}` : enumText) + const enumName = enumDeclaration.getName() + + this.newSourceFile.addEnum({ + name: enumName, + isExported: true, + members: enumDeclaration.getMembers().map((member) => ({ + name: member.getName(), + value: member.hasInitializer() ? member.getValue() : undefined, + })), + }) + + // Generate TypeBox type + const typeboxType = `Type.Enum(${enumName})` this.newSourceFile.addVariableStatement({ - isExported, + isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { - name: typeName, - initializer: `Type.Enum(${typeName})`, + name: enumName, + initializer: typeboxType, }, ], }) addStaticTypeAlias( this.newSourceFile, - typeName, + enumName, this.newSourceFile.compilerNode, this.printer, - isExported, ) } } diff --git a/src/parsers/parse-function-declarations.ts b/src/parsers/parse-function-declarations.ts index 0407a3a..b7ca5fa 100644 --- a/src/parsers/parse-function-declarations.ts +++ b/src/parsers/parse-function-declarations.ts @@ -6,16 +6,15 @@ import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph' export class FunctionDeclarationParser extends BaseParser { parse(functionDecl: FunctionDeclaration): void { - this.parseWithImportFlag(functionDecl, false) + this.parseWithImportFlag(functionDecl) } - parseWithImportFlag(functionDecl: FunctionDeclaration, isImported: boolean): void { - this.parseFunctionWithImportFlag(functionDecl, isImported) + parseWithImportFlag(functionDecl: FunctionDeclaration): void { + this.parseFunctionWithImportFlag(functionDecl) } private parseFunctionWithImportFlag( functionDecl: FunctionDeclaration, - isImported: boolean, ): void { const functionName = functionDecl.getName() if (!functionName) { @@ -70,10 +69,8 @@ export class FunctionDeclarationParser extends BaseParser { this.newSourceFile.compilerNode, ) - const isExported = this.getIsExported(functionDecl, isImported) - this.newSourceFile.addVariableStatement({ - isExported, + isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { @@ -88,7 +85,6 @@ export class FunctionDeclarationParser extends BaseParser { functionName, this.newSourceFile.compilerNode, this.printer, - isExported, ) } } diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts index d2a2a9c..5cf8c94 100644 --- a/src/parsers/parse-interfaces.ts +++ b/src/parsers/parse-interfaces.ts @@ -10,17 +10,6 @@ import { export class InterfaceParser extends BaseParser { parse(interfaceDecl: InterfaceDeclaration): void { - this.parseWithImportFlag(interfaceDecl, false) - } - - parseWithImportFlag(interfaceDecl: InterfaceDeclaration, isImported: boolean): void { - this.parseInterfaceWithImportFlag(interfaceDecl, isImported) - } - - private parseInterfaceWithImportFlag( - interfaceDecl: InterfaceDeclaration, - isImported: boolean, - ): void { const interfaceName = interfaceDecl.getName() if (this.processedTypes.has(interfaceName)) { @@ -30,17 +19,16 @@ export class InterfaceParser extends BaseParser { this.processedTypes.add(interfaceName) const typeParameters = interfaceDecl.getTypeParameters() - const isExported = this.getIsExported(interfaceDecl, isImported) // Check if interface has type parameters (generic) if (typeParameters.length > 0) { - this.parseGenericInterface(interfaceDecl, isExported) + this.parseGenericInterface(interfaceDecl) } else { - this.parseRegularInterface(interfaceDecl, isExported) + this.parseRegularInterface(interfaceDecl) } } - private parseRegularInterface(interfaceDecl: InterfaceDeclaration, isExported: boolean): void { + private parseRegularInterface(interfaceDecl: InterfaceDeclaration): void { const interfaceName = interfaceDecl.getName() // Generate TypeBox type definition @@ -52,7 +40,7 @@ export class InterfaceParser extends BaseParser { ) this.newSourceFile.addVariableStatement({ - isExported, + isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { @@ -67,11 +55,10 @@ export class InterfaceParser extends BaseParser { interfaceName, this.newSourceFile.compilerNode, this.printer, - isExported, ) } - private parseGenericInterface(interfaceDecl: InterfaceDeclaration, isExported: boolean): void { + private parseGenericInterface(interfaceDecl: InterfaceDeclaration): void { const interfaceName = interfaceDecl.getName() const typeParameters = interfaceDecl.getTypeParameters() @@ -85,7 +72,7 @@ export class InterfaceParser extends BaseParser { // Add the function declaration this.newSourceFile.addVariableStatement({ - isExported, + isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { @@ -96,13 +83,12 @@ export class InterfaceParser extends BaseParser { }) // Add generic type alias: type A = Static>> - this.addGenericTypeAlias(interfaceName, typeParameters, isExported) + this.addGenericTypeAlias(interfaceName, typeParameters) } private addGenericTypeAlias( name: string, typeParameters: TypeParameterDeclaration[], - isExported: boolean, ): void { // Create type parameters for the type alias const typeParamDeclarations = typeParameters.map((typeParam) => { @@ -152,12 +138,12 @@ export class InterfaceParser extends BaseParser { ) this.newSourceFile.addTypeAlias({ - isExported, + isExported: true, name, typeParameters: typeParamDeclarations.map((tp) => this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode), - ), - type: staticType, + ), + type: staticType, }) } } diff --git a/src/parsers/parse-type-aliases.ts b/src/parsers/parse-type-aliases.ts index 0a42573..9cb3e94 100644 --- a/src/parsers/parse-type-aliases.ts +++ b/src/parsers/parse-type-aliases.ts @@ -6,18 +6,12 @@ import { ts, TypeAliasDeclaration, VariableDeclarationKind } from 'ts-morph' export class TypeAliasParser extends BaseParser { parse(typeAlias: TypeAliasDeclaration): void { - this.parseWithImportFlag(typeAlias, false) + this.parseWithImportFlag(typeAlias) } - parseWithImportFlag(typeAlias: TypeAliasDeclaration, isImported: boolean): void { + parseWithImportFlag(typeAlias: TypeAliasDeclaration): void { const typeName = typeAlias.getName() - if (this.processedTypes.has(typeName)) { - return - } - - this.processedTypes.add(typeName) - const typeNode = typeAlias.getTypeNode() const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any') const typeboxType = this.printer.printNode( @@ -26,10 +20,8 @@ export class TypeAliasParser extends BaseParser { this.newSourceFile.compilerNode, ) - const isExported = this.getIsExported(typeAlias, isImported) - this.newSourceFile.addVariableStatement({ - isExported, + isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { @@ -44,7 +36,6 @@ export class TypeAliasParser extends BaseParser { typeName, this.newSourceFile.compilerNode, this.printer, - isExported, ) } } diff --git a/src/printer/typebox-printer.ts b/src/printer/typebox-printer.ts new file mode 100644 index 0000000..c3796aa --- /dev/null +++ b/src/printer/typebox-printer.ts @@ -0,0 +1,61 @@ +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' +import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases' +import { Node, SourceFile, ts } from 'ts-morph' + +export interface PrinterOptions { + newSourceFile: SourceFile + printer: ts.Printer +} + +export class TypeBoxPrinter { + private readonly newSourceFile: SourceFile + private readonly printer: ts.Printer + private readonly processedTypes = new Set() + private readonly typeAliasParser: TypeAliasParser + private readonly interfaceParser: InterfaceParser + private readonly enumParser: EnumParser + private readonly functionParser: FunctionDeclarationParser + + constructor(options: PrinterOptions) { + this.newSourceFile = options.newSourceFile + this.printer = options.printer + + // Initialize parsers with the same configuration + const parserOptions = { + newSourceFile: this.newSourceFile, + printer: this.printer, + processedTypes: this.processedTypes, + } + + this.typeAliasParser = new TypeAliasParser(parserOptions) + this.interfaceParser = new InterfaceParser(parserOptions) + this.enumParser = new EnumParser(parserOptions) + this.functionParser = new FunctionDeclarationParser(parserOptions) + } + + printNode(traversedNode: { node: Node; isImported?: boolean }): void { + const { node } = traversedNode + + switch (true) { + case Node.isTypeAliasDeclaration(node): + this.typeAliasParser.parseWithImportFlag(node) + break + + case Node.isInterfaceDeclaration(node): + this.interfaceParser.parse(node) + break + + case Node.isEnumDeclaration(node): + this.enumParser.parse(node) + break + + case Node.isFunctionDeclaration(node): + this.functionParser.parse(node) + break + + default: + } + } +} diff --git a/src/traverse/dependency-file-resolver.ts b/src/traverse/dependency-file-resolver.ts deleted file mode 100644 index 4985de0..0000000 --- a/src/traverse/dependency-file-resolver.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ImportDeclaration, SourceFile, TypeAliasDeclaration } from 'ts-morph' - -export interface FileResolver { - getModuleSpecifierSourceFile(importDeclaration: ImportDeclaration): SourceFile | undefined - getFilePath(sourceFile: SourceFile): string - getImportDeclarations(sourceFile: SourceFile): ImportDeclaration[] - getTypeAliases(sourceFile: SourceFile): TypeAliasDeclaration[] -} - -export class DefaultFileResolver implements FileResolver { - getModuleSpecifierSourceFile(importDeclaration: ImportDeclaration): SourceFile | undefined { - return importDeclaration.getModuleSpecifierSourceFile() - } - - getFilePath(sourceFile: SourceFile): string { - return sourceFile.getFilePath() - } - - getImportDeclarations(sourceFile: SourceFile): ImportDeclaration[] { - return sourceFile.getImportDeclarations() - } - - getTypeAliases(sourceFile: SourceFile): TypeAliasDeclaration[] { - return sourceFile.getTypeAliases() - } -} diff --git a/src/traverse/dependency-graph.ts b/src/traverse/dependency-graph.ts new file mode 100644 index 0000000..caa9a1a --- /dev/null +++ b/src/traverse/dependency-graph.ts @@ -0,0 +1,8 @@ +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { DirectedGraph } from 'graphology' + +export class DependencyGraph extends DirectedGraph { + add(name: string, node: TraversedNode) { + this.addNode(name, node) + } +} diff --git a/src/traverse/dependency-traversal.ts b/src/traverse/dependency-traversal.ts index a814181..40576c3 100644 --- a/src/traverse/dependency-traversal.ts +++ b/src/traverse/dependency-traversal.ts @@ -1,614 +1,275 @@ -import { - DefaultFileResolver, - type FileResolver, -} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver' -import { DirectedGraph } from 'graphology' +import { FileGraph } from '@daxserver/validation-schema-codegen/traverse/file-graph' +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name' 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' }> -} +import { ImportDeclaration, InterfaceDeclaration, Node, SourceFile, TypeAliasDeclaration, TypeReferenceNode } from 'ts-morph' /** - * Unified dependency traversal class that combines AST traversal, dependency collection, and analysis - * Uses Graphology for efficient graph-based dependency management + * Dependency traversal class for AST traversal, dependency collection, and analysis + * Uses separate graphs for files and nodes for better separation of concerns */ 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) - } - } + private fileGraph = new FileGraph() + private nodeGraph = new NodeGraph() - for (const heritageClause of interfaceDecl.getHeritageClauses()) { - for (const typeNode of heritageClause.getTypeNodes()) { - traverse(typeNode) - } - } - - return references - } + constructor() {} /** - * Extract type alias references from a declaration + * Start the traversal process from the main source file + * This method handles the complete recursive traversal and returns sorted nodes */ - extractTypeAliasReferencesFromDeclaration(typeAlias: TypeAliasDeclaration): string[] { - const references: string[] = [] - const aliasName = typeAlias.getName() + startTraversal(mainSourceFile: SourceFile): TraversedNode[] { + // Mark main source file nodes as main code + this.addLocalTypes(mainSourceFile, true) - if (!this.dependencyGraph.hasNode(aliasName)) { - this.dependencyGraph.addNode(aliasName, { - type: 'typeAlias', - declaration: typeAlias, - sourceFile: typeAlias.getSourceFile(), - isImported: false, - }) - } + // Start recursive traversal from imports + const importDeclarations = mainSourceFile.getImportDeclarations() + this.collectFromImports(importDeclarations, true, mainSourceFile) - const typeNode = typeAlias.getTypeNode() - if (typeNode) { - const typeReferences = this.typeReferenceExtractor.extractTypeReferences( - typeNode, - this.dependencyGraph, - ) - references.push(...typeReferences) - } + // Extract dependencies for all nodes + this.extractDependencies() - return references + // Return topologically sorted nodes + return this.getNodesToPrint() } /** - * Extract type references from a node + * Add local types from a source file to the node graph */ - extractTypeReferences(typeNode: Node): string[] { - const references: string[] = [] - const visited = new Set() + addLocalTypes(sourceFile: SourceFile, isMainCode: boolean = false): void { + const typeAliases = sourceFile.getTypeAliases() + const interfaces = sourceFile.getInterfaces() + const enums = sourceFile.getEnums() + const functions = sourceFile.getFunctions() - const traverse = (node: Node): void => { - if (visited.has(node)) return - visited.add(node) + // First pass: Add all nodes to the graph without extracting dependencies - 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 { + // Collect type aliases 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(), { + const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: typeAlias, type: 'typeAlias', - declaration: typeAlias, + originalName: typeName, + qualifiedName, + isImported: false, + isMainCode, }) } + // Collect interfaces for (const interfaceDecl of interfaces) { - interfaceMap.set(interfaceDecl.getName(), interfaceDecl) - this.dependencyGraph.addNode(interfaceDecl.getName(), { + const interfaceName = interfaceDecl.getName() + const qualifiedName = generateQualifiedNodeName(interfaceName, interfaceDecl.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: interfaceDecl, type: 'interface', - declaration: interfaceDecl, + originalName: interfaceName, + qualifiedName, + isImported: false, + isMainCode, }) } - 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', - }) - } - } + // Collect enums + for (const enumDecl of enums) { + const enumName = enumDecl.getName() + const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: enumDecl, + type: 'enum', + originalName: enumName, + qualifiedName, + isImported: false, + isMainCode, + }) } - 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', - }) - } - } + // Collect functions + for (const functionDecl of functions) { + const functionName = functionDecl.getName() + if (!functionName) continue + + const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: functionDecl, + type: 'function', + originalName: functionName, + qualifiedName, + isImported: false, + isMainCode, + }) } - 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 + * Extract dependencies for all nodes in the graph */ - 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 + extractDependencies(): void { + // Extract dependencies for all nodes in the graph + for (const nodeId of this.nodeGraph.nodes()) { + const nodeData = this.nodeGraph.getNode(nodeId) - if (!this.dependencyGraph.hasNode(typeName)) return - const nodeData = this.dependencyGraph.getNodeAttributes(typeName) - if (!nodeData || nodeData.type !== 'typeAlias') return + if (nodeData.type === 'typeAlias') { + const typeAlias = nodeData.node as TypeAliasDeclaration + const typeNode = typeAlias.getTypeNode() + if (!typeNode) continue + + const typeReferences = this.extractTypeReferences(typeNode) + + // Add edges for dependencies + for (const referencedType of typeReferences) { + if (this.nodeGraph.hasNode(referencedType)) { + this.nodeGraph.addDependency(referencedType, nodeId) + } + } + } else if (nodeData.type === 'interface') { + const interfaceDecl = nodeData.node as InterfaceDeclaration + const typeReferences = this.extractTypeReferences(interfaceDecl) + + // Add edges for dependencies + for (const referencedType of typeReferences) { + if (this.nodeGraph.hasNode(referencedType)) { + this.nodeGraph.addDependency(referencedType, nodeId) + } + } + } + } + } - usedTypes.add(typeName) - const typeNode = nodeData.declaration.getTypeNode() - if (typeNode) { - const referencedTypes = this.extractTypeReferences(typeNode) - for (const referencedType of referencedTypes) { - findUsedTypes(referencedType) - } - } - } + /** + * Check if a type is used in the source file + */ + private isTypeUsedInSourceFile(typeName: string, sourceFile: SourceFile): boolean { + const typeReferences: string[] = [] - // Start from local types (non-imported) and find all their dependencies - this.dependencyGraph.forEachNode((nodeName, nodeData) => { - if (nodeData.type === 'typeAlias' && !nodeData.isImported) { - findUsedTypes(nodeName) + sourceFile.forEachDescendant((node) => { + if (Node.isTypeReference(node)) { + const typeRefNode = node as TypeReferenceNode + const referencedTypeName = typeRefNode.getTypeName().getText() + typeReferences.push(referencedTypeName) } }) - // 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) - } + return typeReferences.includes(typeName) } /** - * Get topologically sorted types with preference for imported types first + * Collect dependencies from import declarations */ - 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 - } + collectFromImports(importDeclarations: ImportDeclaration[], isDirectImport: boolean = true, mainSourceFile?: SourceFile): void { + for (const importDecl of importDeclarations) { + const moduleSourceFile = importDecl.getModuleSpecifierSourceFile() + if (!moduleSourceFile) 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]! - } + const filePath = moduleSourceFile.getFilePath() - result.push(typeToAdd) - processed.add(typeToAdd.typeAlias.getName()) + // Prevent infinite loops by tracking visited files + if (this.fileGraph.hasNode(filePath)) continue - // Remove the processed type from remaining - const index = remaining.indexOf(typeToAdd) - if (index > -1) { - remaining.splice(index, 1) - } - } + this.fileGraph.addFile(filePath, moduleSourceFile) - return result - } + const imports = moduleSourceFile.getImportDeclarations() + const typeAliases = moduleSourceFile.getTypeAliases() + const interfaces = moduleSourceFile.getInterfaces() + const enums = moduleSourceFile.getEnums() + const functions = moduleSourceFile.getFunctions() - // 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 - } + // Add all imported types to the graph + for (const typeAlias of typeAliases) { + const typeName = typeAlias.getName() + const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) + const isRootImport = isDirectImport + this.nodeGraph.addTypeNode(qualifiedName, { + node: typeAlias, + type: 'typeAlias', + originalName: typeName, + qualifiedName, + isImported: true, + isDirectImport, + isRootImport, + }) } - // 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()) + for (const interfaceDecl of interfaces) { + const interfaceName = interfaceDecl.getName() + const qualifiedName = generateQualifiedNodeName(interfaceName, interfaceDecl.getSourceFile()) + const isRootImport = isDirectImport + this.nodeGraph.addTypeNode(qualifiedName, { + node: interfaceDecl, + type: 'interface', + originalName: interfaceName, + qualifiedName, + isImported: true, + isDirectImport, + isRootImport, + }) } - } - - 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 + for (const enumDecl of enums) { + const enumName = enumDecl.getName() + const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) + const isRootImport = isDirectImport + this.nodeGraph.addTypeNode(qualifiedName, { + node: enumDecl, + type: 'enum', + originalName: enumName, + qualifiedName, + isImported: true, + isDirectImport, + isRootImport, + }) } - } - - 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, + for (const functionDecl of functions) { + const functionName = functionDecl.getName() + if (!functionName) continue + + const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) + const isRootImport = isDirectImport + this.nodeGraph.addTypeNode(qualifiedName, { + node: functionDecl, + type: 'function', + originalName: functionName, + qualifiedName, + isImported: true, + isDirectImport, + isRootImport, }) } - }) - return dependencies + // Recursively collect from nested imports (mark as transitive) + this.collectFromImports(imports, false, mainSourceFile) + } } /** - * Get visited files + * Get nodes in dependency order (dependencies first) + * Retrieved from the graph, not from SourceFile */ - getVisitedFiles(): Set { - const visitedFiles = new Set() - this.dependencyGraph.forEachNode((nodeName, nodeData) => { - if (nodeData.type === 'file') { - visitedFiles.add(nodeName) - } - }) - - return visitedFiles + getNodesToPrint(): TraversedNode[] { + try { + // Use topological sort to ensure dependencies are printed first + const sortedNodeIds = topologicalSort(this.nodeGraph) + return sortedNodeIds.map((nodeId: string) => + this.nodeGraph.getNodeAttributes(nodeId), + ) + } catch { + // Handle circular dependencies by returning nodes in insertion order + // This ensures dependencies are still processed before dependents when possible + return Array.from(this.nodeGraph.nodes()).map((nodeId: string) => + this.nodeGraph.getNodeAttributes(nodeId), + ) + } } - /** - * 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[] { + private extractTypeReferences(typeNode: Node): string[] { const references: string[] = [] const visited = new Set() @@ -620,9 +281,14 @@ export class DefaultTypeReferenceExtractor implements TypeReferenceExtractor { const typeRefNode = node as TypeReferenceNode const typeName = typeRefNode.getTypeName().getText() - if (dependencyGraph.hasNode(typeName)) { - references.push(typeName) + for (const qualifiedName of this.nodeGraph.nodes()) { + const nodeData = this.nodeGraph.getNode(qualifiedName) + if (nodeData.originalName === typeName) { + references.push(qualifiedName) + break + } } + return } diff --git a/src/traverse/file-graph.ts b/src/traverse/file-graph.ts new file mode 100644 index 0000000..2aaae6b --- /dev/null +++ b/src/traverse/file-graph.ts @@ -0,0 +1,20 @@ +import { DirectedGraph } from 'graphology' +import type { SourceFile } from 'ts-morph' + +/** + * Graph for managing file dependencies + * Tracks relationships between source files + */ +export class FileGraph extends DirectedGraph { + /** + * Add a file to the graph + */ + addFile(filePath: string, sourceFile: SourceFile): void { + if (this.hasNode(filePath)) return + + this.addNode(filePath, { + type: 'file', + sourceFile, + }) + } +} diff --git a/src/traverse/node-graph.ts b/src/traverse/node-graph.ts new file mode 100644 index 0000000..de00ffc --- /dev/null +++ b/src/traverse/node-graph.ts @@ -0,0 +1,57 @@ +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { DirectedGraph } from 'graphology' + +/** + * Graph for managing type node dependencies + * Tracks relationships between type nodes (interfaces, type aliases, enums, functions) + */ +export class NodeGraph extends DirectedGraph { + /** + * Add a type node to the graph + */ + addTypeNode(qualifiedName: string, node: TraversedNode): void { + if (this.hasNode(qualifiedName)) return + + this.addNode(qualifiedName, node) + } + + /** + * Get node by qualified name + */ + getNode(qualifiedName: string): TraversedNode { + return this.getNodeAttributes(qualifiedName) as TraversedNode + } + + /** + * Remove unused imported nodes that have no outgoing edges + * Never removes root imports (types directly imported from the main file) + */ + removeUnusedImportedNodes(): void { + const nodesToRemove: string[] = [] + + for (const nodeId of this.nodes()) { + const nodeData = this.getNodeAttributes(nodeId) + if (nodeData?.isImported && !nodeData?.isRootImport) { + // Check if this imported type has any outgoing edges (other nodes depend on it) + const outgoingEdges = this.outboundNeighbors(nodeId) + if (outgoingEdges.length === 0) { + nodesToRemove.push(nodeId) + } + } + } + + // Remove unused imported types + for (const nodeId of nodesToRemove) { + this.dropNode(nodeId) + } + } + + /** + * Add dependency edge between two nodes + */ + addDependency(fromNode: string, toNode: string): void { + if (this.hasNode(fromNode) && this.hasNode(toNode)) { + this.addDirectedEdge(fromNode, toNode) + } + } +} diff --git a/src/traverse/types.ts b/src/traverse/types.ts new file mode 100644 index 0000000..988988a --- /dev/null +++ b/src/traverse/types.ts @@ -0,0 +1,19 @@ +import type { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import type { Node } from 'ts-morph' + +export interface TypeReferenceExtractor { + extractTypeReferences(typeNode: Node, nodeGraph: NodeGraph): string[] +} + +export type SupportedNodeType = 'interface' | 'typeAlias' | 'enum' | 'function' + +export interface TraversedNode { + node: Node + type: SupportedNodeType + originalName: string + qualifiedName: string + isImported?: boolean + isDirectImport?: boolean + isRootImport?: boolean + isMainCode?: boolean +} diff --git a/src/ts-morph-codegen.ts b/src/ts-morph-codegen.ts deleted file mode 100644 index e2b8453..0000000 --- a/src/ts-morph-codegen.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { - createSourceFileFromInput, - type InputOptions, -} from '@daxserver/validation-schema-codegen/input-handler' -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' -import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases' -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' - -export interface GenerateCodeOptions extends InputOptions { - exportEverything?: boolean -} - -export const generateCode = async ({ - sourceCode, - filePath, - exportEverything = false, - ...options -}: GenerateCodeOptions): Promise => { - const sourceFile = createSourceFileFromInput({ - sourceCode, - filePath, - ...options, - }) - const processedTypes = new Set() - const newSourceFile = new Project().createSourceFile('output.ts', '', { - overwrite: true, - }) - - // Check if any interfaces have generic type parameters - const hasGenericInterfaces = sourceFile - .getInterfaces() - .some((i) => i.getTypeParameters().length > 0) - - // Add imports - const namedImports = [ - 'Type', - { - name: 'Static', - isTypeOnly: true, - }, - ] - - if (hasGenericInterfaces) { - namedImports.push({ - name: 'TSchema', - isTypeOnly: true, - }) - } - - newSourceFile.addImportDeclaration({ - moduleSpecifier: '@sinclair/typebox', - namedImports, - }) - - const parserOptions = { - newSourceFile, - printer: ts.createPrinter(), - processedTypes, - exportEverything, - } - - const typeAliasParser = new TypeAliasParser(parserOptions) - const enumParser = new EnumParser(parserOptions) - const interfaceParser = new InterfaceParser(parserOptions) - const functionDeclarationParser = new FunctionDeclarationParser(parserOptions) - const dependencyTraversal = new DependencyTraversal() - - // 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 = dependencyTraversal.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 - dependencyTraversal.addLocalTypes(localTypeAliases, sourceFile) - - // 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 - 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 = dependencyTraversal.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 enums - sourceFile.getEnums().forEach((e) => { - enumParser.parse(e) - }) - - // Process interfaces in dependency order - 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) { - // Process remaining type aliases (phase 2) - 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) => { - functionDeclarationParser.parse(f) - }) - - return newSourceFile.getFullText() -} diff --git a/src/utils/add-static-type-alias.ts b/src/utils/add-static-type-alias.ts index 3e02a90..1f4dcfb 100644 --- a/src/utils/add-static-type-alias.ts +++ b/src/utils/add-static-type-alias.ts @@ -6,7 +6,6 @@ export const addStaticTypeAlias = ( name: string, compilerNode: ts.SourceFile, printer: ts.Printer, - isExported: boolean, ) => { const staticTypeNode = ts.factory.createTypeReferenceNode( ts.factory.createIdentifier(TypeBoxStatic), @@ -16,7 +15,7 @@ export const addStaticTypeAlias = ( const staticType = printer.printNode(ts.EmitHint.Unspecified, staticTypeNode, compilerNode) newSourceFile.addTypeAlias({ - isExported, + isExported: true, name, type: staticType, }) diff --git a/src/utils/generate-qualified-name.ts b/src/utils/generate-qualified-name.ts new file mode 100644 index 0000000..0866ea5 --- /dev/null +++ b/src/utils/generate-qualified-name.ts @@ -0,0 +1,25 @@ +import { basename } from 'path' +import type { SourceFile } from 'ts-morph' + +/** + * Simple hash function for generating unique identifiers + */ +const hashString = (str: string): string => { + let hash = 0 + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32-bit integer + } + return Math.abs(hash).toString(36) +} + +/** + * Generate a fully qualified node name to prevent naming conflicts + */ +export const generateQualifiedNodeName = (typeName: string, sourceFile: SourceFile): string => { + const filePath = sourceFile.getFilePath() + const fileName = basename(filePath) + const fileHash = hashString(filePath) + return `${typeName}__${fileName}__${fileHash}` +} diff --git a/tests/export-everything.test.ts b/tests/export-everything.test.ts deleted file mode 100644 index 4ad31de..0000000 --- a/tests/export-everything.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' -import { beforeEach, describe, expect, it } from 'bun:test' -import { Project } from 'ts-morph' - -describe('exportEverything flag', () => { - let project: Project - - beforeEach(() => { - project = new Project() - }) - - describe('export everything is true', () => { - it('should export all declarations', () => { - const sourceFile = project.createSourceFile( - 'test.ts', - ` - type MyType = string; - enum MyEnum { A, B, C = 'c' } - `, - ) - - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( - formatWithPrettier(` - export const MyType = Type.String(); - - export type MyType = Static; - export enum MyEnum { A, B, C = 'c' } - - export const MyEnum = Type.Enum(MyEnum); - - export type MyEnum = Static; - `), - ) - }) - - it('should export imported types', () => { - project.createSourceFile('utils.ts', 'export type ImportedType = string;') - const sourceFile = project.createSourceFile( - 'test.ts', - ` - import { ImportedType } from './utils'; - type MyType = ImportedType; - type LocalType = string; - `, - ) - - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( - formatWithPrettier(` - export const ImportedType = Type.String(); - - export type ImportedType = Static; - - export const MyType = ImportedType; - - export type MyType = Static; - - export const LocalType = Type.String(); - - export type LocalType = Static; - `), - ) - }) - - it('should export unused imported types', () => { - project.createSourceFile('unused-utils.ts', 'export type UnusedImportedType = number;') - const sourceFile = project.createSourceFile( - 'test.ts', - ` - import { UnusedImportedType } from './unused-utils'; - type MyType = string; - `, - ) - - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( - formatWithPrettier(` - export const UnusedImportedType = Type.Number(); - - export type UnusedImportedType = Static; - - export const MyType = Type.String(); - - export type MyType = Static; - `), - ) - }) - }) - - describe('export everything is false', () => { - it('should only export processed declarations', () => { - const sourceFile = project.createSourceFile( - 'test.ts', - ` - type MyType = string; - - enum MyEnum { - A, - B, - C = 'c', - } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const MyType = Type.String(); - - type MyType = Static; - enum MyEnum { - A, - B, - C = 'c', - } - - const MyEnum = Type.Enum(MyEnum); - - type MyEnum = Static; - `), - ) - }) - - it('should not export imported types', () => { - project.createSourceFile('utils.ts', 'export type ImportedType = string;') - const sourceFile = project.createSourceFile( - 'test.ts', - ` - import { ImportedType } from './utils'; - type MyType = ImportedType; - type LocalType = string; - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const ImportedType = Type.String(); - - type ImportedType = Static; - - const MyType = ImportedType; - - type MyType = Static; - - const LocalType = Type.String(); - - type LocalType = Static; - `), - ) - }) - - it('should not export unused imported types', () => { - project.createSourceFile('unused-utils.ts', 'export type UnusedImportedType = number;') - const sourceFile = project.createSourceFile( - 'test.ts', - ` - import { UnusedImportedType } from './unused-utils'; - export type MyType = string; - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - export const MyType = Type.String(); - - export type MyType = Static; - `), - ) - }) - }) -}) diff --git a/tests/handlers/typebox/advanced-types.test.ts b/tests/handlers/typebox/advanced-types.test.ts deleted file mode 100644 index e558687..0000000 --- a/tests/handlers/typebox/advanced-types.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' -import { beforeEach, describe, expect, test } from 'bun:test' -import { Project } from 'ts-morph' - -describe('Advanced types', () => { - let project: Project - - beforeEach(() => { - project = new Project() - }) - - describe('Typeof expressions', () => { - test('typeof variable', () => { - const sourceFile = createSourceFile( - project, - ` - const myVar = { x: 1, y: 'hello' } - type A = typeof myVar - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const A = myVar; - - type A = Static; - `), - ) - }) - - test('typeof with qualified name', () => { - const sourceFile = createSourceFile( - project, - ` - namespace MyNamespace { - export const config = { port: 3000 } - } - type A = typeof MyNamespace.config - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const A = MyNamespace_config; - - type A = Static; - `), - ) - }) - }) -}) diff --git a/tests/handlers/typebox/array-types.test.ts b/tests/handlers/typebox/arrays.test.ts similarity index 85% rename from tests/handlers/typebox/array-types.test.ts rename to tests/handlers/typebox/arrays.test.ts index 7111573..07848c9 100644 --- a/tests/handlers/typebox/array-types.test.ts +++ b/tests/handlers/typebox/arrays.test.ts @@ -15,9 +15,9 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Array(Type.String()); + export const A = Type.Array(Type.String()); - type A = Static; + export type A = Static; `), ) }) @@ -27,9 +27,9 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Array(Type.String()); + export const A = Type.Array(Type.String()); - type A = Static; + export type A = Static; `), ) }) @@ -45,9 +45,9 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]); + export const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]); - type A = Static; + export type A = Static; `), ) }) @@ -64,17 +64,17 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Number(); + export const A = Type.Number(); - type A = Static; + export type A = Static; - const B = Type.String(); + export const B = Type.String(); - type B = Static; + export type B = Static; - const T = Type.Union([A, B]); + export const T = Type.Union([A, B]); - type T = Static; + export type T = Static; `), ) }) @@ -93,7 +93,7 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Intersect([ + export const T = Type.Intersect([ Type.Object({ x: Type.Number(), }), @@ -102,7 +102,7 @@ describe('Array types', () => { }), ]); - type T = Static; + export type T = Static; `), ) }) @@ -112,9 +112,9 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); + export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); - type T = Static; + export type T = Static; `), ) }) diff --git a/tests/handlers/typebox/enum-types.test.ts b/tests/handlers/typebox/enum-types.test.ts deleted file mode 100644 index 15fe5a1..0000000 --- a/tests/handlers/typebox/enum-types.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' -import { beforeEach, describe, expect, test } from 'bun:test' -import { Project } from 'ts-morph' - -describe('Enum types', () => { - let project: Project - - beforeEach(() => { - project = new Project() - }) - - describe('without export', () => { - test('only enum', () => { - const sourceFile = createSourceFile( - project, - `enum A { - B, - C, - } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(`enum A { - B, - C, - } - - const A = Type.Enum(A); - - type A = Static; - `), - ) - }) - - test('enum with values', () => { - const sourceFile = createSourceFile( - project, - `enum A { - B = 'b', - C = 'c', - } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(`enum A { - B = 'b', - C = 'c', - } - - const A = Type.Enum(A); - - type A = Static; - `), - ) - }) - }) - - describe('with export', () => { - test('only enum', () => { - const sourceFile = createSourceFile( - project, - `export enum A { - B, - C, - } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(`export enum A { - B, - C, - } - - export const A = Type.Enum(A); - - export type A = Static; - `), - ) - }) - - test('enum with values', () => { - const sourceFile = createSourceFile( - project, - `export enum A { - B = 'b', - C = 'c', - } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(`export enum A { - B = 'b', - C = 'c', - } - - export const A = Type.Enum(A); - - export type A = Static; - `), - ) - }) - }) -}) diff --git a/tests/handlers/typebox/enums.test.ts b/tests/handlers/typebox/enums.test.ts new file mode 100644 index 0000000..27fa9ea --- /dev/null +++ b/tests/handlers/typebox/enums.test.ts @@ -0,0 +1,115 @@ +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Enum types', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + describe('without exports', () => { + test('only enum', () => { + const sourceFile = createSourceFile( + project, + ` + enum A { + B, + C, + } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export enum A { + B, + C, + } + + export const A = Type.Enum(A); + + export type A = Static; + `), + ) + }) + + test('enum with values', () => { + const sourceFile = createSourceFile( + project, + ` + enum A { + B = 'b', + C = 'c', + } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export enum A { + B = 'b', + C = 'c', + } + + export const A = Type.Enum(A); + + export type A = Static; + `), + ) + }) + }) + + describe('with exports', () => { + test('only enum', () => { + const sourceFile = createSourceFile( + project, + ` + export enum A { + B, + C, + } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export enum A { + B, + C, + } + + export const A = Type.Enum(A); + + export type A = Static; + `), + ) + }) + + test('enum with values', () => { + const sourceFile = createSourceFile( + project, + ` + export enum A { + B = 'b', + C = 'c', + } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export enum A { + B = 'b', + C = 'c', + } + + export const A = Type.Enum(A); + + export type A = Static; + `), + ) + }) + }) +}) diff --git a/tests/handlers/typebox/function-types.test.ts b/tests/handlers/typebox/functions.test.ts similarity index 79% rename from tests/handlers/typebox/function-types.test.ts rename to tests/handlers/typebox/functions.test.ts index d8068ca..f50d7ee 100644 --- a/tests/handlers/typebox/function-types.test.ts +++ b/tests/handlers/typebox/functions.test.ts @@ -16,9 +16,9 @@ describe('Function types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Function([], Type.String()); + export const A = Type.Function([], Type.String()); - type A = Static; + export type A = Static; `), ) }) @@ -28,9 +28,9 @@ describe('Function types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); + export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); - type A = Static; + export type A = Static; `), ) }) @@ -40,9 +40,9 @@ describe('Function types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); + export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); - type A = Static; + export type A = Static; `), ) }) @@ -52,7 +52,7 @@ describe('Function types', () => { test('simple function type', () => { const sourceFile = createSourceFile(project, `export type A = () => string`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Function([], Type.String()); @@ -67,7 +67,7 @@ describe('Function types', () => { `export type A = (x: number, y: string) => boolean`, ) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); @@ -82,7 +82,7 @@ describe('Function types', () => { `export type A = (x: number, y?: string) => void`, ) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); @@ -100,9 +100,9 @@ describe('Function types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Function([], Type.String()); + export const A = Type.Function([], Type.String()); - type A = Static; + export type A = Static; `), ) }) @@ -115,9 +115,9 @@ describe('Function types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); + export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); - type A = Static; + export type A = Static; `), ) }) @@ -127,9 +127,9 @@ describe('Function types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); + export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); - type A = Static; + export type A = Static; `), ) }) @@ -139,7 +139,7 @@ describe('Function types', () => { test('simple function declaration', () => { const sourceFile = createSourceFile(project, `export function A(): string { return '' }`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Function([], Type.String()); @@ -154,7 +154,7 @@ describe('Function types', () => { `export function A(x: number, y: string): boolean { return true }`, ) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); @@ -169,7 +169,7 @@ describe('Function types', () => { `export function A(x: number, y?: string): void { }`, ) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); diff --git a/tests/handlers/typebox/interfaces.test.ts b/tests/handlers/typebox/interfaces.test.ts index a8aa1aa..f6f9bf2 100644 --- a/tests/handlers/typebox/interfaces.test.ts +++ b/tests/handlers/typebox/interfaces.test.ts @@ -15,11 +15,11 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Object({ + export const A = Type.Object({ a: Type.String(), }); - type A = Static; + export type A = Static; `), ) }) @@ -51,20 +51,20 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const Base = Type.Object({ + export const Base = Type.Object({ id: Type.String(), }); - type Base = Static; + export type Base = Static; - const Extended = Type.Composite([ + export const Extended = Type.Composite([ Base, Type.Object({ name: Type.String(), }), ]); - type Extended = Static; + export type Extended = Static; `), ) }) @@ -81,19 +81,19 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Object({ + export const A = Type.Object({ a: Type.String(), }); - type A = Static; + export type A = Static; - const B = Type.Object({ + export const B = Type.Object({ b: Type.Number(), }); - type B = Static; + export type B = Static; - const C = Type.Composite([ + export const C = Type.Composite([ A, B, Type.Object({ @@ -101,7 +101,7 @@ describe('Interfaces', () => { }), ]); - type C = Static; + export type C = Static; `), ) }) @@ -146,18 +146,18 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const Base = Type.Object({ + export const Base = Type.Object({ id: Type.String(), }); - type Base = Static; + export type Base = Static; - const Extended = Type.Composite([ + export const Extended = Type.Composite([ Base, Type.Object({}), ]); - type Extended = Static; + export type Extended = Static; `), ) }) @@ -174,29 +174,29 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Object({ + export const A = Type.Object({ a: Type.String(), }); - type A = Static; + export type A = Static; - const B = Type.Composite([ + export const B = Type.Composite([ A, Type.Object({ b: Type.Number(), }), ]); - type B = Static; + export type B = Static; - const C = Type.Composite([ + export const C = Type.Composite([ B, Type.Object({ c: Type.Boolean(), }), ]); - type C = Static; + export type C = Static; `), ) }) @@ -214,18 +214,18 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier( ` - const A = (T: T) => Type.Object({ - a: T - }); + export const A = (T: T) => Type.Object({ + a: T + }); - type A = Static>>; + export type A = Static>>; - const B = Type.Composite([A(Type.Number()), Type.Object({ - b: Type.Number() - })]); + export const B = Type.Composite([A(Type.Number()), Type.Object({ + b: Type.Number() + })]); - type B = Static; - `, + export type B = Static; + `, true, true, ), @@ -244,18 +244,18 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier( ` - const A = (T: T) => Type.Object({ - a: T - }); + export const A = (T: T) => Type.Object({ + a: T + }); - type A = Static>>; + export type A = Static>>; - const B = (T: T) => Type.Composite([A(T), Type.Object({ - b: T - })]); + export const B = (T: T) => Type.Composite([A(T), Type.Object({ + b: T + })]); - type B = Static>>; - `, + export type B = Static>>; + `, true, true, ), @@ -277,23 +277,23 @@ describe('Interfaces', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier( ` - const A = Type.Union([Type.Literal('a'), Type.Literal('b')]) + export const A = Type.Union([Type.Literal('a'), Type.Literal('b')]) - type A = Static + export type A = Static - const B = (T: T) => Type.Object({ + export const B = (T: T) => Type.Object({ a: T }) - type B = Static>> + export type B = Static>> - const C = B(Type.Literal('a')) + export const C = B(Type.Literal('a')) - type C = Static + export type C = Static - const D = B(Type.Literal('b')) + export const D = B(Type.Literal('b')) - type D = Static + export type D = Static `, true, true, diff --git a/tests/handlers/typebox/object-types.test.ts b/tests/handlers/typebox/objects.test.ts similarity index 88% rename from tests/handlers/typebox/object-types.test.ts rename to tests/handlers/typebox/objects.test.ts index 4d99354..0cd4074 100644 --- a/tests/handlers/typebox/object-types.test.ts +++ b/tests/handlers/typebox/objects.test.ts @@ -15,11 +15,11 @@ describe('Object types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Object({ + export const A = Type.Object({ a: Type.String(), }); - type A = Static; + export type A = Static; `), ) }) @@ -29,9 +29,9 @@ describe('Object types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Tuple([Type.Number(), Type.Null()]); + export const T = Type.Tuple([Type.Number(), Type.Null()]); - type T = Static; + export type T = Static; `), ) }) diff --git a/tests/handlers/typebox/primitive-types.test.ts b/tests/handlers/typebox/primitive-types.test.ts index 1103468..7243149 100644 --- a/tests/handlers/typebox/primitive-types.test.ts +++ b/tests/handlers/typebox/primitive-types.test.ts @@ -15,9 +15,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.String(); + export const A = Type.String(); - type A = Static; + export type A = Static; `), ) }) @@ -27,9 +27,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Number(); + export const A = Type.Number(); - type A = Static; + export type A = Static; `), ) }) @@ -39,9 +39,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Boolean(); + export const A = Type.Boolean(); - type A = Static; + export type A = Static; `), ) }) @@ -51,9 +51,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Any(); + export const A = Type.Any(); - type A = Static; + export type A = Static; `), ) }) @@ -63,9 +63,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Unknown(); + export const A = Type.Unknown(); - type A = Static; + export type A = Static; `), ) }) @@ -75,9 +75,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Never(); + export const A = Type.Never(); - type A = Static; + export type A = Static; `), ) }) @@ -87,9 +87,9 @@ describe('Primitive types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Null(); + export const A = Type.Null(); - type A = Static; + export type A = Static; `), ) }) @@ -99,7 +99,7 @@ describe('Primitive types', () => { test('string', () => { const sourceFile = createSourceFile(project, `export type A = string`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.String(); @@ -111,7 +111,7 @@ describe('Primitive types', () => { test('number', () => { const sourceFile = createSourceFile(project, `export type A = number`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Number(); @@ -123,7 +123,7 @@ describe('Primitive types', () => { test('boolean', () => { const sourceFile = createSourceFile(project, `export type A = boolean`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Boolean(); @@ -135,7 +135,7 @@ describe('Primitive types', () => { test('any', () => { const sourceFile = createSourceFile(project, `export type A = any`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Any(); @@ -147,7 +147,7 @@ describe('Primitive types', () => { test('unknown', () => { const sourceFile = createSourceFile(project, `export type A = unknown`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Unknown(); @@ -159,7 +159,7 @@ describe('Primitive types', () => { test('never', () => { const sourceFile = createSourceFile(project, `export type A = never`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Never(); @@ -171,7 +171,7 @@ describe('Primitive types', () => { test('null', () => { const sourceFile = createSourceFile(project, `export type A = null`) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` export const A = Type.Null(); diff --git a/tests/handlers/typebox/template-literal-types.test.ts b/tests/handlers/typebox/template-literal-types.test.ts deleted file mode 100644 index 1186f53..0000000 --- a/tests/handlers/typebox/template-literal-types.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' -import { beforeEach, describe, expect, test } from 'bun:test' -import { Project } from 'ts-morph' - -describe('Template literals', () => { - let project: Project - - beforeEach(() => { - project = new Project() - }) - - test('one string', () => { - const sourceFile = createSourceFile(project, "type T = `${'A'}`;") - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const T = Type.TemplateLiteral([Type.Literal('A')]); - - type T = Static; - `), - ) - }) - - test('multiple strings', () => { - const sourceFile = createSourceFile(project, "type T = `${'A'|'B'}`;") - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const T = Type.TemplateLiteral([ - Type.Union([Type.Literal('A'), Type.Literal('B')]) - ]); - - type T = Static; - `), - ) - }) - - test('concatenated with literal at start', () => { - const sourceFile = createSourceFile(project, "type T = `${'A'|'B'}prop`;") - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const T = Type.TemplateLiteral([ - Type.Union([Type.Literal('A'), Type.Literal('B')]), - Type.Literal('prop'), - ]); - - type T = Static; - `), - ) - }) - - test('concatenated with literal at end', () => { - const sourceFile = createSourceFile(project, "type T = `prop${'A'|'B'}`;") - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const T = Type.TemplateLiteral([ - Type.Literal('prop'), - Type.Union([Type.Literal('A'), Type.Literal('B')]), - ]); - - type T = Static; - `), - ) - }) - - test('concatenated with numeric type', () => { - const sourceFile = createSourceFile(project, 'type T = `prop${number}`;') - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const T = Type.TemplateLiteral([Type.Literal('prop'), Type.Number()]); - - type T = Static; - `), - ) - }) - - test('concatenation before and after', () => { - const sourceFile = createSourceFile(project, 'type A = `prefix-${string}-suffix`') - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier(` - const A = Type.TemplateLiteral([ - Type.Literal("prefix-"), - Type.String(), - Type.Literal("-suffix"), - ]); - - type A = Static; - `), - ) - }) -}) diff --git a/tests/handlers/typebox/template-literals.test.ts b/tests/handlers/typebox/template-literals.test.ts new file mode 100644 index 0000000..31d53af --- /dev/null +++ b/tests/handlers/typebox/template-literals.test.ts @@ -0,0 +1,183 @@ +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Template literals', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + describe('without exports', () => { + test('one string', () => { + const sourceFile = createSourceFile(project, "type T = `${'A'}`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([Type.Literal('A')]); + + export type T = Static; + `), + ) + }) + + test('multiple strings', () => { + const sourceFile = createSourceFile(project, "type T = `${'A'|'B'}`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([ + Type.Union([Type.Literal('A'), Type.Literal('B')]) + ]); + + export type T = Static; + `), + ) + }) + + test('concatenated with literal at start', () => { + const sourceFile = createSourceFile(project, "type T = `${'A'|'B'}prop`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([ + Type.Union([Type.Literal('A'), Type.Literal('B')]), + Type.Literal('prop'), + ]); + + export type T = Static; + `), + ) + }) + + test('concatenated with literal at end', () => { + const sourceFile = createSourceFile(project, "type T = `prop${'A'|'B'}`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([ + Type.Literal('prop'), + Type.Union([Type.Literal('A'), Type.Literal('B')]), + ]); + + export type T = Static; + `), + ) + }) + + test('concatenated with numeric type', () => { + const sourceFile = createSourceFile(project, 'type T = `prop${number}`;') + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([Type.Literal('prop'), Type.Number()]); + + export type T = Static; + `), + ) + }) + + test('concatenation before and after', () => { + const sourceFile = createSourceFile(project, 'type A = `prefix-${string}-suffix`') + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = Type.TemplateLiteral([ + Type.Literal("prefix-"), + Type.String(), + Type.Literal("-suffix"), + ]); + + export type A = Static; + `), + ) + }) + }) + + describe('with exports', () => { + test('one string', () => { + const sourceFile = createSourceFile(project, "export type T = `${'A'}`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([Type.Literal('A')]); + + export type T = Static; + `), + ) + }) + + test('multiple strings', () => { + const sourceFile = createSourceFile(project, "export type T = `${'A'|'B'}`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([ + Type.Union([Type.Literal('A'), Type.Literal('B')]) + ]); + + export type T = Static; + `), + ) + }) + + test('concatenated with literal at start', () => { + const sourceFile = createSourceFile(project, "export type T = `${'A'|'B'}prop`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([ + Type.Union([Type.Literal('A'), Type.Literal('B')]), + Type.Literal('prop'), + ]); + + export type T = Static; + `), + ) + }) + + test('concatenated with literal at end', () => { + const sourceFile = createSourceFile(project, "export type T = `prop${'A'|'B'}`;") + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([ + Type.Literal('prop'), + Type.Union([Type.Literal('A'), Type.Literal('B')]), + ]); + + export type T = Static; + `), + ) + }) + + test('concatenated with numeric type', () => { + const sourceFile = createSourceFile(project, 'export type T = `prop${number}`;') + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const T = Type.TemplateLiteral([Type.Literal('prop'), Type.Number()]); + + export type T = Static; + `), + ) + }) + + test('concatenation before and after', () => { + const sourceFile = createSourceFile(project, 'export type A = `prefix-${string}-suffix`') + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = Type.TemplateLiteral([ + Type.Literal("prefix-"), + Type.String(), + Type.Literal("-suffix"), + ]); + + export type A = Static; + `), + ) + }) + }) +}) diff --git a/tests/handlers/typebox/typeof.test.ts b/tests/handlers/typebox/typeof.test.ts new file mode 100644 index 0000000..ee996b9 --- /dev/null +++ b/tests/handlers/typebox/typeof.test.ts @@ -0,0 +1,91 @@ +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Typeof expressions', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + describe('without exports', () => { + test('typeof variable', () => { + const sourceFile = createSourceFile( + project, + ` + const myVar = { x: 1, y: 'hello' } + type A = typeof myVar + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = myVar; + + export type A = Static; + `), + ) + }) + + test('typeof with qualified name', () => { + const sourceFile = createSourceFile( + project, + ` + namespace MyNamespace { + export const config = { port: 3000 } + } + type A = typeof MyNamespace.config + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = MyNamespace_config; + + export type A = Static; + `), + ) + }) + }) + + describe('with exports', () => { + test('typeof variable', () => { + const sourceFile = createSourceFile( + project, + ` + export const myVar = { x: 1, y: 'hello' } + export type A = typeof myVar + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = myVar; + + export type A = Static; + `), + ) + }) + + test('typeof with qualified name', () => { + const sourceFile = createSourceFile( + project, + ` + namespace MyNamespace { + export const config = { port: 3000 } + } + export type A = typeof MyNamespace.config + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = MyNamespace_config; + + export type A = Static; + `), + ) + }) + }) +}) diff --git a/tests/handlers/typebox/utility-types.test.ts b/tests/handlers/typebox/utility-types.test.ts index 9a7c516..d2280f5 100644 --- a/tests/handlers/typebox/utility-types.test.ts +++ b/tests/handlers/typebox/utility-types.test.ts @@ -9,7 +9,7 @@ describe('Utility', () => { project = new Project() }) - describe('without export', () => { + describe('without exports', () => { test('keyof', () => { const sourceFile = createSourceFile( project, @@ -23,14 +23,14 @@ describe('Utility', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.KeyOf( + export const T = Type.KeyOf( Type.Object({ x: Type.Number(), y: Type.String(), }), ); - type T = Static; + export type T = Static; `), ) }) @@ -40,9 +40,9 @@ describe('Utility', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Record(Type.String(), Type.Number()); + export const T = Type.Record(Type.String(), Type.Number()); - type T = Static; + export type T = Static; `), ) }) @@ -52,14 +52,14 @@ describe('Utility', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Partial( + export const T = Type.Partial( Type.Object({ a: Type.Literal(1), b: Type.Literal(2), }) ); - type T = Static; + export type T = Static; `), ) }) @@ -69,7 +69,7 @@ describe('Utility', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Pick( + export const T = Type.Pick( Type.Object({ a: Type.Literal(1), b: Type.Literal(2), @@ -77,7 +77,7 @@ describe('Utility', () => { Type.Literal("a") ); - type T = Static; + export type T = Static; `), ) }) @@ -87,7 +87,7 @@ describe('Utility', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Omit( + export const T = Type.Omit( Type.Object({ a: Type.Literal(1), b: Type.Literal(2), @@ -95,7 +95,7 @@ describe('Utility', () => { Type.Literal("a") ); - type T = Static; + export type T = Static; `), ) }) @@ -105,14 +105,14 @@ describe('Utility', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Required( + export const T = Type.Required( Type.Object({ a: Type.Optional(Type.Literal(1)), b: Type.Optional(Type.Literal(2)), }) ); - type T = Static; + export type T = Static; `), ) }) @@ -124,28 +124,27 @@ describe('Utility', () => { type A = { a: number; }; - type T = A["a"]; `, ) expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Object({ + export const A = Type.Object({ a: Type.Number(), }); - type A = Static; + export type A = Static; - const T = Type.Index(A, Type.Literal("a")); + export const T = Type.Index(A, Type.Literal("a")); - type T = Static; + export type T = Static; `), ) }) }) - describe('with export', () => { + describe('with exports', () => { test('keyof', () => { const sourceFile = createSourceFile( project, diff --git a/tests/import-resolution.test.ts b/tests/import-resolution.test.ts index a64029d..b6aaea7 100644 --- a/tests/import-resolution.test.ts +++ b/tests/import-resolution.test.ts @@ -37,20 +37,20 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const ExternalType = Type.Object({ + export const ExternalType = Type.Object({ value: Type.String(), }) - type ExternalType = Static + export type ExternalType = Static - const User = Type.Object({ + export const User = Type.Object({ id: Type.String(), name: Type.String(), local: Type.String(), external: ExternalType, }) - type User = Static + export type User = Static `), ) }) @@ -82,20 +82,20 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const ExternalType = Type.Object({ + export const ExternalType = Type.Object({ value: Type.String(), }) - type ExternalType = Static + export type ExternalType = Static - const User = Type.Object({ + export const User = Type.Object({ id: Type.String(), name: Type.String(), local: Type.String(), external: ExternalType, }) - type User = Static + export type User = Static `), ) }) @@ -138,25 +138,25 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const DeeplyNestedType = Type.Object({ + export const DeeplyNestedType = Type.Object({ value: Type.Boolean(), }) - type DeeplyNestedType = Static + export type DeeplyNestedType = Static - const IntermediateType = Type.Object({ + export const IntermediateType = Type.Object({ id: Type.String(), nested: DeeplyNestedType, }) - type IntermediateType = Static + export type IntermediateType = Static - const FinalUser = Type.Object({ + export const FinalUser = Type.Object({ name: Type.String(), data: IntermediateType, }) - type FinalUser = Static + export type FinalUser = Static `), ) }) @@ -212,32 +212,32 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const VeryDeeplyNestedType = Type.Object({ + export const VeryDeeplyNestedType = Type.Object({ core: Type.String(), }) - type VeryDeeplyNestedType = Static + export type VeryDeeplyNestedType = Static - const DeeplyNestedType = Type.Object({ + export const DeeplyNestedType = Type.Object({ value: Type.Boolean(), veryDeep: VeryDeeplyNestedType, }) - type DeeplyNestedType = Static + export type DeeplyNestedType = Static - const IntermediateType = Type.Object({ + export const IntermediateType = Type.Object({ id: Type.String(), nested: DeeplyNestedType, }) - type IntermediateType = Static + export type IntermediateType = Static - const UltimateUser = Type.Object({ + export const UltimateUser = Type.Object({ name: Type.String(), data: IntermediateType, }) - type UltimateUser = Static + export type UltimateUser = Static `), ) }) @@ -271,11 +271,11 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const ExternalType = Type.Object({ + export const ExternalType = Type.Object({ value: Type.String(), }) - type ExternalType = Static + export type ExternalType = Static export const User = Type.Object({ id: Type.String(), @@ -316,11 +316,11 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const ExternalType = Type.Object({ + export const ExternalType = Type.Object({ value: Type.String(), }) - type ExternalType = Static + export type ExternalType = Static export const User = Type.Object({ id: Type.String(), @@ -372,18 +372,18 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const DeeplyNestedType = Type.Object({ + export const DeeplyNestedType = Type.Object({ value: Type.Boolean(), }) - type DeeplyNestedType = Static + export type DeeplyNestedType = Static - const IntermediateType = Type.Object({ + export const IntermediateType = Type.Object({ id: Type.String(), nested: DeeplyNestedType, }) - type IntermediateType = Static + export type IntermediateType = Static export const FinalUser = Type.Object({ name: Type.String(), @@ -446,25 +446,25 @@ describe('ts-morph codegen with imports', () => { expect(generateFormattedCode(userFile)).resolves.toBe( formatWithPrettier(` - const VeryDeeplyNestedType = Type.Object({ + export const VeryDeeplyNestedType = Type.Object({ core: Type.String(), }) - type VeryDeeplyNestedType = Static + export type VeryDeeplyNestedType = Static - const DeeplyNestedType = Type.Object({ + export const DeeplyNestedType = Type.Object({ value: Type.Boolean(), veryDeep: VeryDeeplyNestedType, }) - type DeeplyNestedType = Static + export type DeeplyNestedType = Static - const IntermediateType = Type.Object({ + export const IntermediateType = Type.Object({ id: Type.String(), nested: DeeplyNestedType, }) - type IntermediateType = Static + export type IntermediateType = Static export const UltimateUser = Type.Object({ name: Type.String(), diff --git a/tests/traverse/dependency-collector.integration.test.ts b/tests/traverse/dependency-collector.integration.test.ts index 2f296e1..c8e19e5 100644 --- a/tests/traverse/dependency-collector.integration.test.ts +++ b/tests/traverse/dependency-collector.integration.test.ts @@ -1,429 +1,431 @@ -// 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') -// }) -// }) -// }) +import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' +import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { createSourceFile } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +const getNodeName = (traversedNode: TraversedNode): string => { + return traversedNode.originalName +} + +describe('DependencyCollector', () => { + let project: Project + let traverser: DependencyTraversal + + beforeEach(() => { + project = new Project() + traverser = new DependencyTraversal() + }) + + describe('collectFromImports', () => { + test('should collect dependencies from single import', () => { + 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() + traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.extractDependencies() + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(1) + expect(getNodeName(dependencies[0]!)).toBe('User') + expect(dependencies[0]!.isImported).toBe(true) + }) + + 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() + traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.extractDependencies() + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(2) + const typeNames = dependencies.map((d) => getNodeName(d)) + 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() + traverser.collectFromImports(importDeclarations, true, mainFile) + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(2) + const typeNames = dependencies.map((d) => getNodeName(d)) + 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() + traverser.collectFromImports(importDeclarations, true, mainFile) + const dependencies = traverser.getNodesToPrint() + + 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() + traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.extractDependencies() + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(1) + expect(getNodeName(dependencies[0]!)).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; + }; + `, + ) + + traverser.addLocalTypes(sourceFile) + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(2) + const typeNames = dependencies.map((d) => getNodeName(d)) + expect(typeNames).toContain('LocalUser') + expect(typeNames).toContain('LocalProduct') + + dependencies.forEach((dep) => { + expect(dep!.isImported).toBe(false) + }) + }) + + test('should not duplicate existing types', () => { + const sourceFile = createSourceFile( + project, + ` + type User = { + id: string; + name: string; + }; + `, + ) + + traverser.addLocalTypes(sourceFile) + traverser.addLocalTypes(sourceFile) + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(1) + expect(getNodeName(dependencies[0]!)).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() + traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.extractDependencies() + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(2) + expect(getNodeName(dependencies[0]!)).toBe('BaseType') + expect(getNodeName(dependencies[1]!)).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() + traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.extractDependencies() + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(3) + expect(getNodeName(dependencies[0]!)).toBe('A') + expect(getNodeName(dependencies[1]!)).toBe('B') + expect(getNodeName(dependencies[2]!)).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() + traverser.collectFromImports(importDeclarations, true, mainFile) + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(2) + const typeNames = dependencies.map((d) => getNodeName(d)) + 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() + traverser.collectFromImports(importDeclarations, true, mainFile) + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(1) + expect(getNodeName(dependencies[0]!)).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() + + traverser.addLocalTypes(mainFile) + traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.extractDependencies() + const dependencies = traverser.getNodesToPrint() + + expect(dependencies).toHaveLength(2) + + const externalDep = dependencies.find((d) => getNodeName(d) === 'ExternalType') + const localDep = dependencies.find((d) => getNodeName(d) === 'LocalType') + + expect(externalDep!.isImported).toBe(true) + expect(localDep!.isImported).toBe(false) + + expect(getNodeName(dependencies[0]!)).toBe('ExternalType') + expect(getNodeName(dependencies[1]!)).toBe('LocalType') + }) + }) +}) diff --git a/tests/traverse/dependency-collector.unit.test.ts b/tests/traverse/dependency-collector.unit.test.ts deleted file mode 100644 index 7e6811a..0000000 --- a/tests/traverse/dependency-collector.unit.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { - DefaultFileResolver, - FileResolver, -} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver' -import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' -import { describe, expect, mock, test } from 'bun:test' -import type { ImportDeclaration, SourceFile, TypeAliasDeclaration, TypeNode } from 'ts-morph' - -describe('DependencyTraversal Unit Tests', () => { - const createMockTypeAlias = (name: string): TypeAliasDeclaration => { - return { - getName: () => name, - getTypeNode: () => undefined, - } as unknown as TypeAliasDeclaration - } - - const createMockSourceFile = (filePath: string): SourceFile => { - return { - getFilePath: () => filePath, - } as SourceFile - } - - const createMockImportDeclaration = (): ImportDeclaration => { - return {} as ImportDeclaration - } - - describe('constructor', () => { - test('should initialize with default dependencies', () => { - const collector = new DependencyTraversal() - expect(collector.getDependencies().size).toBe(0) - expect(collector.getVisitedFiles().size).toBe(0) - }) - - test('should accept custom file resolver', () => { - const mockFileResolver = {} as DefaultFileResolver - const collector = new DependencyTraversal(mockFileResolver) - expect(collector.getDependencies().size).toBe(0) - }) - }) - - describe('getDependencies', () => { - test('should return a copy of dependencies map', () => { - const collector = new DependencyTraversal() - const mockTypeAlias = createMockTypeAlias('TestType') - const mockSourceFile = createMockSourceFile('/test.ts') - - collector.addLocalTypes([mockTypeAlias], mockSourceFile) - - const dependencies1 = collector.getDependencies() - const dependencies2 = collector.getDependencies() - - expect(dependencies1).not.toBe(dependencies2) - expect(dependencies1.size).toBe(1) - expect(dependencies2.size).toBe(1) - }) - }) - - describe('getVisitedFiles', () => { - test('should return a copy of visited files set', () => { - const collector = new DependencyTraversal() - - const visitedFiles1 = collector.getVisitedFiles() - const visitedFiles2 = collector.getVisitedFiles() - - expect(visitedFiles1).not.toBe(visitedFiles2) - }) - }) - - describe('addLocalTypes', () => { - test('should add local types to dependencies', () => { - const collector = new DependencyTraversal() - const mockTypeAlias = createMockTypeAlias('LocalType') - const mockSourceFile = createMockSourceFile('/local.ts') - - collector.addLocalTypes([mockTypeAlias], mockSourceFile) - - const dependencies = collector.getDependencies() - expect(dependencies.size).toBe(1) - const dependency = dependencies.get('LocalType') - expect(dependency?.typeAlias.getName()).toBe('LocalType') - expect(dependency?.isImported).toBe(false) - }) - - test('should not add duplicate types', () => { - const collector = new DependencyTraversal() - const mockTypeAlias1 = createMockTypeAlias('DuplicateType') - const mockTypeAlias2 = createMockTypeAlias('DuplicateType') - const mockSourceFile = createMockSourceFile('/local.ts') - - collector.addLocalTypes([mockTypeAlias1], mockSourceFile) - collector.addLocalTypes([mockTypeAlias2], mockSourceFile) - - expect(collector.getDependencies().size).toBe(1) - }) - }) - - describe('collectFromImports', () => { - test('should collect dependencies from imports using file resolver', () => { - const mockFileResolver: FileResolver = { - getModuleSpecifierSourceFile: mock(() => { - const mockSourceFile = createMockSourceFile('/imported.ts') - return mockSourceFile - }), - getFilePath: mock((sourceFile: SourceFile) => sourceFile.getFilePath()), - getImportDeclarations: mock(() => []), - getTypeAliases: mock(() => [ - createMockTypeAlias('ImportedType1'), - createMockTypeAlias('ImportedType2'), - ]), - } - - const collector = new DependencyTraversal(mockFileResolver) - const mockImport = createMockImportDeclaration() - - const result = collector.collectFromImports([mockImport]) - - expect(mockFileResolver.getModuleSpecifierSourceFile).toHaveBeenCalledWith(mockImport) - expect(mockFileResolver.getTypeAliases).toHaveBeenCalled() - expect(result).toHaveLength(2) - expect(collector.getDependencies().size).toBe(2) - }) - - test('should handle missing source files', () => { - const mockFileResolver: FileResolver = { - getModuleSpecifierSourceFile: mock(() => undefined), - getFilePath: mock(() => ''), - getImportDeclarations: mock(() => []), - getTypeAliases: mock(() => []), - } - - const collector = new DependencyTraversal(mockFileResolver) - const mockImport = createMockImportDeclaration() - - const result = collector.collectFromImports([mockImport]) - - expect(result).toHaveLength(0) - expect(collector.getDependencies().size).toBe(0) - }) - - test('should prevent infinite recursion with circular imports', () => { - const mockFileResolver: FileResolver = { - getModuleSpecifierSourceFile: mock(() => { - const mockSourceFile = createMockSourceFile('/circular.ts') - return mockSourceFile - }), - getFilePath: mock(() => '/circular.ts'), - getImportDeclarations: mock(() => [createMockImportDeclaration()]), - getTypeAliases: mock(() => [createMockTypeAlias('CircularType')]), - } - - const collector = new DependencyTraversal(mockFileResolver) - const mockImport = createMockImportDeclaration() - - const result = collector.collectFromImports([mockImport]) - - expect(result).toHaveLength(1) - expect(collector.getVisitedFiles().has('/circular.ts')).toBe(true) - }) - }) - - describe('topological sort integration', () => { - test('should handle dependency resolution', () => { - const mockTypeAlias1 = createMockTypeAlias('TypeA') - const mockTypeAlias2 = createMockTypeAlias('ReferencedType') - - mockTypeAlias1.getTypeNode = mock(() => ({}) as TypeNode) - mockTypeAlias2.getTypeNode = mock(() => undefined) - - const collector = new DependencyTraversal() - const mockSourceFile = createMockSourceFile('/test.ts') - - collector.addLocalTypes([mockTypeAlias1, mockTypeAlias2], mockSourceFile) - - const dependencies = collector.getDependencies() - expect(dependencies.size).toBe(2) - }) - }) -}) diff --git a/tests/traverse/dependency-ordering.test.ts b/tests/traverse/dependency-ordering.test.ts index 99b6a13..7e31fec 100644 --- a/tests/traverse/dependency-ordering.test.ts +++ b/tests/traverse/dependency-ordering.test.ts @@ -1,92 +1,92 @@ -// import { formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' -// import { beforeEach, describe, expect, test } from 'bun:test' -// import { Project } from 'ts-morph' - -// describe('Dependency Ordering Bug', () => { -// let project: Project - -// beforeEach(() => { -// project = new Project() -// }) - -// test('should define StringSnakDataValue before using it', () => { -// const sourceFile = project.createSourceFile( -// 'test.ts', -// ` -// export type CommonsMediaSnakDataValue = StringSnakDataValue; -// export type StringSnakDataValue = { -// value: string; -// type: 'string'; -// }; -// `, -// ) - -// expect(generateFormattedCode(sourceFile, true)).resolves.toBe( -// formatWithPrettier(` -// export const StringSnakDataValue = Type.Object({ -// value: Type.String(), -// type: Type.Literal('string'), -// }); - -// export type StringSnakDataValue = Static; - -// export const CommonsMediaSnakDataValue = StringSnakDataValue; - -// export type CommonsMediaSnakDataValue = Static; -// `), -// ) -// }) - -// test('should handle complex dependency chains correctly', () => { -// const sourceFile = project.createSourceFile( -// 'test.ts', -// ` -// export type CommonsMediaSnakDataValue = StringSnakDataValue; -// export type ExternalIdSnakDataValue = StringSnakDataValue; -// export type GeoShapeSnakDataValue = StringSnakDataValue; -// export type StringSnakDataValue = { -// value: string; -// type: 'string'; -// }; -// export type DataValueByDataType = { -// 'string': StringSnakDataValue; -// 'commonsMedia': CommonsMediaSnakDataValue; -// 'external-id': ExternalIdSnakDataValue; -// 'geo-shape': GeoShapeSnakDataValue; -// }; -// `, -// ) - -// expect(generateFormattedCode(sourceFile, true)).resolves.toBe( -// formatWithPrettier(` -// export const StringSnakDataValue = Type.Object({ -// value: Type.String(), -// type: Type.Literal('string'), -// }); - -// export type StringSnakDataValue = Static; - -// export const CommonsMediaSnakDataValue = StringSnakDataValue; - -// export type CommonsMediaSnakDataValue = Static; - -// export const ExternalIdSnakDataValue = StringSnakDataValue; - -// export type ExternalIdSnakDataValue = Static; - -// export const GeoShapeSnakDataValue = StringSnakDataValue; - -// export type GeoShapeSnakDataValue = Static; - -// export const DataValueByDataType = Type.Object({ -// "'string'": StringSnakDataValue, -// "'commonsMedia'": CommonsMediaSnakDataValue, -// "'external-id'": ExternalIdSnakDataValue, -// "'geo-shape'": GeoShapeSnakDataValue, -// }); - -// export type DataValueByDataType = Static; -// `), -// ) -// }) -// }) +import { formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Dependency Ordering Bug', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + test('should define StringSnakDataValue before using it', () => { + const sourceFile = project.createSourceFile( + 'test.ts', + ` + export type CommonsMediaSnakDataValue = StringSnakDataValue; + export type StringSnakDataValue = { + value: string; + type: 'string'; + }; + `, + ) + + expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + formatWithPrettier(` + export const StringSnakDataValue = Type.Object({ + value: Type.String(), + type: Type.Literal('string'), + }); + + export type StringSnakDataValue = Static; + + export const CommonsMediaSnakDataValue = StringSnakDataValue; + + export type CommonsMediaSnakDataValue = Static; + `), + ) + }) + + test('should handle complex dependency chains correctly', () => { + const sourceFile = project.createSourceFile( + 'test.ts', + ` + export type CommonsMediaSnakDataValue = StringSnakDataValue; + export type ExternalIdSnakDataValue = StringSnakDataValue; + export type GeoShapeSnakDataValue = StringSnakDataValue; + export type StringSnakDataValue = { + value: string; + type: 'string'; + }; + export type DataValueByDataType = { + 'string': StringSnakDataValue; + 'commonsMedia': CommonsMediaSnakDataValue; + 'external-id': ExternalIdSnakDataValue; + 'geo-shape': GeoShapeSnakDataValue; + }; + `, + ) + + expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + formatWithPrettier(` + export const StringSnakDataValue = Type.Object({ + value: Type.String(), + type: Type.Literal('string'), + }); + + export type StringSnakDataValue = Static; + + export const CommonsMediaSnakDataValue = StringSnakDataValue; + + export type CommonsMediaSnakDataValue = Static; + + export const ExternalIdSnakDataValue = StringSnakDataValue; + + export type ExternalIdSnakDataValue = Static; + + export const GeoShapeSnakDataValue = StringSnakDataValue; + + export type GeoShapeSnakDataValue = Static; + + export const DataValueByDataType = Type.Object({ + "'string'": StringSnakDataValue, + "'commonsMedia'": CommonsMediaSnakDataValue, + "'external-id'": ExternalIdSnakDataValue, + "'geo-shape'": GeoShapeSnakDataValue, + }); + + export type DataValueByDataType = Static; + `), + ) + }) +}) diff --git a/tests/utils.ts b/tests/utils.ts index 4309c56..d333e9a 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,4 +1,4 @@ -import { generateCode } from '@daxserver/validation-schema-codegen/ts-morph-codegen' +import { generateCode } from '@daxserver/validation-schema-codegen' import synchronizedPrettier from '@prettier/sync' import { Project, SourceFile } from 'ts-morph' @@ -25,12 +25,10 @@ export const formatWithPrettier = ( export const generateFormattedCode = async ( sourceFile: SourceFile, - exportEverything: boolean = false, withTSchema: boolean = false, ): Promise => { const code = await generateCode({ sourceCode: sourceFile.getFullText(), - exportEverything, callerFile: sourceFile.getFilePath(), project: sourceFile.getProject(), }) diff --git a/tsconfig.json b/tsconfig.json index e7df132..c61aed7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "noPropertyAccessFromIndexSignature": false, "baseUrl": ".", "paths": { + "@daxserver/validation-schema-codegen": ["./src/index.ts"], "@daxserver/validation-schema-codegen/*": ["./src/*"], "@test-fixtures/*": ["./tests/*"] } From e6d43b16151f3094abebf632eccbd9e413578cec Mon Sep 17 00:00:00 2001 From: DaxServer Date: Thu, 28 Aug 2025 18:34:14 +0200 Subject: [PATCH 2/2] feat: apply code review suggestions --- ARCHITECTURE.md | 16 ++- src/index.ts | 27 ++--- src/parsers/parse-enums.ts | 5 +- src/parsers/parse-function-declarations.ts | 18 +-- src/parsers/parse-interfaces.ts | 9 +- src/parsers/parse-type-aliases.ts | 7 +- src/printer/typebox-printer.ts | 3 +- src/traverse/dependency-traversal.ts | 109 +++++++----------- src/traverse/file-graph.ts | 7 +- src/traverse/node-graph.ts | 2 +- src/traverse/types.ts | 11 +- src/utils/generate-qualified-name.ts | 6 +- tests/handlers/typebox/arrays.test.ts | 24 ++-- tests/handlers/typebox/enums.test.ts | 24 ++-- tests/handlers/typebox/functions.test.ts | 24 ++-- tests/handlers/typebox/interfaces.test.ts | 20 ++-- tests/handlers/typebox/objects.test.ts | 8 +- .../handlers/typebox/primitive-types.test.ts | 28 ++--- .../typebox/template-literals.test.ts | 24 ++-- tests/handlers/typebox/typeof.test.ts | 8 +- tests/handlers/typebox/utility-types.test.ts | 28 ++--- tests/import-resolution.test.ts | 16 +-- .../dependency-collector.integration.test.ts | 64 +++------- tests/traverse/dependency-ordering.test.ts | 4 +- tests/utils.ts | 6 +- 25 files changed, 208 insertions(+), 290 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 24d185f..812944d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -80,6 +80,7 @@ The parser system is built around a base class architecture in provides: + - Common interface for all parsers - Shared access to output `SourceFile`, TypeScript printer, and processed types tracking - Abstract `parse` method for implementation by specific parsers @@ -111,6 +112,7 @@ The handler system in class orchestrates all handlers through: + - **Handler Caching**: Caches handler instances for performance optimization - **Fallback System**: Provides fallback handlers for complex cases @@ -211,10 +213,12 @@ The directory provides essential 1. **Input**: A TypeScript source file containing `enum`, `type alias`, `interface`, and `function` declarations. 2. **Parsing**: `ts-morph` parses the input TypeScript file into an Abstract Syntax Tree (AST). 3. **Centralized Dependency Traversal**: The `DependencyTraversal.startTraversal()` method orchestrates the complete dependency collection and ordering process: - - Collects local types from the main source file - - Recursively traverses import chains to gather all dependencies - - Establishes dependency relationships between all types - - Returns topologically sorted nodes for processing + +- Collects local types from the main source file +- Recursively traverses import chains to gather all dependencies +- Establishes dependency relationships between all types +- Returns topologically sorted nodes for processing + 4. **Sequential Node Processing**: The sorted nodes are processed sequentially using specialized parsers, ensuring dependencies are handled before dependent types. 5. **TypeBox Schema Generation**: For each node, the corresponding TypeBox schema is constructed using appropriate `Type` methods (e.g., `Type.Enum`, `Type.Object`, `Type.Function`, etc.). This process involves sophisticated mapping of TypeScript types to their TypeBox equivalents. 6. **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. @@ -223,7 +227,7 @@ The directory provides essential ## Basic Usage ```typescript -const result = await generateCode({ +const result = generateCode({ sourceCode: sourceFile.getFullText(), callerFile: sourceFile.getFilePath(), }) @@ -232,7 +236,7 @@ const result = await generateCode({ ### Using File Path ```typescript -const result = await generateCode({ +const result = generateCode({ filePath: './types.ts', }) ``` diff --git a/src/index.ts b/src/index.ts index ea0c0bf..3c8441f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { import { TypeBoxPrinter } from '@daxserver/validation-schema-codegen/printer/typebox-printer' import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' -import { Project, SourceFile, ts } from 'ts-morph' +import { Node, Project, SourceFile, ts } from 'ts-morph' const createOutputFile = (hasGenericInterfaces: boolean) => { const newSourceFile = new Project().createSourceFile('output.ts', '', { @@ -36,10 +36,7 @@ const createOutputFile = (hasGenericInterfaces: boolean) => { return newSourceFile } -const printSortedNodes = ( - sortedTraversedNodes: TraversedNode[], - newSourceFile: SourceFile, -) => { +const printSortedNodes = (sortedTraversedNodes: TraversedNode[], newSourceFile: SourceFile) => { const printer = new TypeBoxPrinter({ newSourceFile, printer: ts.createPrinter(), @@ -53,11 +50,7 @@ const printSortedNodes = ( return newSourceFile.getFullText() } -export const generateCode = async ({ - sourceCode, - filePath, - ...options -}: InputOptions): Promise => { +export const generateCode = ({ sourceCode, filePath, ...options }: InputOptions): string => { // Create source file from input const sourceFile = createSourceFileFromInput({ sourceCode, @@ -65,18 +58,18 @@ export const generateCode = async ({ ...options, }) + // Create dependency traversal and start traversal + const dependencyTraversal = new DependencyTraversal() + const traversedNodes = dependencyTraversal.startTraversal(sourceFile) + // Check if any interfaces have generic type parameters - const hasGenericInterfaces = sourceFile - .getInterfaces() - .some((i) => i.getTypeParameters().length > 0) + const hasGenericInterfaces = traversedNodes.some( + (t) => Node.isInterfaceDeclaration(t.node) && t.node.getTypeParameters().length > 0, + ) // Create output file with proper imports const newSourceFile = createOutputFile(hasGenericInterfaces) - // Create dependency traversal and start traversal - const dependencyTraversal = new DependencyTraversal() - const traversedNodes = dependencyTraversal.startTraversal(sourceFile) - // Print sorted nodes to output const result = printSortedNodes(traversedNodes, newSourceFile) diff --git a/src/parsers/parse-enums.ts b/src/parsers/parse-enums.ts index fcb4cae..7ddc6b8 100644 --- a/src/parsers/parse-enums.ts +++ b/src/parsers/parse-enums.ts @@ -5,6 +5,7 @@ import { EnumDeclaration, VariableDeclarationKind } from 'ts-morph' export class EnumParser extends BaseParser { parse(enumDeclaration: EnumDeclaration): void { const enumName = enumDeclaration.getName() + const schemaName = `${enumName}Schema` this.newSourceFile.addEnum({ name: enumName, @@ -23,7 +24,7 @@ export class EnumParser extends BaseParser { declarationKind: VariableDeclarationKind.Const, declarations: [ { - name: enumName, + name: schemaName, initializer: typeboxType, }, ], @@ -31,7 +32,7 @@ export class EnumParser extends BaseParser { addStaticTypeAlias( this.newSourceFile, - enumName, + schemaName, this.newSourceFile.compilerNode, this.printer, ) diff --git a/src/parsers/parse-function-declarations.ts b/src/parsers/parse-function-declarations.ts index b7ca5fa..7259851 100644 --- a/src/parsers/parse-function-declarations.ts +++ b/src/parsers/parse-function-declarations.ts @@ -6,24 +6,10 @@ import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph' export class FunctionDeclarationParser extends BaseParser { parse(functionDecl: FunctionDeclaration): void { - this.parseWithImportFlag(functionDecl) - } - - parseWithImportFlag(functionDecl: FunctionDeclaration): void { - this.parseFunctionWithImportFlag(functionDecl) - } - - private parseFunctionWithImportFlag( - functionDecl: FunctionDeclaration, - ): void { const functionName = functionDecl.getName() - if (!functionName) { - return - } + if (!functionName) return - if (this.processedTypes.has(functionName)) { - return - } + if (this.processedTypes.has(functionName)) return this.processedTypes.add(functionName) // Get function parameters and return type diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts index 5cf8c94..46a77e2 100644 --- a/src/parsers/parse-interfaces.ts +++ b/src/parsers/parse-interfaces.ts @@ -86,10 +86,7 @@ export class InterfaceParser extends BaseParser { this.addGenericTypeAlias(interfaceName, typeParameters) } - private addGenericTypeAlias( - name: string, - typeParameters: TypeParameterDeclaration[], - ): void { + private addGenericTypeAlias(name: string, typeParameters: TypeParameterDeclaration[]): void { // Create type parameters for the type alias const typeParamDeclarations = typeParameters.map((typeParam) => { const paramName = typeParam.getName() @@ -142,8 +139,8 @@ export class InterfaceParser extends BaseParser { name, typeParameters: typeParamDeclarations.map((tp) => this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode), - ), - type: staticType, + ), + type: staticType, }) } } diff --git a/src/parsers/parse-type-aliases.ts b/src/parsers/parse-type-aliases.ts index 9cb3e94..9fd5695 100644 --- a/src/parsers/parse-type-aliases.ts +++ b/src/parsers/parse-type-aliases.ts @@ -31,11 +31,6 @@ export class TypeAliasParser extends BaseParser { ], }) - addStaticTypeAlias( - this.newSourceFile, - typeName, - this.newSourceFile.compilerNode, - this.printer, - ) + addStaticTypeAlias(this.newSourceFile, typeName, this.newSourceFile.compilerNode, this.printer) } } diff --git a/src/printer/typebox-printer.ts b/src/printer/typebox-printer.ts index c3796aa..034fb55 100644 --- a/src/printer/typebox-printer.ts +++ b/src/printer/typebox-printer.ts @@ -2,6 +2,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 type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' import { Node, SourceFile, ts } from 'ts-morph' export interface PrinterOptions { @@ -35,7 +36,7 @@ export class TypeBoxPrinter { this.functionParser = new FunctionDeclarationParser(parserOptions) } - printNode(traversedNode: { node: Node; isImported?: boolean }): void { + printNode(traversedNode: TraversedNode): void { const { node } = traversedNode switch (true) { diff --git a/src/traverse/dependency-traversal.ts b/src/traverse/dependency-traversal.ts index 40576c3..82d5d48 100644 --- a/src/traverse/dependency-traversal.ts +++ b/src/traverse/dependency-traversal.ts @@ -3,7 +3,14 @@ import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-gr import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name' import { topologicalSort } from 'graphology-dag' -import { ImportDeclaration, InterfaceDeclaration, Node, SourceFile, TypeAliasDeclaration, TypeReferenceNode } from 'ts-morph' +import { + ImportDeclaration, + InterfaceDeclaration, + Node, + SourceFile, + TypeAliasDeclaration, + TypeReferenceNode, +} from 'ts-morph' /** * Dependency traversal class for AST traversal, dependency collection, and analysis @@ -13,8 +20,6 @@ export class DependencyTraversal { private fileGraph = new FileGraph() private nodeGraph = new NodeGraph() - constructor() {} - /** * Start the traversal process from the main source file * This method handles the complete recursive traversal and returns sorted nodes @@ -25,7 +30,7 @@ export class DependencyTraversal { // Start recursive traversal from imports const importDeclarations = mainSourceFile.getImportDeclarations() - this.collectFromImports(importDeclarations, true, mainSourceFile) + this.collectFromImports(importDeclarations, true) // Extract dependencies for all nodes this.extractDependencies() @@ -102,7 +107,6 @@ export class DependencyTraversal { isMainCode, }) } - } /** @@ -114,54 +118,36 @@ export class DependencyTraversal { const nodeData = this.nodeGraph.getNode(nodeId) if (nodeData.type === 'typeAlias') { - const typeAlias = nodeData.node as TypeAliasDeclaration - const typeNode = typeAlias.getTypeNode() - if (!typeNode) continue - - const typeReferences = this.extractTypeReferences(typeNode) - - // Add edges for dependencies - for (const referencedType of typeReferences) { - if (this.nodeGraph.hasNode(referencedType)) { - this.nodeGraph.addDependency(referencedType, nodeId) - } - } - } else if (nodeData.type === 'interface') { - const interfaceDecl = nodeData.node as InterfaceDeclaration - const typeReferences = this.extractTypeReferences(interfaceDecl) - - // Add edges for dependencies - for (const referencedType of typeReferences) { - if (this.nodeGraph.hasNode(referencedType)) { - this.nodeGraph.addDependency(referencedType, nodeId) - } - } - } - } - } - + const typeAlias = nodeData.node as TypeAliasDeclaration + const typeNode = typeAlias.getTypeNode() + if (!typeNode) continue - /** - * Check if a type is used in the source file - */ - private isTypeUsedInSourceFile(typeName: string, sourceFile: SourceFile): boolean { - const typeReferences: string[] = [] + const typeReferences = this.extractTypeReferences(typeNode) - sourceFile.forEachDescendant((node) => { - if (Node.isTypeReference(node)) { - const typeRefNode = node as TypeReferenceNode - const referencedTypeName = typeRefNode.getTypeName().getText() - typeReferences.push(referencedTypeName) + // Add edges for dependencies + for (const referencedType of typeReferences) { + if (this.nodeGraph.hasNode(referencedType)) { + this.nodeGraph.addDependency(referencedType, nodeId) + } + } + } else if (nodeData.type === 'interface') { + const interfaceDecl = nodeData.node as InterfaceDeclaration + const typeReferences = this.extractTypeReferences(interfaceDecl) + + // Add edges for dependencies + for (const referencedType of typeReferences) { + if (this.nodeGraph.hasNode(referencedType)) { + this.nodeGraph.addDependency(referencedType, nodeId) + } + } } - }) - - return typeReferences.includes(typeName) + } } /** * Collect dependencies from import declarations */ - collectFromImports(importDeclarations: ImportDeclaration[], isDirectImport: boolean = true, mainSourceFile?: SourceFile): void { + collectFromImports(importDeclarations: ImportDeclaration[], isMainCode: boolean): void { for (const importDecl of importDeclarations) { const moduleSourceFile = importDecl.getModuleSpecifierSourceFile() if (!moduleSourceFile) continue @@ -183,45 +169,42 @@ export class DependencyTraversal { for (const typeAlias of typeAliases) { const typeName = typeAlias.getName() const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) - const isRootImport = isDirectImport this.nodeGraph.addTypeNode(qualifiedName, { node: typeAlias, type: 'typeAlias', originalName: typeName, qualifiedName, isImported: true, - isDirectImport, - isRootImport, + isMainCode, }) } for (const interfaceDecl of interfaces) { const interfaceName = interfaceDecl.getName() - const qualifiedName = generateQualifiedNodeName(interfaceName, interfaceDecl.getSourceFile()) - const isRootImport = isDirectImport + const qualifiedName = generateQualifiedNodeName( + interfaceName, + interfaceDecl.getSourceFile(), + ) this.nodeGraph.addTypeNode(qualifiedName, { node: interfaceDecl, type: 'interface', originalName: interfaceName, qualifiedName, isImported: true, - isDirectImport, - isRootImport, + isMainCode, }) } for (const enumDecl of enums) { const enumName = enumDecl.getName() const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) - const isRootImport = isDirectImport this.nodeGraph.addTypeNode(qualifiedName, { node: enumDecl, type: 'enum', originalName: enumName, qualifiedName, isImported: true, - isDirectImport, - isRootImport, + isMainCode, }) } @@ -230,20 +213,18 @@ export class DependencyTraversal { if (!functionName) continue const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) - const isRootImport = isDirectImport this.nodeGraph.addTypeNode(qualifiedName, { node: functionDecl, type: 'function', originalName: functionName, qualifiedName, isImported: true, - isDirectImport, - isRootImport, + isMainCode, }) } // Recursively collect from nested imports (mark as transitive) - this.collectFromImports(imports, false, mainSourceFile) + this.collectFromImports(imports, false) } } @@ -255,9 +236,7 @@ export class DependencyTraversal { try { // Use topological sort to ensure dependencies are printed first const sortedNodeIds = topologicalSort(this.nodeGraph) - return sortedNodeIds.map((nodeId: string) => - this.nodeGraph.getNodeAttributes(nodeId), - ) + return sortedNodeIds.map((nodeId: string) => this.nodeGraph.getNodeAttributes(nodeId)) } catch { // Handle circular dependencies by returning nodes in insertion order // This ensures dependencies are still processed before dependents when possible @@ -267,9 +246,7 @@ export class DependencyTraversal { } } - - - private extractTypeReferences(typeNode: Node): string[] { + private extractTypeReferences(node: Node): string[] { const references: string[] = [] const visited = new Set() @@ -295,7 +272,7 @@ export class DependencyTraversal { node.forEachChild(traverse) } - traverse(typeNode) + traverse(node) return references } diff --git a/src/traverse/file-graph.ts b/src/traverse/file-graph.ts index 2aaae6b..2f564c1 100644 --- a/src/traverse/file-graph.ts +++ b/src/traverse/file-graph.ts @@ -1,11 +1,16 @@ import { DirectedGraph } from 'graphology' import type { SourceFile } from 'ts-morph' +type FileNodeAttributes = { + type: 'file' + sourceFile: SourceFile +} + /** * Graph for managing file dependencies * Tracks relationships between source files */ -export class FileGraph extends DirectedGraph { +export class FileGraph extends DirectedGraph { /** * Add a file to the graph */ diff --git a/src/traverse/node-graph.ts b/src/traverse/node-graph.ts index de00ffc..4a048ff 100644 --- a/src/traverse/node-graph.ts +++ b/src/traverse/node-graph.ts @@ -31,7 +31,7 @@ export class NodeGraph extends DirectedGraph { for (const nodeId of this.nodes()) { const nodeData = this.getNodeAttributes(nodeId) - if (nodeData?.isImported && !nodeData?.isRootImport) { + if (nodeData?.isImported && !nodeData?.isMainCode) { // Check if this imported type has any outgoing edges (other nodes depend on it) const outgoingEdges = this.outboundNeighbors(nodeId) if (outgoingEdges.length === 0) { diff --git a/src/traverse/types.ts b/src/traverse/types.ts index 988988a..884dd1b 100644 --- a/src/traverse/types.ts +++ b/src/traverse/types.ts @@ -1,10 +1,5 @@ -import type { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' import type { Node } from 'ts-morph' -export interface TypeReferenceExtractor { - extractTypeReferences(typeNode: Node, nodeGraph: NodeGraph): string[] -} - export type SupportedNodeType = 'interface' | 'typeAlias' | 'enum' | 'function' export interface TraversedNode { @@ -12,8 +7,6 @@ export interface TraversedNode { type: SupportedNodeType originalName: string qualifiedName: string - isImported?: boolean - isDirectImport?: boolean - isRootImport?: boolean - isMainCode?: boolean + isImported: boolean + isMainCode: boolean } diff --git a/src/utils/generate-qualified-name.ts b/src/utils/generate-qualified-name.ts index 0866ea5..8db55c1 100644 --- a/src/utils/generate-qualified-name.ts +++ b/src/utils/generate-qualified-name.ts @@ -1,4 +1,4 @@ -import { basename } from 'path' +import { basename } from 'node:path' import type { SourceFile } from 'ts-morph' /** @@ -9,9 +9,9 @@ const hashString = (str: string): string => { for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i) hash = (hash << 5) - hash + char - hash = hash & hash // Convert to 32-bit integer + hash |= 0 // force signed 32-bit } - return Math.abs(hash).toString(36) + return (hash >>> 0).toString(36) // unsigned 32-bit } /** diff --git a/tests/handlers/typebox/arrays.test.ts b/tests/handlers/typebox/arrays.test.ts index 07848c9..dab9edd 100644 --- a/tests/handlers/typebox/arrays.test.ts +++ b/tests/handlers/typebox/arrays.test.ts @@ -13,7 +13,7 @@ describe('Array types', () => { test('Array', () => { const sourceFile = createSourceFile(project, `type A = string[]`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Array(Type.String()); @@ -25,7 +25,7 @@ describe('Array types', () => { test('string[]', () => { const sourceFile = createSourceFile(project, `type A = string[]`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Array(Type.String()); @@ -43,7 +43,7 @@ describe('Array types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]); @@ -62,7 +62,7 @@ describe('Array types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Number(); @@ -91,7 +91,7 @@ describe('Array types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Intersect([ Type.Object({ @@ -110,7 +110,7 @@ describe('Array types', () => { test('Literal', () => { const sourceFile = createSourceFile(project, `type T = "a" | "b";`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); @@ -124,7 +124,7 @@ describe('Array types', () => { test('Array', () => { const sourceFile = createSourceFile(project, `export type A = string[]`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Array(Type.String()); @@ -136,7 +136,7 @@ describe('Array types', () => { test('string[]', () => { const sourceFile = createSourceFile(project, `export type A = string[]`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Array(Type.String()); @@ -154,7 +154,7 @@ describe('Array types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]); @@ -173,7 +173,7 @@ describe('Array types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Number(); @@ -202,7 +202,7 @@ describe('Array types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Intersect([ Type.Object({ @@ -221,7 +221,7 @@ describe('Array types', () => { test('Literal', () => { const sourceFile = createSourceFile(project, 'export type T = "a" | "b";') - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); diff --git a/tests/handlers/typebox/enums.test.ts b/tests/handlers/typebox/enums.test.ts index 27fa9ea..1bef710 100644 --- a/tests/handlers/typebox/enums.test.ts +++ b/tests/handlers/typebox/enums.test.ts @@ -21,16 +21,16 @@ describe('Enum types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export enum A { B, C, } - export const A = Type.Enum(A); + export const ASchema = Type.Enum(A); - export type A = Static; + export type ASchema = Static; `), ) }) @@ -46,16 +46,16 @@ describe('Enum types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export enum A { B = 'b', C = 'c', } - export const A = Type.Enum(A); + export const ASchema = Type.Enum(A); - export type A = Static; + export type ASchema = Static; `), ) }) @@ -73,16 +73,16 @@ describe('Enum types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export enum A { B, C, } - export const A = Type.Enum(A); + export const ASchema = Type.Enum(A); - export type A = Static; + export type ASchema = Static; `), ) }) @@ -98,16 +98,16 @@ describe('Enum types', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export enum A { B = 'b', C = 'c', } - export const A = Type.Enum(A); + export const ASchema = Type.Enum(A); - export type A = Static; + export type ASchema = Static; `), ) }) diff --git a/tests/handlers/typebox/functions.test.ts b/tests/handlers/typebox/functions.test.ts index f50d7ee..3f5658a 100644 --- a/tests/handlers/typebox/functions.test.ts +++ b/tests/handlers/typebox/functions.test.ts @@ -14,7 +14,7 @@ describe('Function types', () => { test('simple function type', () => { const sourceFile = createSourceFile(project, `type A = () => string`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([], Type.String()); @@ -26,7 +26,7 @@ describe('Function types', () => { test('function type with parameters', () => { const sourceFile = createSourceFile(project, `type A = (x: number, y: string) => boolean`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); @@ -38,7 +38,7 @@ describe('Function types', () => { test('function type with optional parameters', () => { const sourceFile = createSourceFile(project, `type A = (x: number, y?: string) => void`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); @@ -52,7 +52,7 @@ describe('Function types', () => { test('simple function type', () => { const sourceFile = createSourceFile(project, `export type A = () => string`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([], Type.String()); @@ -67,7 +67,7 @@ describe('Function types', () => { `export type A = (x: number, y: string) => boolean`, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); @@ -82,7 +82,7 @@ describe('Function types', () => { `export type A = (x: number, y?: string) => void`, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); @@ -98,7 +98,7 @@ describe('Function types', () => { test('simple function declaration', () => { const sourceFile = createSourceFile(project, `function A(): string { return '' }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([], Type.String()); @@ -113,7 +113,7 @@ describe('Function types', () => { `function A(x: number, y: string): boolean { return true }`, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); @@ -125,7 +125,7 @@ describe('Function types', () => { test('function declaration with optional parameters', () => { const sourceFile = createSourceFile(project, `function A(x: number, y?: string): void { }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); @@ -139,7 +139,7 @@ describe('Function types', () => { test('simple function declaration', () => { const sourceFile = createSourceFile(project, `export function A(): string { return '' }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([], Type.String()); @@ -154,7 +154,7 @@ describe('Function types', () => { `export function A(x: number, y: string): boolean { return true }`, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.String()], Type.Boolean()); @@ -169,7 +169,7 @@ describe('Function types', () => { `export function A(x: number, y?: string): void { }`, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Function([Type.Number(), Type.Optional(Type.String())], Type.Void()); diff --git a/tests/handlers/typebox/interfaces.test.ts b/tests/handlers/typebox/interfaces.test.ts index f6f9bf2..4a1c697 100644 --- a/tests/handlers/typebox/interfaces.test.ts +++ b/tests/handlers/typebox/interfaces.test.ts @@ -13,7 +13,7 @@ describe('Interfaces', () => { test('without export', () => { const sourceFile = createSourceFile(project, `interface A { a: string }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.String(), @@ -27,7 +27,7 @@ describe('Interfaces', () => { test('with export', () => { const sourceFile = createSourceFile(project, `export interface A { a: string }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.String(), @@ -49,7 +49,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const Base = Type.Object({ id: Type.String(), @@ -79,7 +79,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.String(), @@ -115,7 +115,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const Base = Type.Object({ id: Type.String(), @@ -144,7 +144,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const Base = Type.Object({ id: Type.String(), @@ -172,7 +172,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.String(), @@ -211,7 +211,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier( ` export const A = (T: T) => Type.Object({ @@ -241,7 +241,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier( ` export const A = (T: T) => Type.Object({ @@ -274,7 +274,7 @@ describe('Interfaces', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier( ` export const A = Type.Union([Type.Literal('a'), Type.Literal('b')]) diff --git a/tests/handlers/typebox/objects.test.ts b/tests/handlers/typebox/objects.test.ts index 0cd4074..aadd084 100644 --- a/tests/handlers/typebox/objects.test.ts +++ b/tests/handlers/typebox/objects.test.ts @@ -13,7 +13,7 @@ describe('Object types', () => { test('object', () => { const sourceFile = createSourceFile(project, `type A = { a: string }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.String(), @@ -27,7 +27,7 @@ describe('Object types', () => { test('Tuple', () => { const sourceFile = createSourceFile(project, `type T = [number, null];`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Tuple([Type.Number(), Type.Null()]); @@ -41,7 +41,7 @@ describe('Object types', () => { test('object', () => { const sourceFile = createSourceFile(project, `export type A = { a: string }`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.String(), @@ -55,7 +55,7 @@ describe('Object types', () => { test('Tuple', () => { const sourceFile = createSourceFile(project, `export type T = [number, null];`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Tuple([Type.Number(), Type.Null()]); diff --git a/tests/handlers/typebox/primitive-types.test.ts b/tests/handlers/typebox/primitive-types.test.ts index 7243149..8a7eec8 100644 --- a/tests/handlers/typebox/primitive-types.test.ts +++ b/tests/handlers/typebox/primitive-types.test.ts @@ -13,7 +13,7 @@ describe('Primitive types', () => { test('string', () => { const sourceFile = createSourceFile(project, `type A = string`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.String(); @@ -25,7 +25,7 @@ describe('Primitive types', () => { test('number', () => { const sourceFile = createSourceFile(project, `type A = number`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Number(); @@ -37,7 +37,7 @@ describe('Primitive types', () => { test('boolean', () => { const sourceFile = createSourceFile(project, `type A = boolean`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Boolean(); @@ -49,7 +49,7 @@ describe('Primitive types', () => { test('any', () => { const sourceFile = createSourceFile(project, `type A = any`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Any(); @@ -61,7 +61,7 @@ describe('Primitive types', () => { test('unknown', () => { const sourceFile = createSourceFile(project, `type A = unknown`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Unknown(); @@ -73,7 +73,7 @@ describe('Primitive types', () => { test('never', () => { const sourceFile = createSourceFile(project, `type A = never`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Never(); @@ -85,7 +85,7 @@ describe('Primitive types', () => { test('null', () => { const sourceFile = createSourceFile(project, `type A = null`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Null(); @@ -99,7 +99,7 @@ describe('Primitive types', () => { test('string', () => { const sourceFile = createSourceFile(project, `export type A = string`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.String(); @@ -111,7 +111,7 @@ describe('Primitive types', () => { test('number', () => { const sourceFile = createSourceFile(project, `export type A = number`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Number(); @@ -123,7 +123,7 @@ describe('Primitive types', () => { test('boolean', () => { const sourceFile = createSourceFile(project, `export type A = boolean`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Boolean(); @@ -135,7 +135,7 @@ describe('Primitive types', () => { test('any', () => { const sourceFile = createSourceFile(project, `export type A = any`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Any(); @@ -147,7 +147,7 @@ describe('Primitive types', () => { test('unknown', () => { const sourceFile = createSourceFile(project, `export type A = unknown`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Unknown(); @@ -159,7 +159,7 @@ describe('Primitive types', () => { test('never', () => { const sourceFile = createSourceFile(project, `export type A = never`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Never(); @@ -171,7 +171,7 @@ describe('Primitive types', () => { test('null', () => { const sourceFile = createSourceFile(project, `export type A = null`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Null(); diff --git a/tests/handlers/typebox/template-literals.test.ts b/tests/handlers/typebox/template-literals.test.ts index 31d53af..977c7d9 100644 --- a/tests/handlers/typebox/template-literals.test.ts +++ b/tests/handlers/typebox/template-literals.test.ts @@ -13,7 +13,7 @@ describe('Template literals', () => { test('one string', () => { const sourceFile = createSourceFile(project, "type T = `${'A'}`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([Type.Literal('A')]); @@ -25,7 +25,7 @@ describe('Template literals', () => { test('multiple strings', () => { const sourceFile = createSourceFile(project, "type T = `${'A'|'B'}`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([ Type.Union([Type.Literal('A'), Type.Literal('B')]) @@ -39,7 +39,7 @@ describe('Template literals', () => { test('concatenated with literal at start', () => { const sourceFile = createSourceFile(project, "type T = `${'A'|'B'}prop`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([ Type.Union([Type.Literal('A'), Type.Literal('B')]), @@ -54,7 +54,7 @@ describe('Template literals', () => { test('concatenated with literal at end', () => { const sourceFile = createSourceFile(project, "type T = `prop${'A'|'B'}`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([ Type.Literal('prop'), @@ -69,7 +69,7 @@ describe('Template literals', () => { test('concatenated with numeric type', () => { const sourceFile = createSourceFile(project, 'type T = `prop${number}`;') - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([Type.Literal('prop'), Type.Number()]); @@ -81,7 +81,7 @@ describe('Template literals', () => { test('concatenation before and after', () => { const sourceFile = createSourceFile(project, 'type A = `prefix-${string}-suffix`') - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.TemplateLiteral([ Type.Literal("prefix-"), @@ -99,7 +99,7 @@ describe('Template literals', () => { test('one string', () => { const sourceFile = createSourceFile(project, "export type T = `${'A'}`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([Type.Literal('A')]); @@ -111,7 +111,7 @@ describe('Template literals', () => { test('multiple strings', () => { const sourceFile = createSourceFile(project, "export type T = `${'A'|'B'}`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([ Type.Union([Type.Literal('A'), Type.Literal('B')]) @@ -125,7 +125,7 @@ describe('Template literals', () => { test('concatenated with literal at start', () => { const sourceFile = createSourceFile(project, "export type T = `${'A'|'B'}prop`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([ Type.Union([Type.Literal('A'), Type.Literal('B')]), @@ -140,7 +140,7 @@ describe('Template literals', () => { test('concatenated with literal at end', () => { const sourceFile = createSourceFile(project, "export type T = `prop${'A'|'B'}`;") - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([ Type.Literal('prop'), @@ -155,7 +155,7 @@ describe('Template literals', () => { test('concatenated with numeric type', () => { const sourceFile = createSourceFile(project, 'export type T = `prop${number}`;') - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.TemplateLiteral([Type.Literal('prop'), Type.Number()]); @@ -167,7 +167,7 @@ describe('Template literals', () => { test('concatenation before and after', () => { const sourceFile = createSourceFile(project, 'export type A = `prefix-${string}-suffix`') - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.TemplateLiteral([ Type.Literal("prefix-"), diff --git a/tests/handlers/typebox/typeof.test.ts b/tests/handlers/typebox/typeof.test.ts index ee996b9..75962c0 100644 --- a/tests/handlers/typebox/typeof.test.ts +++ b/tests/handlers/typebox/typeof.test.ts @@ -19,7 +19,7 @@ describe('Typeof expressions', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = myVar; @@ -39,7 +39,7 @@ describe('Typeof expressions', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = MyNamespace_config; @@ -59,7 +59,7 @@ describe('Typeof expressions', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = myVar; @@ -79,7 +79,7 @@ describe('Typeof expressions', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = MyNamespace_config; diff --git a/tests/handlers/typebox/utility-types.test.ts b/tests/handlers/typebox/utility-types.test.ts index d2280f5..cf2228f 100644 --- a/tests/handlers/typebox/utility-types.test.ts +++ b/tests/handlers/typebox/utility-types.test.ts @@ -21,7 +21,7 @@ describe('Utility', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.KeyOf( Type.Object({ @@ -38,7 +38,7 @@ describe('Utility', () => { test('Record', () => { const sourceFile = createSourceFile(project, `type T = Record;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Record(Type.String(), Type.Number()); @@ -50,7 +50,7 @@ describe('Utility', () => { test('Partial', () => { const sourceFile = createSourceFile(project, `type T = Partial<{ a: 1; b: 2 }>;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Partial( Type.Object({ @@ -67,7 +67,7 @@ describe('Utility', () => { test('Pick', () => { const sourceFile = createSourceFile(project, `type T = Pick<{ a: 1; b: 2 }, "a">;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Pick( Type.Object({ @@ -85,7 +85,7 @@ describe('Utility', () => { test('Omit', () => { const sourceFile = createSourceFile(project, `type T = Omit<{ a: 1; b: 2 }, "a">;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Omit( Type.Object({ @@ -103,7 +103,7 @@ describe('Utility', () => { test('Required', () => { const sourceFile = createSourceFile(project, `type T = Required<{ a?: 1; b?: 2 }>;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Required( Type.Object({ @@ -128,7 +128,7 @@ describe('Utility', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.Number(), @@ -156,7 +156,7 @@ describe('Utility', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.KeyOf( Type.Object({ @@ -173,7 +173,7 @@ describe('Utility', () => { test('Record', () => { const sourceFile = createSourceFile(project, `export type T = Record;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Record(Type.String(), Type.Number()); @@ -185,7 +185,7 @@ describe('Utility', () => { test('Partial', () => { const sourceFile = createSourceFile(project, `export type T = Partial<{ a: 1; b: 2 }>;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Partial( Type.Object({ @@ -202,7 +202,7 @@ describe('Utility', () => { test('Pick', () => { const sourceFile = createSourceFile(project, `export type T = Pick<{ a: 1; b: 2 }, "a">;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Pick( Type.Object({ @@ -220,7 +220,7 @@ describe('Utility', () => { test('Omit', () => { const sourceFile = createSourceFile(project, `export type T = Omit<{ a: 1; b: 2 }, "a">;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Omit( Type.Object({ @@ -238,7 +238,7 @@ describe('Utility', () => { test('Required', () => { const sourceFile = createSourceFile(project, `export type T = Required<{ a?: 1; b?: 2 }>;`) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const T = Type.Required( Type.Object({ @@ -264,7 +264,7 @@ describe('Utility', () => { `, ) - expect(generateFormattedCode(sourceFile)).resolves.toBe( + expect(generateFormattedCode(sourceFile)).toBe( formatWithPrettier(` export const A = Type.Object({ a: Type.Number(), diff --git a/tests/import-resolution.test.ts b/tests/import-resolution.test.ts index b6aaea7..cbaa149 100644 --- a/tests/import-resolution.test.ts +++ b/tests/import-resolution.test.ts @@ -35,7 +35,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const ExternalType = Type.Object({ value: Type.String(), @@ -80,7 +80,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const ExternalType = Type.Object({ value: Type.String(), @@ -136,7 +136,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const DeeplyNestedType = Type.Object({ value: Type.Boolean(), @@ -210,7 +210,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const VeryDeeplyNestedType = Type.Object({ core: Type.String(), @@ -269,7 +269,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const ExternalType = Type.Object({ value: Type.String(), @@ -314,7 +314,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const ExternalType = Type.Object({ value: Type.String(), @@ -370,7 +370,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const DeeplyNestedType = Type.Object({ value: Type.Boolean(), @@ -444,7 +444,7 @@ describe('ts-morph codegen with imports', () => { `, ) - expect(generateFormattedCode(userFile)).resolves.toBe( + expect(generateFormattedCode(userFile)).toBe( formatWithPrettier(` export const VeryDeeplyNestedType = Type.Object({ core: Type.String(), diff --git a/tests/traverse/dependency-collector.integration.test.ts b/tests/traverse/dependency-collector.integration.test.ts index c8e19e5..d340c10 100644 --- a/tests/traverse/dependency-collector.integration.test.ts +++ b/tests/traverse/dependency-collector.integration.test.ts @@ -30,15 +30,9 @@ describe('DependencyCollector', () => { 'external.ts', ) - const mainFile = createSourceFile( - project, - 'import { User } from "./external";', - 'main.ts', - ) + const mainFile = createSourceFile(project, 'import { User } from "./external";', 'main.ts') - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) - traverser.extractDependencies() + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(1) @@ -78,9 +72,7 @@ describe('DependencyCollector', () => { 'main.ts', ) - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) - traverser.extractDependencies() + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(2) @@ -111,14 +103,9 @@ describe('DependencyCollector', () => { 'user.ts', ) - const mainFile = createSourceFile( - project, - 'import { User } from "./user";', - 'main.ts', - ) + const mainFile = createSourceFile(project, 'import { User } from "./user";', 'main.ts') - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(2) @@ -134,8 +121,7 @@ describe('DependencyCollector', () => { 'main.ts', ) - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(0) @@ -162,9 +148,7 @@ describe('DependencyCollector', () => { 'main.ts', ) - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) - traverser.extractDependencies() + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(1) @@ -189,7 +173,7 @@ describe('DependencyCollector', () => { `, ) - traverser.addLocalTypes(sourceFile) + traverser.startTraversal(sourceFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(2) @@ -245,15 +229,9 @@ describe('DependencyCollector', () => { 'user.ts', ) - const mainFile = createSourceFile( - project, - 'import { User } from "./user";', - 'main.ts', - ) + const mainFile = createSourceFile(project, 'import { User } from "./user";', 'main.ts') - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) - traverser.extractDependencies() + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(2) @@ -296,15 +274,9 @@ describe('DependencyCollector', () => { 'c.ts', ) - const mainFile = createSourceFile( - project, - 'import { C } from "./c";', - 'main.ts', - ) + const mainFile = createSourceFile(project, 'import { C } from "./c";', 'main.ts') - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) - traverser.extractDependencies() + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(3) @@ -347,8 +319,7 @@ describe('DependencyCollector', () => { 'main.ts', ) - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(2) @@ -375,8 +346,7 @@ describe('DependencyCollector', () => { 'main.ts', ) - const importDeclarations = mainFile.getImportDeclarations() - traverser.collectFromImports(importDeclarations, true, mainFile) + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(1) @@ -409,11 +379,7 @@ describe('DependencyCollector', () => { 'main.ts', ) - const importDeclarations = mainFile.getImportDeclarations() - - traverser.addLocalTypes(mainFile) - traverser.collectFromImports(importDeclarations, true, mainFile) - traverser.extractDependencies() + traverser.startTraversal(mainFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(2) diff --git a/tests/traverse/dependency-ordering.test.ts b/tests/traverse/dependency-ordering.test.ts index 7e31fec..dd0e1ac 100644 --- a/tests/traverse/dependency-ordering.test.ts +++ b/tests/traverse/dependency-ordering.test.ts @@ -21,7 +21,7 @@ describe('Dependency Ordering Bug', () => { `, ) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile, true)).toBe( formatWithPrettier(` export const StringSnakDataValue = Type.Object({ value: Type.String(), @@ -57,7 +57,7 @@ describe('Dependency Ordering Bug', () => { `, ) - expect(generateFormattedCode(sourceFile, true)).resolves.toBe( + expect(generateFormattedCode(sourceFile, true)).toBe( formatWithPrettier(` export const StringSnakDataValue = Type.Object({ value: Type.String(), diff --git a/tests/utils.ts b/tests/utils.ts index d333e9a..61adfbe 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -23,11 +23,11 @@ export const formatWithPrettier = ( return synchronizedPrettier.format(code, prettierOptions) } -export const generateFormattedCode = async ( +export const generateFormattedCode = ( sourceFile: SourceFile, withTSchema: boolean = false, -): Promise => { - const code = await generateCode({ +): string => { + const code = generateCode({ sourceCode: sourceFile.getFullText(), callerFile: sourceFile.getFilePath(), project: sourceFile.getProject(),