Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions codeflash/languages/javascript/module_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s
"""Detect the module system used by a JavaScript/TypeScript project.

Detection strategy:
1. Check file extension for explicit module type (.mjs, .cjs, .ts, .tsx, .mts)
- TypeScript files always use ESM syntax regardless of package.json
1. Check file extension for explicit module type (.mjs, .cjs, .mts, .cts)
- .mjs and .mts always use ES Modules
- .cjs and .cts always use CommonJS
- .ts and .tsx defer to package.json "type" field
2. Check package.json for explicit "type" field (only if explicitly set)
3. Analyze import/export statements in the file content
4. Default to CommonJS if uncertain
Expand All @@ -61,40 +63,58 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s

"""
# Strategy 1: Check file extension first for explicit module type indicators
# TypeScript files always use ESM syntax (import/export)
if file_path:
suffix = file_path.suffix.lower()
# Explicit JavaScript module system extensions
if suffix == ".mjs":
logger.debug("Detected ES Module from .mjs extension")
return ModuleSystem.ES_MODULE
if suffix == ".cjs":
logger.debug("Detected CommonJS from .cjs extension")
return ModuleSystem.COMMONJS
if suffix in (".ts", ".tsx", ".mts"):
# TypeScript always uses ESM syntax (import/export)
# even if package.json doesn't have "type": "module"
logger.debug("Detected ES Module from TypeScript file extension")

# Explicit TypeScript module system extensions
if suffix == ".mts":
logger.debug("Detected ES Module from .mts extension")
return ModuleSystem.ES_MODULE
if suffix == ".cts":
logger.debug("Detected CommonJS from .cts extension")
return ModuleSystem.COMMONJS

# For .ts/.tsx files, defer to package.json "type" field
# TypeScript source uses ESM syntax (import/export), but the module system
# at runtime depends on package.json and tsconfig compilation settings

# Strategy 2: Check package.json for explicit type field
package_json = project_root / "package.json"
pkg_type_from_json = None
if package_json.exists():
try:
with package_json.open("r") as f:
pkg = json.load(f)
pkg_type = pkg.get("type") # Don't default - only use if explicitly set
pkg_type_from_json = pkg.get("type") # Don't default - only use if explicitly set

if pkg_type == "module":
if pkg_type_from_json == "module":
logger.debug("Detected ES Module from package.json type field")
return ModuleSystem.ES_MODULE
if pkg_type == "commonjs":
if pkg_type_from_json == "commonjs":
logger.debug("Detected CommonJS from package.json type field")
return ModuleSystem.COMMONJS
# If type is not explicitly set, continue to file content analysis

except Exception as e:
logger.warning("Failed to parse package.json: %s", e)

# For TypeScript files (.ts, .tsx), if package.json doesn't specify a type,
# default to CommonJS since that's the Node.js default.
# We skip file content analysis for TypeScript because TypeScript source
# always uses ESM syntax (import/export), but the actual module system
# depends on how TypeScript compiles and how Node.js loads the files.
if file_path and file_path.suffix.lower() in (".ts", ".tsx"):
if pkg_type_from_json is None:
logger.debug("TypeScript file without explicit package.json type field - defaulting to CommonJS")
return ModuleSystem.COMMONJS

# Strategy 3: Analyze file content for import/export patterns
if file_path and file_path.exists():
try:
Expand Down Expand Up @@ -399,22 +419,47 @@ def uses_ts_jest(project_root: Path) -> bool:
return False


def ensure_module_system_compatibility(code: str, target_module_system: str, project_root: Path | None = None) -> str:
def ensure_module_system_compatibility(
code: str, target_module_system: str, project_root: Path | None = None, file_path: Path | None = None
) -> str:
"""Ensure code uses the correct module system syntax.

If the project uses ts-jest, no conversion is performed because ts-jest
handles module interoperability internally. Otherwise, converts between
CommonJS and ES Modules as needed.

IMPORTANT: TypeScript test files (.test.ts, .spec.ts) ALWAYS keep ESM import
syntax, even in CommonJS projects. This is because TypeScript test runners
(@swc/jest, ts-jest) expect ESM syntax in .ts files. Converting ESM → CommonJS
causes SyntaxError when Jest tries to parse TypeScript source files via require().

Args:
code: JavaScript code to check and potentially convert.
target_module_system: Target ModuleSystem (COMMONJS or ES_MODULE).
project_root: Project root directory for ts-jest detection.
file_path: Path to the file being converted (used to detect TypeScript test files).

Returns:
Converted code, or unchanged if ts-jest handles interop.
Converted code, or unchanged if ts-jest handles interop or file is TypeScript test.

"""
# TypeScript test files must preserve ESM imports regardless of project module system
# See Issue #15: https://github.com/codeflash-ai/codeflash/issues/XXXX
if file_path is not None:
is_typescript_test = file_path.suffix in (".ts", ".tsx") and (
".test." in file_path.name
or ".spec." in file_path.name
or "/tests/" in str(file_path)
or "/__tests__/" in str(file_path)
)
if is_typescript_test and target_module_system == ModuleSystem.COMMONJS:
logger.debug(
"Preserving ESM imports for TypeScript test file: %s. "
"TypeScript test runners expect ESM syntax even in CommonJS projects.",
file_path,
)
return code

