From 3269ceb76fe883e41aac345edd3174309f5d9277 Mon Sep 17 00:00:00 2001 From: mohammed ahmed Date: Mon, 6 Apr 2026 11:44:44 +0000 Subject: [PATCH 1/2] fix(js): detect TypeScript transformers beyond ts-jest **Problem:** When a TypeScript project uses transformers other than ts-jest (e.g., @swc/jest, babel-jest, esbuild-jest), Codeflash would generate a Jest config without a transform directive. This caused Jest to fail parsing TypeScript files with syntax errors like "Unexpected token" at type annotations. **Root Cause:** File: codeflash/languages/javascript/test_runner.py:282-295 The code only checked for ts-jest in package.json and skipped transform configuration when ts-jest was not found, even though other TypeScript transformers might be present. **Solution:** Added _detect_typescript_transformer() function that checks for common TypeScript transformers in order of preference: 1. ts-jest 2. @swc/jest 3. babel-jest (with @babel/preset-typescript) 4. esbuild-jest When detected, the appropriate transform directive is included in the generated Jest config, enabling Jest to parse TypeScript files. **Impact:** - Fixes optimization failures for TypeScript projects using @swc/jest (e.g., budibase) - Fixes optimization failures for TypeScript projects using babel-jest - Fixes optimization failures for TypeScript projects using esbuild-jest - Maintains backward compatibility with ts-jest projects **Testing:** - Added test_typescript_transform_extraction.py with two test cases - All existing test_javascript_test_runner.py tests pass (34 tests) **Trace IDs:** - 0acc4768-706a-4fe1-91e6-ec64983bd519 (budibase API.patch optimization) Co-Authored-By: Claude Sonnet 4.5 --- codeflash/languages/javascript/test_runner.py | 82 +++++++++++--- .../test_typescript_transform_extraction.py | 102 ++++++++++++++++++ 2 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 tests/test_languages/test_typescript_transform_extraction.py diff --git a/codeflash/languages/javascript/test_runner.py b/codeflash/languages/javascript/test_runner.py index a7ba0a974..9bbea2260 100644 --- a/codeflash/languages/javascript/test_runner.py +++ b/codeflash/languages/javascript/test_runner.py @@ -219,6 +219,70 @@ def _has_ts_jest_dependency(project_root: Path) -> bool: 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. + + 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) + + 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: @@ -278,21 +342,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: diff --git a/tests/test_languages/test_typescript_transform_extraction.py b/tests/test_languages/test_typescript_transform_extraction.py new file mode 100644 index 000000000..0c38f5e5d --- /dev/null +++ b/tests/test_languages/test_typescript_transform_extraction.py @@ -0,0 +1,102 @@ +"""Tests for TypeScript transform extraction from jest.config.ts""" + +import tempfile +from pathlib import Path + +import pytest + + +class TestTypeScriptTransformExtraction: + """Test that Codeflash extracts transform directives from TypeScript Jest configs.""" + + def test_extracts_swc_transform_from_typescript_config(self): + """Test that @swc/jest transform is preserved when original config is TypeScript.""" + from codeflash.languages.javascript.test_runner import ( + _create_codeflash_jest_config, + clear_created_config_files, + ) + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir).resolve() + + # Create package.json without ts-jest (mimics budibase) + (tmpdir_path / "package.json").write_text('{"name": "test", "devDependencies": {"@swc/jest": "^0.2.0"}}') + + # Create jest.config.ts with @swc/jest transform (mimics budibase) + jest_config_content = """import { Config } from "jest" + +const config: Config = { + transform: { + "^.+\\\\.ts?$": "@swc/jest", + }, +} + +export default config +""" + jest_config_path = tmpdir_path / "jest.config.ts" + jest_config_path.write_text(jest_config_content) + + clear_created_config_files() + + # Call _create_codeflash_jest_config + result = _create_codeflash_jest_config(tmpdir_path, jest_config_path) + + assert result is not None, "Should create a codeflash config" + + # Read the generated config + generated_config = result.read_text() + + print(f"Generated config:\n{generated_config}") + + # The generated config MUST include a transform directive for TypeScript files + # This is critical - without it, Jest cannot parse TypeScript + assert "transform" in generated_config, f"Generated config must include transform directive. Got:\n{generated_config}" + + # Verify it can actually transform TypeScript files (.ts extension) + assert ( + "@swc/jest" in generated_config or "ts-jest" in generated_config or '"^.+\\\\.ts' in generated_config + ), f"Generated config must include TypeScript file transformation. Got:\n{generated_config}" + + clear_created_config_files() + + def test_extracts_ts_jest_transform_from_typescript_config(self): + """Test that ts-jest transform is preserved when original config is TypeScript.""" + from codeflash.languages.javascript.test_runner import ( + _create_codeflash_jest_config, + clear_created_config_files, + ) + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir).resolve() + + # Create package.json with ts-jest + (tmpdir_path / "package.json").write_text('{"name": "test", "devDependencies": {"ts-jest": "^29.0.0"}}') + + # Create jest.config.ts with ts-jest transform + jest_config_content = """import { Config } from "jest" + +const config: Config = { + transform: { + "^.+\\\\.(ts|tsx)$": "ts-jest", + }, +} + +export default config +""" + jest_config_path = tmpdir_path / "jest.config.ts" + jest_config_path.write_text(jest_config_content) + + clear_created_config_files() + + # Call _create_codeflash_jest_config + result = _create_codeflash_jest_config(tmpdir_path, jest_config_path) + + assert result is not None, "Should create a codeflash config" + + # Read the generated config + generated_config = result.read_text() + + # Should include ts-jest transform + assert "ts-jest" in generated_config, f"Generated config should include ts-jest transform. Got:\n{generated_config}" + + clear_created_config_files() From aa4637093a2ed5acf6507d94a4ed93a079bf00c6 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:49:26 +0000 Subject: [PATCH 2/2] style: fix test conventions and remove docstrings from new code - Use tmp_path fixture instead of tempfile.TemporaryDirectory (project convention) - Add -> None return type annotations to test methods - Add encoding="utf-8" to read_text/write_text calls in new code - Remove docstring from _detect_typescript_transformer (project style) - Remove unused import pytest from test file Co-authored-by: mohammed ahmed --- codeflash/languages/javascript/test_runner.py | 17 +--- .../test_typescript_transform_extraction.py | 89 ++++++++----------- 2 files changed, 38 insertions(+), 68 deletions(-) diff --git a/codeflash/languages/javascript/test_runner.py b/codeflash/languages/javascript/test_runner.py index 9bbea2260..beb2843fc 100644 --- a/codeflash/languages/javascript/test_runner.py +++ b/codeflash/languages/javascript/test_runner.py @@ -220,27 +220,12 @@ def _has_ts_jest_dependency(project_root: Path) -> bool: 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. - - 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()) + content = json.loads(package_json.read_text(encoding="utf-8")) deps = {**content.get("dependencies", {}), **content.get("devDependencies", {})} # Check for various TypeScript transformers in order of preference diff --git a/tests/test_languages/test_typescript_transform_extraction.py b/tests/test_languages/test_typescript_transform_extraction.py index 0c38f5e5d..933e9596b 100644 --- a/tests/test_languages/test_typescript_transform_extraction.py +++ b/tests/test_languages/test_typescript_transform_extraction.py @@ -1,29 +1,22 @@ -"""Tests for TypeScript transform extraction from jest.config.ts""" - -import tempfile from pathlib import Path -import pytest - class TestTypeScriptTransformExtraction: - """Test that Codeflash extracts transform directives from TypeScript Jest configs.""" - - def test_extracts_swc_transform_from_typescript_config(self): - """Test that @swc/jest transform is preserved when original config is TypeScript.""" + def test_extracts_swc_transform_from_typescript_config(self, tmp_path: Path) -> None: from codeflash.languages.javascript.test_runner import ( _create_codeflash_jest_config, clear_created_config_files, ) - with tempfile.TemporaryDirectory() as tmpdir: - tmpdir_path = Path(tmpdir).resolve() + tmpdir_path = tmp_path.resolve() - # Create package.json without ts-jest (mimics budibase) - (tmpdir_path / "package.json").write_text('{"name": "test", "devDependencies": {"@swc/jest": "^0.2.0"}}') + # Create package.json without ts-jest (mimics budibase) + (tmpdir_path / "package.json").write_text( + '{"name": "test", "devDependencies": {"@swc/jest": "^0.2.0"}}', encoding="utf-8" + ) - # Create jest.config.ts with @swc/jest transform (mimics budibase) - jest_config_content = """import { Config } from "jest" + # Create jest.config.ts with @swc/jest transform (mimics budibase) + jest_config_content = """import { Config } from "jest" const config: Config = { transform: { @@ -33,47 +26,42 @@ def test_extracts_swc_transform_from_typescript_config(self): export default config """ - jest_config_path = tmpdir_path / "jest.config.ts" - jest_config_path.write_text(jest_config_content) - - clear_created_config_files() + jest_config_path = tmpdir_path / "jest.config.ts" + jest_config_path.write_text(jest_config_content, encoding="utf-8") - # Call _create_codeflash_jest_config - result = _create_codeflash_jest_config(tmpdir_path, jest_config_path) + clear_created_config_files() - assert result is not None, "Should create a codeflash config" + result = _create_codeflash_jest_config(tmpdir_path, jest_config_path) - # Read the generated config - generated_config = result.read_text() + assert result is not None, "Should create a codeflash config" - print(f"Generated config:\n{generated_config}") + generated_config = result.read_text(encoding="utf-8") - # The generated config MUST include a transform directive for TypeScript files - # This is critical - without it, Jest cannot parse TypeScript - assert "transform" in generated_config, f"Generated config must include transform directive. Got:\n{generated_config}" + # The generated config MUST include a transform directive for TypeScript files + assert "transform" in generated_config, f"Generated config must include transform directive. Got:\n{generated_config}" - # Verify it can actually transform TypeScript files (.ts extension) - assert ( - "@swc/jest" in generated_config or "ts-jest" in generated_config or '"^.+\\\\.ts' in generated_config - ), f"Generated config must include TypeScript file transformation. Got:\n{generated_config}" + # Verify it can actually transform TypeScript files (.ts extension) + assert ( + "@swc/jest" in generated_config or "ts-jest" in generated_config or '"^.+\\\\.ts' in generated_config + ), f"Generated config must include TypeScript file transformation. Got:\n{generated_config}" - clear_created_config_files() + clear_created_config_files() - def test_extracts_ts_jest_transform_from_typescript_config(self): - """Test that ts-jest transform is preserved when original config is TypeScript.""" + def test_extracts_ts_jest_transform_from_typescript_config(self, tmp_path: Path) -> None: from codeflash.languages.javascript.test_runner import ( _create_codeflash_jest_config, clear_created_config_files, ) - with tempfile.TemporaryDirectory() as tmpdir: - tmpdir_path = Path(tmpdir).resolve() + tmpdir_path = tmp_path.resolve() - # Create package.json with ts-jest - (tmpdir_path / "package.json").write_text('{"name": "test", "devDependencies": {"ts-jest": "^29.0.0"}}') + # Create package.json with ts-jest + (tmpdir_path / "package.json").write_text( + '{"name": "test", "devDependencies": {"ts-jest": "^29.0.0"}}', encoding="utf-8" + ) - # Create jest.config.ts with ts-jest transform - jest_config_content = """import { Config } from "jest" + # Create jest.config.ts with ts-jest transform + jest_config_content = """import { Config } from "jest" const config: Config = { transform: { @@ -83,20 +71,17 @@ def test_extracts_ts_jest_transform_from_typescript_config(self): export default config """ - jest_config_path = tmpdir_path / "jest.config.ts" - jest_config_path.write_text(jest_config_content) + jest_config_path = tmpdir_path / "jest.config.ts" + jest_config_path.write_text(jest_config_content, encoding="utf-8") - clear_created_config_files() + clear_created_config_files() - # Call _create_codeflash_jest_config - result = _create_codeflash_jest_config(tmpdir_path, jest_config_path) + result = _create_codeflash_jest_config(tmpdir_path, jest_config_path) - assert result is not None, "Should create a codeflash config" + assert result is not None, "Should create a codeflash config" - # Read the generated config - generated_config = result.read_text() + generated_config = result.read_text(encoding="utf-8") - # Should include ts-jest transform - assert "ts-jest" in generated_config, f"Generated config should include ts-jest transform. Got:\n{generated_config}" + assert "ts-jest" in generated_config, f"Generated config should include ts-jest transform. Got:\n{generated_config}" - clear_created_config_files() + clear_created_config_files()