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
162 changes: 149 additions & 13 deletions codeflash/languages/javascript/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,150 @@ def _has_ts_jest_dependency(project_root: Path) -> bool:
return False


def _ensure_babel_preset_typescript(project_root: Path) -> bool:
"""Ensure @babel/preset-typescript is installed if @babel/core is present.

Args:
project_root: Root of the project.

Returns:
True if @babel/preset-typescript is available (already installed or just installed),
False if installation failed or @babel/core is not present.

"""
package_json = project_root / "package.json"
if not package_json.exists():
return False

try:
content = json.loads(package_json.read_text())
deps = {**content.get("dependencies", {}), **content.get("devDependencies", {})}

# Only proceed if @babel/core is installed
if "@babel/core" not in deps:
return False

# Check if already available
if "@babel/preset-typescript" in deps:
return True

# Check if actually resolvable (might be transitively installed)
check_cmd = [
"node",
"-e",
"try { require.resolve('@babel/preset-typescript'); process.exit(0); } catch { process.exit(1); }",
]
result = subprocess.run(check_cmd, cwd=project_root, capture_output=True, timeout=5, check=False)
if result.returncode == 0:
logger.debug("@babel/preset-typescript available transitively")
return True

# Not available - install it
logger.info("Installing @babel/preset-typescript for TypeScript transformation...")
install_cmd = get_package_install_command(project_root, "@babel/preset-typescript", dev=True)
result = subprocess.run(install_cmd, check=False, cwd=project_root, capture_output=True, text=True, timeout=120)

if result.returncode == 0:
logger.debug(f"Installed @babel/preset-typescript using {install_cmd[0]}")
return True

logger.warning(f"Failed to install @babel/preset-typescript: {result.stderr}")
return False

except Exception as e:
logger.warning(f"Error ensuring @babel/preset-typescript: {e}")
return False


def _detect_typescript_transformer(project_root: Path) -> tuple[str | None, str]:
"""Detect the TypeScript transformer configured in the project.

Checks package.json for common TypeScript transformers and returns
the transformer name and its configuration string for Jest config.

If no transformer is found but @babel/core is installed, attempts to
install @babel/preset-typescript and returns a babel-jest config.

Args:
project_root: Root of the project.

Returns:
Tuple of (transformer_name, config_string) where:
- transformer_name is the package name (e.g., "@swc/jest", "ts-jest")
- config_string is the Jest transform config snippet to inject
Returns (None, "") if no TypeScript transformer is found.

"""
package_json = project_root / "package.json"
if not package_json.exists():
return (None, "")

try:
content = json.loads(package_json.read_text())
deps = {**content.get("dependencies", {}), **content.get("devDependencies", {})}

# Check for various TypeScript transformers in order of preference
if "ts-jest" in deps:
config = """
// Ensure TypeScript files are transformed using ts-jest
transform: {
'^.+\\\\.(ts|tsx)$': ['ts-jest', { isolatedModules: true }],
// Use ts-jest for JS files in ESM packages too
'^.+\\\\.js$': ['ts-jest', { isolatedModules: true }],
},"""
return ("ts-jest", config)

if "@swc/jest" in deps:
config = """
// Ensure TypeScript files are transformed using @swc/jest
transform: {
'^.+\\\\.(ts|tsx)$': '@swc/jest',
},"""
return ("@swc/jest", config)

if "babel-jest" in deps and "@babel/preset-typescript" in deps:
config = """
// Ensure TypeScript files are transformed using babel-jest
transform: {
'^.+\\\\.(ts|tsx)$': 'babel-jest',
},"""
return ("babel-jest", config)

if "esbuild-jest" in deps:
config = """
// Ensure TypeScript files are transformed using esbuild-jest
transform: {
'^.+\\\\.(ts|tsx)$': 'esbuild-jest',
},"""
return ("esbuild-jest", config)

# Fallback: If @babel/core is installed but no TypeScript transformer found,
# try to ensure @babel/preset-typescript is available and use babel-jest.
# This handles projects that have Babel but no TypeScript-specific setup.
if "@babel/core" in deps:
# Ensure preset-typescript is available (install if needed)
if _ensure_babel_preset_typescript(project_root):
config = """
// Fallback: Use babel-jest with TypeScript preset
// @babel/preset-typescript was installed by codeflash for TypeScript transformation
transform: {
'^.+\\\\.(ts|tsx)$': ['babel-jest', {
presets: [
['@babel/preset-typescript', { allowDeclareFields: true }]
]
}],
},"""
return ("babel-jest (fallback)", config)
logger.warning(
"@babel/core is installed but @babel/preset-typescript could not be installed. "
"TypeScript files may fail to transform. Consider installing ts-jest or @swc/jest."
)

return (None, "")
except (json.JSONDecodeError, OSError):
return (None, "")


def _create_codeflash_jest_config(
project_root: Path, original_jest_config: Path | None, *, for_esm: bool = False
) -> Path | None:
Expand Down Expand Up @@ -278,21 +422,13 @@ def _create_codeflash_jest_config(
]
esm_pattern = "|".join(esm_packages)

# Check if ts-jest is available in the project
has_ts_jest = _has_ts_jest_dependency(project_root)
# Detect TypeScript transformer in the project
transformer_name, transform_config = _detect_typescript_transformer(project_root)