# If ts-jest is installed, skip conversion - it handles interop natively
if is_typescript() and project_root and uses_ts_jest(project_root):
logger.debug(
Expand Down
19 changes: 16 additions & 3 deletions codeflash/languages/javascript/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -2064,13 +2064,26 @@ def process_generated_test_strings(
)

# Convert module system if needed (e.g., CommonJS -> ESM for ESM projects)
# Pass test_path so TypeScript test files can preserve ESM imports (Issue #15)
generated_test_source = ensure_module_system_compatibility(
generated_test_source, project_module_system, test_cfg.tests_project_rootdir
generated_test_source, project_module_system, test_cfg.tests_project_rootdir, file_path=test_path
)

# Add .js extensions to relative imports for ESM projects
# TypeScript + ESM requires explicit .js extensions even for .ts source files
if project_module_system == ModuleSystem.ES_MODULE:
# IMPORTANT: Only for JavaScript source files, NOT TypeScript!
#
# When tests run on TypeScript source with ts-jest/tsx:
# - Imports should NOT have .js extensions (file is .ts, not .js)
# - ts-jest transpiles TypeScript at runtime
# - Adding .js causes "Cannot find module './foo.js'" when file is foo.ts
#
# When tests run on compiled JavaScript output:
# - Imports SHOULD have .js extensions (TypeScript convention for ESM)
# - But we're testing source files, not compiled output
#
# Solution: Skip .js extension for TypeScript test files
is_typescript_test = test_path.suffix in (".ts", ".tsx")
if project_module_system == ModuleSystem.ES_MODULE and not is_typescript_test:
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports

generated_test_source = add_js_extensions_to_relative_imports(generated_test_source)
Expand Down
127 changes: 127 additions & 0 deletions tests/test_languages/test_typescript_test_esm_preservation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Test that TypeScript test files preserve ESM imports in CommonJS projects.

Regression test for Issue #15: TypeScript tests converted to CommonJS cause SyntaxError.

When the AI service generates tests for TypeScript files in CommonJS projects:
1. AI service generates ESM import syntax (correct per Issue #12 fix)
2. CLI should NOT convert these imports to CommonJS require()
3. TypeScript test runners (@swc/jest, ts-jest) expect ESM syntax in .ts files

If imports are converted to require(), Jest fails with SyntaxError when trying
to load TypeScript source files via require().

Trace ID: 024aacf1-42c9-4e06-a27b-870660035d3e
"""

from pathlib import Path

import pytest

from codeflash.languages.javascript.module_system import ensure_module_system_compatibility


class TestTypeScriptTestESMPreservation:
"""Tests for preserving ESM imports in TypeScript test files."""

def test_typescript_test_preserves_esm_in_commonjs_project(self):
"""TypeScript test files should keep ESM imports even in CommonJS projects."""
# TypeScript test with ESM imports (what AI service generates)
typescript_test = """import { destroy } from '../../internal';
import sdk from '../../../sdk';

test('should work', () => {
expect(destroy).toBeDefined();
});
"""

# Convert to CommonJS (simulating CommonJS project)
# For TypeScript tests, this should be a NO-OP
result = ensure_module_system_compatibility(
typescript_test,
target_module_system="commonjs",
project_root=None,
file_path=Path("test_destroy__unit_test_0.test.ts"), # TypeScript test file
)

# This test will FAIL until the fix is implemented
# Should preserve ESM syntax for TypeScript tests
assert "import { destroy } from" in result, "Named import should be preserved"
assert "import sdk from" in result, "Default import should be preserved"
assert "require(" not in result, "Should NOT convert to require() for TypeScript tests"

def test_javascript_test_converts_esm_in_commonjs_project(self):
"""JavaScript test files should still convert ESM to CommonJS in CommonJS projects."""
# JavaScript test with ESM imports
javascript_test = """import { destroy } from '../../internal';
import sdk from '../../../sdk';

test('should work', () => {
expect(destroy).toBeDefined();
});
"""

# Convert to CommonJS (for JavaScript test, this SHOULD convert)
# This behavior is CORRECT and should remain unchanged
result = ensure_module_system_compatibility(
javascript_test,
target_module_system="commonjs",
project_root=None,
)

# Should convert to CommonJS for JavaScript tests
assert "const { destroy } = require(" in result, "Named import should convert to require"
assert "const sdk = require(" in result, "Default import should convert to require"
assert "import " not in result, "Should NOT have ESM imports for JavaScript tests"

@pytest.mark.skip(reason="Test demonstrates intended behavior, but we can't distinguish source vs test files yet")
def test_typescript_source_converts_esm_in_commonjs_project(self):
"""TypeScript SOURCE files (not tests) should still convert in CommonJS projects."""
# This test ensures we only special-case TypeScript TEST files
# NOTE: Currently we can't distinguish source files from test files without additional context
# This test is skipped because it would require API changes
typescript_source = """import { foo } from './bar';
export const result = foo();
"""

# Convert to CommonJS (for source files, should still convert)
result = ensure_module_system_compatibility(
typescript_source,
target_module_system="commonjs",
project_root=None,
)

# Source files should convert normally
assert "const { foo } = require(" in result, "Source file should convert to require"
assert "import " not in result, "Source file should not have ESM imports"

def test_typescript_test_with_multiple_import_styles(self):
"""Test all import styles are preserved for TypeScript tests."""
typescript_test = """import { destroy, create } from '../../internal';
import * as utils from '../../../utils';
import sdk from '../../../sdk';
import type { Table } from '@types';

describe('tests', () => {
test('should work', () => {
expect(destroy).toBeDefined();
});
});
"""

result = ensure_module_system_compatibility(
typescript_test,
target_module_system="commonjs",
project_root=None,
file_path=Path("test.spec.ts"), # TypeScript test file
)

# All import styles should be preserved for TypeScript tests
assert "import { destroy, create } from" in result
assert "import * as utils from" in result
assert "import sdk from" in result
assert "import type { Table } from" in result
assert "require(" not in result


if __name__ == "__main__":
pytest.main([__file__, "-v"])
Loading