diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 8ff7505..7845001 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -1,7 +1,5 @@
# TypeBox Code Generation Documentation
-# Table of Contents
-
- [Overview](#overview)
- [Core Component](#core-component)
- [Function Flow](#function-flow)
@@ -9,13 +7,17 @@
- [DependencyCollector](#dependencycollector)
- [Key Features](#key-features)
- [Implementation Details](#implementation-details)
-- [TSConfig Support](#tsconfig-support)
- - [TSConfig Overview](#tsconfig-overview)
- - [Usage Examples](#usage-examples)
+- [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 Considerations](#performance-considerations)
- [Performance Optimizations](#performance-optimizations)
- [TypeBox Type Handler Optimization](#typebox-type-handler-optimization)
- [Parser Instance Reuse](#parser-instance-reuse)
@@ -26,14 +28,10 @@
- [TypeReferenceExtractor Optimizations](#typereferenceextractor-optimizations)
- [TypeBoxTypeHandlers Optimizations](#typeboxtypehandlers-optimizations)
- [Performance Testing](#performance-testing)
- - [Test Categories](#test-categories)
- [Process Overview](#process-overview)
- [Test-Driven Development (TDD) Approach](#test-driven-development-tdd-approach)
- [TDD Cycle](#tdd-cycle)
- [Running Tests](#running-tests)
- - [Running All Tests](#running-all-tests)
- - [Running Specific Test Files](#running-specific-test-files)
- - [Running Tests by Pattern](#running-tests-by-pattern)
- [TDD Workflow for New Features](#tdd-workflow-for-new-features)
- [Test Organization](#test-organization)
- [Best Practices](#best-practices)
@@ -47,25 +45,27 @@ The primary goal of this codebase is to automate the creation of TypeBox schemas
## Core Component
-The main logic for code generation resides in the file. Its primary function, `generateCode`, takes a `SourceFile` object (representing a TypeScript file) as input and returns a string containing the generated TypeBox code.
+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. **Initialization**: A new in-memory `SourceFile` (`temp.ts`) is created to build the generated code. Essential imports for TypeBox (`Type`, `Static`) are added.
+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.
-2. **Parser Instantiation**: Instances of `ImportParser`, `EnumParser`, `TypeAliasParser`, and `FunctionDeclarationParser` are created, each responsible for handling specific types of declarations.
+3. **Parser Instantiation**: Instances of `ImportParser`, `EnumParser`, `TypeAliasParser`, and `FunctionDeclarationParser` are created, each responsible for handling specific types of declarations.
-3. **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.
+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.
-4. **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.
+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.
-5. **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.
+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.
-6. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. For each interface, its properties and methods are converted into TypeBox object schemas with corresponding static type aliases.
+7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. For each interface, its properties and methods are converted into TypeBox object schemas with corresponding static type aliases.
-7. **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.
+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.
-8. **Output**: Finally, the full text content of the newly generated `temp.ts` source file (which now contains all the TypeBox schemas and static types) is returned as a string.
+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.
### Import Resolution and Dependency Management
@@ -103,25 +103,61 @@ The import resolution process works in two phases:
This approach ensures that complex import scenarios work correctly and generated code compiles without dependency errors.
-## TSConfig Support
+## Input Handling System
+
+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
+
+The `InputOptions` interface defines the available input parameters:
+
+```typescript
+export interface InputOptions {
+ filePath?: string // Path to TypeScript file
+ sourceCode?: string // TypeScript source code as string
+ callerFile?: string // Context file path for relative import resolution
+ project?: Project // Existing ts-morph Project instance
+}
+```
+
+### Input Processing Features
-### TSConfig Overview
+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
+3. **Relative Import Validation**: Prevents relative imports in string-based source code unless a `callerFile` context is provided
+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
-The TypeBox code generation system includes automatic support for TypeScript configuration files (tsconfig.json). The system automatically detects and parses the closest tsconfig.json file using `tsconfck.parseNative`, ensuring that generated code respects project-specific TypeScript compiler options, particularly the `verbatimModuleSyntax` setting, which affects how import statements are generated.
+### Usage Patterns
-### Usage Examples
+- **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
+## Basic Usage
```typescript
-const result = generateCode(sourceFile)
+const result = await generateCode({
+ sourceCode: sourceFile.getFullText(),
+ callerFile: sourceFile.getFilePath(),
+})
```
-#### With Export Everything
+### With Export Everything
```typescript
-const result = generateCode(sourceFile, {
+const result = await generateCode({
+ sourceCode: sourceFile.getFullText(),
exportEverything: true,
+ callerFile: sourceFile.getFilePath(),
+})
+```
+
+### Using File Path
+
+```typescript
+const result = await generateCode({
+ filePath: './types.ts',
})
```
@@ -237,31 +273,7 @@ The optimizations maintain full backward compatibility and test reliability whil
### Performance Testing
-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:
-
-#### Test Categories
-
-1. **Large Dependency Chains**:
- - **Deep Import Chains**: Tests performance with 50+ levels of nested imports to verify the system handles deep dependency trees efficiently
- - **Wide Import Trees**: Tests scenarios with 100+ parallel imports to ensure the system scales well with broad dependency graphs
-
-2. **Cache Efficiency**:
- - **Complex Type Structures**: Validates caching performance with intricate type definitions involving unions, intersections, and nested objects
- - **Large Cache Operations**: Tests the system's ability to handle substantial cache sizes without performance degradation
-
-3. **Repeated File Processing**:
- - **Diamond Dependency Patterns**: Tests scenarios where multiple import paths converge on the same files, ensuring efficient deduplication
- - **Complex Topological Sort**: Validates performance of dependency ordering algorithms with interconnected type relationships
-
-4. **Memory Usage Patterns**:
- - **Large Type Definitions**: Tests processing of substantial type definitions to ensure memory efficiency
- - **Dependency Map Operations**: Validates performance of core dependency tracking data structures
-
-These performance tests provide baseline measurements and help identify potential bottlenecks before they impact production usage. The tests are designed to complete within reasonable timeframes while exercising the system under stress conditions that could reveal performance issues not apparent in standard unit tests.
-
-- **`ts-morph`**: The `ts-morph` library is heavily utilized for parsing, traversing, and manipulating the TypeScript Abstract Syntax Tree (AST). It provides a programmatic way to interact with TypeScript code.
-
-- **`@sinclair/typebox`**: This is the target library for schema generation. It provides a powerful and performant way to define JSON schemas with TypeScript type inference.
+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.
## Process Overview
@@ -286,33 +298,15 @@ This project follows a Test-Driven Development methodology to ensure code qualit
The project uses Bun as the test runner. Here are the key commands for running tests:
-#### Running All Tests
-
```bash
+# Run all tests
bun test
-```
-#### Running Specific Test Files
-
-```bash
# Run a specific test file
bun test tests/ts-morph/function-types.test.ts
# Run tests in a specific directory
bun test tests/ts-morph/
-
-# Run integration tests
-bun test tests/integration/
-```
-
-#### Running Tests by Pattern
-
-```bash
-# Run tests matching a pattern
-bun test --grep "function types"
-
-# Run tests for specific handlers
-bun test tests/ts-morph/advanced-types.test.ts
```
### TDD Workflow for New Features
diff --git a/bun.lock b/bun.lock
index 9021b0c..c424687 100644
--- a/bun.lock
+++ b/bun.lock
@@ -3,9 +3,6 @@
"workspaces": {
"": {
"name": "new-bun-project",
- "dependencies": {
- "tsconfck": "^3.1.6",
- },
"devDependencies": {
"@eslint/js": "^9.33.0",
"@prettier/sync": "^0.6.1",
@@ -290,8 +287,6 @@
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
- "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
-
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
diff --git a/package.json b/package.json
index 12747f0..51ce57b 100644
--- a/package.json
+++ b/package.json
@@ -27,12 +27,9 @@
"description": "Codegen for validation schemas",
"private": true,
"scripts": {
- "format": "prettier --write .",
+ "format": "prettier --cache --write .",
"typecheck": "tsc --noEmit",
"lint": "eslint"
},
- "type": "module",
- "dependencies": {
- "tsconfck": "^3.1.6"
- }
+ "type": "module"
}
diff --git a/src/input-handler.ts b/src/input-handler.ts
new file mode 100644
index 0000000..e92eb1f
--- /dev/null
+++ b/src/input-handler.ts
@@ -0,0 +1,102 @@
+import { existsSync, statSync } from 'fs'
+import { dirname, isAbsolute, resolve } from 'path'
+import { Project, SourceFile } from 'ts-morph'
+
+export interface InputOptions {
+ filePath?: string
+ sourceCode?: string
+ callerFile?: string
+ project?: Project
+}
+
+const hasRelativeImports = (sourceFile: SourceFile): boolean => {
+ const hasRelativeImports = sourceFile
+ .getImportDeclarations()
+ .some((importDeclaration) => importDeclaration.isModuleSpecifierRelative())
+
+ return hasRelativeImports
+}
+
+const resolveFilePath = (input: string, callerFile?: string): string => {
+ if (isAbsolute(input)) {
+ if (!existsSync(input)) {
+ throw new Error(`Absolute path does not exist: ${input}`)
+ }
+ return input
+ }
+
+ const possiblePaths: string[] = []
+
+ if (callerFile) {
+ const callerDir = dirname(callerFile)
+ possiblePaths.push(resolve(callerDir, input))
+ }
+
+ possiblePaths.push(resolve(process.cwd(), input))
+
+ const existingPaths = Array.from(new Set(possiblePaths)).filter((path) => existsSync(path))
+
+ if (existingPaths.length === 0) {
+ throw new Error(`Could not resolve path: ${input}. Tried: ${possiblePaths.join(', ')}`)
+ }
+
+ if (existingPaths.length > 1) {
+ throw new Error(
+ `Multiple resolutions found for path: ${input}. '` +
+ `Found: ${existingPaths.join(', ')}. ' +
+ 'Please provide a more specific path.`,
+ )
+ }
+
+ return existingPaths[0]!
+}
+
+const validateInputOptions = (options: InputOptions): void => {
+ const { filePath, sourceCode } = options
+
+ if (!filePath && !sourceCode) {
+ throw new Error('Either filePath or sourceCode must be provided')
+ }
+
+ if (filePath && sourceCode) {
+ throw new Error('Only one of filePath or sourceCode can be provided, not both')
+ }
+}
+
+export const createSourceFileFromInput = (options: InputOptions): SourceFile => {
+ validateInputOptions(options)
+
+ const project = options.project || new Project()
+ const { filePath, sourceCode, callerFile } = options
+
+ if (sourceCode) {
+ // If callerFile is provided, it means this code came from an existing SourceFile
+ // and relative imports should be allowed
+
+ const sourceFile = project.createSourceFile('temp.ts', sourceCode)
+
+ if (!callerFile && hasRelativeImports(sourceFile)) {
+ throw new Error(
+ 'Relative imports are not supported when providing code as string. ' +
+ 'Only package imports from node_modules are allowed. ' +
+ 'Relative imports will be implemented in the future.',
+ )
+ }
+
+ const virtualPath = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'
+
+ return project.createSourceFile(virtualPath, sourceCode, { overwrite: true })
+ }
+
+ if (filePath) {
+ const resolvedPath = resolveFilePath(filePath, callerFile)
+
+ if (!statSync(resolvedPath).isFile()) {
+ throw new Error(`Path is not a file: ${resolvedPath}`)
+ }
+
+ return project.addSourceFileAtPath(resolvedPath)
+ }
+
+ throw new Error('Invalid input options')
+}
diff --git a/src/ts-morph-codegen.ts b/src/ts-morph-codegen.ts
index 0b490a2..31bed13 100644
--- a/src/ts-morph-codegen.ts
+++ b/src/ts-morph-codegen.ts
@@ -1,3 +1,7 @@
+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'
@@ -6,55 +10,45 @@ import {
DependencyCollector,
type TypeDependency,
} from '@daxserver/validation-schema-codegen/utils/dependency-collector'
-import { SourceFile, ts } from 'ts-morph'
-import { parseNative } from 'tsconfck'
+import { Project, ts } from 'ts-morph'
-const sharedPrinter = ts.createPrinter()
+export interface GenerateCodeOptions extends InputOptions {
+ exportEverything?: boolean
+}
-export const generateCode = async (
- sourceFile: SourceFile,
- options: {
- exportEverything: boolean
- } = { exportEverything: false },
-): Promise => {
+export const generateCode = async ({
+ sourceCode,
+ filePath,
+ exportEverything = false,
+ ...options
+}: GenerateCodeOptions): Promise => {
+ const sourceFile = createSourceFileFromInput({
+ sourceCode,
+ filePath,
+ ...options,
+ })
const processedTypes = new Set()
- const newSourceFile = sourceFile.getProject().createSourceFile('temp.ts', '', {
+ const newSourceFile = new Project().createSourceFile('output.ts', '', {
overwrite: true,
})
- // Automatically detect and parse TSConfig using tsconfck.parseNative
- let verbatimModuleSyntax = false
- try {
- const sourceFilePath = sourceFile.getFilePath()
- const tsConfigResult = await parseNative(sourceFilePath)
- verbatimModuleSyntax = tsConfigResult.tsconfig?.compilerOptions?.verbatimModuleSyntax === true
- } catch {
- // If tsconfig detection fails, default to false
- verbatimModuleSyntax = false
- }
-
- // Add imports based on verbatimModuleSyntax setting
- if (verbatimModuleSyntax) {
- newSourceFile.addImportDeclaration({
- moduleSpecifier: '@sinclair/typebox',
- namedImports: ['Type'],
- })
- newSourceFile.addImportDeclaration({
- moduleSpecifier: '@sinclair/typebox',
- namedImports: [{ name: 'Static', isTypeOnly: true }],
- })
- } else {
- newSourceFile.addImportDeclaration({
- moduleSpecifier: '@sinclair/typebox',
- namedImports: ['Type', 'Static'],
- })
- }
+ // Add imports
+ newSourceFile.addImportDeclaration({
+ moduleSpecifier: '@sinclair/typebox',
+ namedImports: [
+ 'Type',
+ {
+ name: 'Static',
+ isTypeOnly: true,
+ },
+ ],
+ })
const parserOptions = {
newSourceFile,
- printer: sharedPrinter,
+ printer: ts.createPrinter(),
processedTypes,
- exportEverything: options.exportEverything,
+ exportEverything,
}
const typeAliasParser = new TypeAliasParser(parserOptions)
@@ -68,11 +62,11 @@ export const generateCode = async (
const localTypeAliases = sourceFile.getTypeAliases()
let orderedDependencies: TypeDependency[]
- if (options.exportEverything) {
+ if (exportEverything) {
// When exporting everything, maintain original order
orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
- options.exportEverything,
+ exportEverything,
)
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
} else {
@@ -80,7 +74,7 @@ export const generateCode = async (
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
- options.exportEverything,
+ exportEverything,
)
}
@@ -92,7 +86,7 @@ export const generateCode = async (
}
// Process local types
- if (options.exportEverything) {
+ if (exportEverything) {
for (const typeAlias of localTypeAliases) {
typeAliasParser.parseWithImportFlag(typeAlias, false)
}
diff --git a/tests/integration/wikibase/wikibase.ts b/tests/integration/wikibase/wikibase.ts
index 4f1bbc2..e52af05 100644
--- a/tests/integration/wikibase/wikibase.ts
+++ b/tests/integration/wikibase/wikibase.ts
@@ -8,8 +8,10 @@ const project = new Project()
const sourceFile = project.createSourceFile('wikibase.ts', 'import { Entity } from "wikibase-sdk"')
// Generate TypeBox code
-const typeboxCode = await generateCode(sourceFile, {
+const typeboxCode = await generateCode({
+ sourceCode: sourceFile.getFullText(),
exportEverything: true,
+ callerFile: sourceFile.getFilePath(),
})
// Write the generated code to the output file
diff --git a/tests/ts-morph/input-handler.test.ts b/tests/ts-morph/input-handler.test.ts
new file mode 100644
index 0000000..b250cb9
--- /dev/null
+++ b/tests/ts-morph/input-handler.test.ts
@@ -0,0 +1,284 @@
+import { createSourceFileFromInput } from '@daxserver/validation-schema-codegen/input-handler'
+import type { StandardizedFilePath } from '@ts-morph/common'
+import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
+import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'
+import { join } from 'path'
+import { Project } from 'ts-morph'
+
+describe('Input Handler', () => {
+ let tempDir: string
+ let testFilePath: string
+
+ beforeEach(() => {
+ tempDir = join(process.cwd(), 'temp-test-input-handler')
+ testFilePath = join(tempDir, 'test.ts')
+
+ mkdirSync(tempDir, { recursive: true })
+ })
+
+ afterEach(() => {
+ if (existsSync(tempDir)) {
+ rmSync(tempDir, { recursive: true, force: true })
+ }
+ })
+
+ describe('hasRelativeImports detection', () => {
+ test('detects relative imports with single quotes', () => {
+ const code = `import { something } from './relative-path'`
+
+ expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow()
+ })
+
+ test('detects relative imports with double quotes', () => {
+ const code = `import { something } from "./relative-path"`
+
+ expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow(
+ 'Relative imports are not supported when providing code as string',
+ )
+ })
+
+ test('detects parent directory imports', () => {
+ const code = `import { something } from '../parent-path'`
+
+ expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow(
+ 'Relative imports are not supported when providing code as string',
+ )
+ })
+
+ test('detects nested relative imports', () => {
+ const code = `import { something } from './nested/deep/path'`
+
+ expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow(
+ 'Relative imports are not supported when providing code as string',
+ )
+ })
+
+ test('allows relative imports when callerFile is provided', () => {
+ const code = `import { something } from './relative-path'`
+
+ const sourceFile = createSourceFileFromInput({
+ sourceCode: code,
+ callerFile: '/some/caller/file.ts',
+ })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ })
+
+ test('allows package imports from node_modules', () => {
+ const code = `import { Type } from '@sinclair/typebox'`
+
+ const sourceFile = createSourceFileFromInput({ sourceCode: code })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ })
+
+ test('allows multiple package imports', () => {
+ const code = `
+ import { Type } from '@sinclair/typebox'
+ import { readFileSync } from 'fs'
+ import express from 'express'
+ `
+
+ const sourceFile = createSourceFileFromInput({ sourceCode: code })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ })
+
+ test('handles mixed imports correctly', () => {
+ const code = `
+ import { Type } from '@sinclair/typebox'
+ import { something } from './relative'
+ `
+
+ expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow(
+ 'Relative imports are not supported when providing code as string',
+ )
+ })
+ })
+
+ describe('resolveFilePath function', () => {
+ test('resolves absolute path that exists', () => {
+ writeFileSync(testFilePath, 'export type Test = string')
+
+ const sourceFile = createSourceFileFromInput({ filePath: testFilePath })
+
+ expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
+ })
+
+ test('throws error for absolute path that does not exist', () => {
+ const nonExistentPath = join(tempDir, 'non-existent.ts')
+
+ expect(() => createSourceFileFromInput({ filePath: nonExistentPath })).toThrow(
+ `Absolute path does not exist: ${nonExistentPath}`,
+ )
+ })
+
+ test('resolves relative path from current working directory', () => {
+ const relativePath = 'temp-test-input-handler/test.ts'
+ writeFileSync(testFilePath, 'export type Test = string')
+
+ const sourceFile = createSourceFileFromInput({ filePath: relativePath })
+
+ expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
+ })
+
+ test('resolves relative path from callerFile directory', () => {
+ const callerFile = join(tempDir, 'caller.ts')
+ const relativeFile = join(tempDir, 'relative.ts')
+ writeFileSync(relativeFile, 'export type Test = string')
+
+ const sourceFile = createSourceFileFromInput({
+ filePath: './relative.ts',
+ callerFile,
+ })
+
+ expect(sourceFile.getFilePath()).toBe(relativeFile as StandardizedFilePath)
+ })
+
+ test('throws error when relative path cannot be resolved', () => {
+ expect(() => createSourceFileFromInput({ filePath: './non-existent.ts' })).toThrow(
+ 'Could not resolve path: ./non-existent.ts',
+ )
+ })
+
+ test('throws error when multiple resolutions found', () => {
+ // Create file in current directory
+ const currentDirFile = join(process.cwd(), 'duplicate.ts')
+ writeFileSync(currentDirFile, 'export type Test = string')
+
+ // Create file in caller directory
+ const callerDir = join(tempDir, 'caller')
+ mkdirSync(callerDir, { recursive: true })
+ const callerFile = join(callerDir, 'caller.ts')
+ const callerDirFile = join(callerDir, 'duplicate.ts')
+ writeFileSync(callerDirFile, 'export type Test = string')
+
+ expect(() =>
+ createSourceFileFromInput({
+ filePath: './duplicate.ts',
+ callerFile,
+ }),
+ ).toThrow('Multiple resolutions found for path: ./duplicate.ts')
+
+ // Cleanup
+ rmSync(currentDirFile, { force: true })
+ })
+
+ test('throws error when path is not a file', () => {
+ expect(() => createSourceFileFromInput({ filePath: tempDir })).toThrow(
+ `Path is not a file: ${tempDir}`,
+ )
+ })
+ })
+
+ describe('validateInputOptions function', () => {
+ test('throws error when neither filePath nor sourceCode provided', () => {
+ expect(() => createSourceFileFromInput({})).toThrow(
+ 'Either filePath or sourceCode must be provided',
+ )
+ })
+
+ test('throws error when both filePath and sourceCode provided', () => {
+ expect(() =>
+ createSourceFileFromInput({
+ filePath: './test.ts',
+ sourceCode: 'type Test = string',
+ }),
+ ).toThrow('Only one of filePath or sourceCode can be provided, not both')
+ })
+
+ test('accepts valid filePath option', () => {
+ writeFileSync(testFilePath, 'export type Test = string')
+
+ const sourceFile = createSourceFileFromInput({ filePath: testFilePath })
+
+ expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
+ })
+
+ test('accepts valid sourceCode option', () => {
+ const code = 'export type Test = string'
+
+ const sourceFile = createSourceFileFromInput({ sourceCode: code })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ })
+ })
+
+ describe('createSourceFileFromInput function', () => {
+ test('creates source file from string code', () => {
+ const code = 'export type Test = string'
+
+ const sourceFile = createSourceFileFromInput({ sourceCode: code })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ })
+
+ test('creates source file from file path', () => {
+ const code = 'export type Test = string'
+ writeFileSync(testFilePath, code)
+
+ const sourceFile = createSourceFileFromInput({ filePath: testFilePath })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
+ })
+
+ test('uses provided project instance', () => {
+ const customProject = new Project()
+ const code = 'export type Test = string'
+
+ const sourceFile = createSourceFileFromInput({
+ sourceCode: code,
+ project: customProject,
+ })
+
+ expect(sourceFile.getProject()).toBe(customProject)
+ })
+
+ test('creates new project when none provided', () => {
+ const code = 'export type Test = string'
+
+ const sourceFile = createSourceFileFromInput({ sourceCode: code })
+
+ expect(sourceFile.getProject()).toBeInstanceOf(Project)
+ })
+
+ test('handles complex TypeScript code', () => {
+ const code = `
+ export interface User {
+ id: number
+ name: string
+ email?: string
+ }
+
+ export type UserArray = User[]
+
+ export enum Status {
+ Active = 'active',
+ Inactive = 'inactive'
+ }
+ `
+
+ const sourceFile = createSourceFileFromInput({ sourceCode: code })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ expect(sourceFile.getInterfaces()).toHaveLength(1)
+ expect(sourceFile.getTypeAliases()).toHaveLength(1)
+ expect(sourceFile.getEnums()).toHaveLength(1)
+ })
+
+ test('preserves file content when loading from disk', () => {
+ const code = `
+ // This is a test file
+ export type Test = {
+ id: number
+ name: string
+ }`
+ writeFileSync(testFilePath, code)
+
+ const sourceFile = createSourceFileFromInput({ filePath: testFilePath })
+
+ expect(sourceFile.getFullText()).toBe(code)
+ })
+ })
+})
diff --git a/tests/ts-morph/utils.ts b/tests/ts-morph/utils.ts
index 1219044..a073aac 100644
--- a/tests/ts-morph/utils.ts
+++ b/tests/ts-morph/utils.ts
@@ -3,9 +3,7 @@ import synchronizedPrettier from '@prettier/sync'
import { Project, SourceFile } from 'ts-morph'
const prettierOptions = { parser: 'typescript' as const }
-const typeboxImport = `import { Type } from "@sinclair/typebox";
-import { type Static } from "@sinclair/typebox";
-`
+const typeboxImport = 'import { Type, type Static } from "@sinclair/typebox";\n'
export const createSourceFile = (project: Project, code: string, filePath: string = 'test.ts') => {
return project.createSourceFile(filePath, code)
@@ -20,6 +18,11 @@ export const generateFormattedCode = async (
sourceFile: SourceFile,
exportEverything: boolean = false,
): Promise => {
- const code = await generateCode(sourceFile, { exportEverything })
+ const code = await generateCode({
+ sourceCode: sourceFile.getFullText(),
+ exportEverything,
+ callerFile: sourceFile.getFilePath(),
+ project: sourceFile.getProject(),
+ })
return formatWithPrettier(code, false)
}