# Build transform config only if ts-jest is available
if has_ts_jest:
transform_config = """
// Ensure TypeScript files are transformed using ts-jest
transform: {
'^.+\\\\.(ts|tsx)$': ['ts-jest', { isolatedModules: true }],
// Use ts-jest for JS files in ESM packages too
'^.+\\\\.js$': ['ts-jest', { isolatedModules: true }],
},"""
if transformer_name:
logger.debug(f"Detected TypeScript transformer: {transformer_name}")
else:
transform_config = ""
logger.debug("ts-jest not found in project dependencies, skipping transform config")
logger.debug("No TypeScript transformer found in project dependencies")

# Create a wrapper Jest config
if original_jest_config:
Expand Down
167 changes: 167 additions & 0 deletions tests/test_languages/test_typescript_babel_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""Test that TypeScript files can be transformed when Babel is installed but no TS transformer exists.

This tests the fix for the bug where projects with @babel/core but no TypeScript
transformer would fail with "experimental syntax 'flow'" error when Jest tried
to transform TypeScript files.

Related trace IDs: 26117bae-39bb-4f2f-9047-f2eb6594b7eb, and 3 others
"""
import json
import tempfile
from pathlib import Path
import pytest


def test_typescript_transform_with_babel_no_preset():
"""
Test that _detect_typescript_transformer() returns a working config
when @babel/core is present but no TypeScript transformer is installed.

This fixes the bug where Jest would use babel-jest by default (when @babel/core
is installed) but fail to transform TypeScript because Babel didn't have
preset-typescript configured.
"""
from codeflash.languages.javascript.test_runner import _detect_typescript_transformer

with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)

# Create a package.json with @babel/core but no TS transformer
package_json = {
"name": "test-project",
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
# NO ts-jest, @swc/jest, @babel/preset-typescript, or esbuild-jest
}
}
(project_root / "package.json").write_text(json.dumps(package_json))

# Test the detection
transformer_name, transform_config = _detect_typescript_transformer(project_root)

# With the fix: should return babel-jest with inline preset-typescript
assert transformer_name == "babel-jest (fallback)"
assert transform_config != ""
assert "babel-jest" in transform_config
assert "@babel/preset-typescript" in transform_config
assert "\\.(ts|tsx)" in transform_config or r"\.(ts|tsx)" in transform_config


def test_generated_jest_config_handles_typescript_with_babel():
"""
Test that the generated Jest config can transform TypeScript when only Babel is available.

This verifies the end-to-end fix: the config should include a transform
directive that uses babel-jest with preset-typescript.
"""
from codeflash.languages.javascript.test_runner import _create_codeflash_jest_config

with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)

# Setup: project with @babel/core but no TS transformer
package_json = {
"name": "test-babel-ts",
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
}
}
(project_root / "package.json").write_text(json.dumps(package_json))

# Create a TypeScript source file (for realism, not used by config generation)
src_dir = project_root / "src"
src_dir.mkdir()
(src_dir / "example.ts").write_text("""
export interface User {
name: string;
age: number;
}

export function greet(user: User): string {
return `Hello, ${user.name}!`;
}
""")

# Generate codeflash Jest config
config_path = _create_codeflash_jest_config(project_root, None, for_esm=False)

assert config_path is not None
assert config_path.exists()

config_content = config_path.read_text()

# The config should include a transform directive
assert "transform:" in config_content, (
"Generated Jest config must include a transform directive to handle "
"TypeScript files when @babel/core is installed"
)

# Should use babel-jest with preset-typescript
assert "babel-jest" in config_content
assert "@babel/preset-typescript" in config_content

# Should handle .ts and .tsx files (may be escaped as \. in regex)
assert "ts" in config_content and "tsx" in config_content


def test_fallback_not_triggered_when_explicit_transformer_exists():
"""
Test that the Babel fallback is NOT used when an explicit TypeScript transformer exists.

When ts-jest, @swc/jest, etc. are installed, those should take precedence.
"""
from codeflash.languages.javascript.test_runner import _detect_typescript_transformer

with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)

# Project with both @babel/core AND ts-jest
package_json = {
"name": "test-project",
"devDependencies": {
"@babel/core": "^7.22.5",
"ts-jest": "^29.0.0",
}
}
(project_root / "package.json").write_text(json.dumps(package_json))

transformer_name, transform_config = _detect_typescript_transformer(project_root)

# Should prefer ts-jest over babel fallback
assert transformer_name == "ts-jest"
assert "ts-jest" in transform_config
assert "babel-jest" not in transform_config


def test_no_transformer_when_babel_not_installed():
"""
Test that no transformer is returned when neither Babel nor TypeScript transformers exist.

This ensures the fallback only triggers when @babel/core is present.
"""
from codeflash.languages.javascript.test_runner import _detect_typescript_transformer

with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)

# Project with no transformers at all
package_json = {
"name": "test-project",
"devDependencies": {
"jest": "^29.0.0",
# NO @babel/core, NO ts-jest, etc.
}
}
(project_root / "package.json").write_text(json.dumps(package_json))

transformer_name, transform_config = _detect_typescript_transformer(project_root)

# Should return no transformer (Jest will use default behavior)
assert transformer_name is None
assert transform_config == ""


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