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
175 changes: 161 additions & 14 deletions codeflash/languages/javascript/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,151 @@ 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)
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)
else:
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 +423,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 Expand Up @@ -382,7 +519,17 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
else:
module_dirs_line_no_base = ""

if base_config_path:
# TypeScript config files cannot be directly required by Node.js without a loader.
# If the base config is a .ts file, skip it and create a standalone config instead.
can_require_base_config = base_config_path and base_config_path.suffix != ".ts"

if base_config_path and not can_require_base_config:
logger.debug(
f"Skipping TypeScript Jest config {base_config_path.name} "
"(cannot be directly required by Node.js)"
)

if can_require_base_config:
require_path = f"./{base_config_path.name}"
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
const baseConfig = require('{require_path}');
Expand Down
100 changes: 100 additions & 0 deletions tests/test_languages/test_typescript_jest_config_require_bug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Test that runtime config generation handles TypeScript Jest configs correctly.

Reproduces bug where requiring a jest.config.ts file in the generated runtime config
causes a SyntaxError because Node.js cannot directly require TypeScript files.
"""

import tempfile
from pathlib import Path

import pytest

from codeflash.languages.javascript.test_runner import _create_runtime_jest_config


def test_runtime_config_with_typescript_base_config():
"""Test that runtime config generation handles jest.config.ts files.

When the base Jest config is a TypeScript file, the generated runtime config
should not try to require() it directly, as that would fail with a SyntaxError.
"""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)

# Create a TypeScript Jest config (common in modern projects)
base_config = project_root / "jest.config.ts"
base_config.write_text("""
import { Config } from "jest"

const config: Config = {
testEnvironment: "node",
roots: ["<rootDir>/src"],
};

export default config;
""")

# Create a test directory
test_dir = project_root / "tests" / "generated"
test_dir.mkdir(parents=True)

# Generate runtime config
runtime_config = _create_runtime_jest_config(
base_config_path=base_config,
project_root=project_root,
test_dirs={test_dir}
)

assert runtime_config is not None
assert runtime_config.exists()

# Read the generated config
config_content = runtime_config.read_text()

# The generated config should NOT try to require a .ts file
# because Node.js cannot directly require TypeScript files
assert "require('./jest.config.ts')" not in config_content, (
"Generated config should not require TypeScript files directly"
)

# It should either:
# 1. Skip the base config and create a standalone config, OR
# 2. Use a different approach (like ts-node/register)
# For now, the fix should be to skip the base config when it's TypeScript
assert "module.exports = {" in config_content


def test_runtime_config_with_javascript_base_config_still_works():
"""Test that JavaScript base configs still work correctly."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)

# Create a JavaScript Jest config
base_config = project_root / "jest.config.js"
base_config.write_text("""
module.exports = {
testEnvironment: "node",
roots: ["<rootDir>/src"],
};
""")

# Create a test directory
test_dir = project_root / "tests" / "generated"
test_dir.mkdir(parents=True)

# Generate runtime config
runtime_config = _create_runtime_jest_config(
base_config_path=base_config,
project_root=project_root,
test_dirs={test_dir}
)

assert runtime_config is not None
assert runtime_config.exists()

# Read the generated config
config_content = runtime_config.read_text()

# JavaScript configs should still be required normally
assert "require('./jest.config.js')" in config_content
assert "...baseConfig" in config_content