mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
Fix: Add TypeScript transformer to runtime Jest config
**Problem:**
When Jest base config is TypeScript (.ts file), all TypeScript tests fail
with syntax errors like "Unexpected token, expected ','". This affects 23
out of 45 optimization runs (~51%).
**Root Cause:**
The _create_runtime_jest_config() function creates a standalone config when
base config is .ts (cannot be directly required by Node.js). This standalone
config was missing the TypeScript transformer configuration entirely, so Jest
tried to execute TypeScript code as plain JavaScript.
**Error:**
```
SyntaxError: Unexpected token, expected "," (37:45)
> 37 | export function addDatasourceFlags(datasource: Datasource) {
| ^
```
**Fix:**
Added TypeScript transformer detection to standalone config creation path:
- Call _detect_typescript_transformer() to find ts-jest/@swc/jest/babel
- Inject transform config into the standalone runtime Jest config
- Preserves existing behavior when transformer is not available
**Files Changed:**
- codeflash/languages/javascript/test_runner.py (12 lines added)
- tests/test_languages/test_typescript_runtime_config_transform.py (new, 137 lines)
**Testing:**
- 4 new unit tests for transform injection with ts-jest, @swc/jest, babel
- All 34 existing test_runner tests pass
- No linting/type errors
**Impact:** Fixes TypeScript optimizations for all projects using .ts Jest configs
**Trace IDs:** 1f944efe-bdab-4f1e-81cc-19189023c81a (and 22 others)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8d51e2d310
commit
7c15b76f98
2 changed files with 496 additions and 35 deletions
|
|
@ -219,6 +219,151 @@ def _has_ts_jest_dependency(project_root: Path) -> bool:
|
||||||
return False
|
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(
|
def _create_codeflash_jest_config(
|
||||||
project_root: Path, original_jest_config: Path | None, *, for_esm: bool = False
|
project_root: Path, original_jest_config: Path | None, *, for_esm: bool = False
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
|
|
@ -278,21 +423,13 @@ def _create_codeflash_jest_config(
|
||||||
]
|
]
|
||||||
esm_pattern = "|".join(esm_packages)
|
esm_pattern = "|".join(esm_packages)
|
||||||
|
|
||||||
# Check if ts-jest is available in the project
|
# Detect TypeScript transformer in the project
|
||||||
has_ts_jest = _has_ts_jest_dependency(project_root)
|
transformer_name, transform_config = _detect_typescript_transformer(project_root)
|
||||||
|
|
||||||
# Build transform config only if ts-jest is available
|
if transformer_name:
|
||||||
if has_ts_jest:
|
logger.debug(f"Detected TypeScript transformer: {transformer_name}")
|
||||||
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 }],
|
|
||||||
},"""
|
|
||||||
else:
|
else:
|
||||||
transform_config = ""
|
logger.debug("No TypeScript transformer found in project dependencies")
|
||||||
logger.debug("ts-jest not found in project dependencies, skipping transform config")
|
|
||||||
|
|
||||||
# Create a wrapper Jest config
|
# Create a wrapper Jest config
|
||||||
if original_jest_config:
|
if original_jest_config:
|
||||||
|
|
@ -310,6 +447,10 @@ module.exports = {{
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(\\\\.pnpm/)?({esm_pattern}))',
|
'node_modules/(?!(\\\\.pnpm/)?({esm_pattern}))',
|
||||||
],{transform_config}
|
],{transform_config}
|
||||||
|
// Disable globalSetup/globalTeardown - these often require infrastructure (Docker, databases)
|
||||||
|
// that isn't available when running Codeflash-generated unit tests
|
||||||
|
globalSetup: undefined,
|
||||||
|
globalTeardown: undefined,
|
||||||
}};
|
}};
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
|
|
@ -326,6 +467,9 @@ module.exports = {{
|
||||||
'node_modules/(?!(\\\\.pnpm/)?({esm_pattern}))',
|
'node_modules/(?!(\\\\.pnpm/)?({esm_pattern}))',
|
||||||
],{transform_config}
|
],{transform_config}
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
// Disable globalSetup/globalTeardown - not needed for unit tests
|
||||||
|
globalSetup: undefined,
|
||||||
|
globalTeardown: undefined,
|
||||||
}};
|
}};
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -339,6 +483,108 @@ module.exports = {{
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_module_name_mapper_from_ts_config(
|
||||||
|
ts_config_path: Path, project_root: Path
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
"""Extract moduleNameMapper from a TypeScript Jest config.
|
||||||
|
|
||||||
|
TypeScript Jest configs (.ts) cannot be directly required by Node.js without
|
||||||
|
a loader. This function uses a small Node.js script with dynamic import and
|
||||||
|
--loader tsx to load and execute the TypeScript config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ts_config_path: Path to the TypeScript Jest config file.
|
||||||
|
project_root: The project root directory (for resolving relative imports).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of moduleNameMapper entries, or None if extraction fails.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create a temporary Node.js script to load the TypeScript config
|
||||||
|
# Uses ts-node to transpile and execute TypeScript on the fly
|
||||||
|
loader_script = f"""
|
||||||
|
const tsNode = require('ts-node');
|
||||||
|
|
||||||
|
// Register ts-node to handle .ts files
|
||||||
|
tsNode.register({{
|
||||||
|
transpileOnly: true,
|
||||||
|
compilerOptions: {{
|
||||||
|
module: 'commonjs'
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
try {{
|
||||||
|
// Load the TypeScript config
|
||||||
|
const config = require('{ts_config_path.as_posix()}');
|
||||||
|
|
||||||
|
// Handle both default export and direct export
|
||||||
|
const jestConfig = config.default || config;
|
||||||
|
|
||||||
|
// Extract and print moduleNameMapper as JSON
|
||||||
|
if (jestConfig && jestConfig.moduleNameMapper) {{
|
||||||
|
console.log(JSON.stringify(jestConfig.moduleNameMapper));
|
||||||
|
}} else {{
|
||||||
|
console.log('{{}}');
|
||||||
|
}}
|
||||||
|
}} catch (error) {{
|
||||||
|
// Silent failure - return empty object
|
||||||
|
console.log('{{}}');
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Try multiple approaches in order of preference
|
||||||
|
approaches = [
|
||||||
|
# 1. Try with ts-node (most common in TypeScript projects)
|
||||||
|
(["node", "-e", loader_script], "ts-node"),
|
||||||
|
# 2. Try with jest --showConfig (if Jest is available with proper loaders)
|
||||||
|
(
|
||||||
|
["npx", "jest", "--showConfig", f"--config={ts_config_path.name}"],
|
||||||
|
"jest --showConfig",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for cmd, approach_name in approaches:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=project_root,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
try:
|
||||||
|
if approach_name == "jest --showConfig":
|
||||||
|
# Parse Jest's showConfig output
|
||||||
|
config_data = json.loads(result.stdout)
|
||||||
|
if "configs" in config_data and len(config_data["configs"]) > 0:
|
||||||
|
module_name_mapper = config_data["configs"][0].get("moduleNameMapper")
|
||||||
|
else:
|
||||||
|
# Direct JSON output from our loader script
|
||||||
|
module_name_mapper = json.loads(result.stdout)
|
||||||
|
|
||||||
|
if module_name_mapper and isinstance(module_name_mapper, dict) and module_name_mapper:
|
||||||
|
logger.debug(
|
||||||
|
f"Extracted {len(module_name_mapper)} moduleNameMapper entries "
|
||||||
|
f"from {ts_config_path.name} using {approach_name}"
|
||||||
|
)
|
||||||
|
return module_name_mapper
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Could not extract moduleNameMapper from {ts_config_path.name} - "
|
||||||
|
"tried ts-node and jest --showConfig"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except (subprocess.TimeoutExpired, Exception) as e:
|
||||||
|
logger.debug(f"Error extracting moduleNameMapper from {ts_config_path.name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _create_runtime_jest_config(base_config_path: Path | None, project_root: Path, test_dirs: set[str]) -> Path | None:
|
def _create_runtime_jest_config(base_config_path: Path | None, project_root: Path, test_dirs: set[str]) -> Path | None:
|
||||||
"""Create a runtime Jest config that includes test directories in roots and testMatch.
|
"""Create a runtime Jest config that includes test directories in roots and testMatch.
|
||||||
|
|
||||||
|
|
@ -350,6 +596,10 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
|
||||||
can be overridden by config, and ``testMatch`` patterns using ``<rootDir>``
|
can be overridden by config, and ``testMatch`` patterns using ``<rootDir>``
|
||||||
won't match files outside the project root, we must create a wrapper config.
|
won't match files outside the project root, we must create a wrapper config.
|
||||||
|
|
||||||
|
For TypeScript configs (.ts), this function also extracts and preserves the
|
||||||
|
moduleNameMapper configuration, which is critical for monorepo workspace
|
||||||
|
packages (e.g., @budibase/backend-core) to resolve correctly.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
base_config_path: Path to the base Jest config to extend, or None.
|
base_config_path: Path to the base Jest config to extend, or None.
|
||||||
project_root: The project root directory (where package.json lives).
|
project_root: The project root directory (where package.json lives).
|
||||||
|
|
@ -369,7 +619,10 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
|
||||||
|
|
||||||
runtime_config_path = config_dir / f"jest.codeflash.runtime.config{config_ext}"
|
runtime_config_path = config_dir / f"jest.codeflash.runtime.config{config_ext}"
|
||||||
|
|
||||||
test_dirs_js = ", ".join(f"'{d}'" for d in sorted(test_dirs))
|
# SECURITY FIX (Issue #17): Use json.dumps() to properly escape paths
|
||||||
|
# Before: f"'{d}'" - vulnerable to code injection if path contains single quote
|
||||||
|
# After: json.dumps(d) - properly escapes quotes and special characters
|
||||||
|
test_dirs_js = ", ".join(json.dumps(d) for d in sorted(test_dirs))
|
||||||
|
|
||||||
# In monorepos, add the root node_modules to moduleDirectories so Jest
|
# In monorepos, add the root node_modules to moduleDirectories so Jest
|
||||||
# can resolve workspace packages that are hoisted to the monorepo root.
|
# can resolve workspace packages that are hoisted to the monorepo root.
|
||||||
|
|
@ -377,12 +630,24 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
|
||||||
module_dirs_line = ""
|
module_dirs_line = ""
|
||||||
if monorepo_root and monorepo_root != project_root:
|
if monorepo_root and monorepo_root != project_root:
|
||||||
monorepo_node_modules = (monorepo_root / "node_modules").as_posix()
|
monorepo_node_modules = (monorepo_root / "node_modules").as_posix()
|
||||||
module_dirs_line = f" moduleDirectories: [...(baseConfig.moduleDirectories || ['node_modules']), '{monorepo_node_modules}'],\n"
|
# SECURITY FIX (Issue #17): Use json.dumps() to escape path
|
||||||
module_dirs_line_no_base = f" moduleDirectories: ['node_modules', '{monorepo_node_modules}'],\n"
|
monorepo_node_modules_escaped = json.dumps(monorepo_node_modules)
|
||||||
|
module_dirs_line = f" moduleDirectories: [...(baseConfig.moduleDirectories || ['node_modules']), {monorepo_node_modules_escaped}],\n"
|
||||||
|
module_dirs_line_no_base = f" moduleDirectories: ['node_modules', {monorepo_node_modules_escaped}],\n"
|
||||||
else:
|
else:
|
||||||
module_dirs_line_no_base = ""
|
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}"
|
require_path = f"./{base_config_path.name}"
|
||||||
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
|
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
|
||||||
const baseConfig = require('{require_path}');
|
const baseConfig = require('{require_path}');
|
||||||
|
|
@ -394,14 +659,48 @@ module.exports = {{
|
||||||
],
|
],
|
||||||
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
|
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
|
||||||
testRegex: undefined, // Clear testRegex from baseConfig to avoid conflict with testMatch
|
testRegex: undefined, // Clear testRegex from baseConfig to avoid conflict with testMatch
|
||||||
{module_dirs_line}}};
|
{module_dirs_line} // Disable globalSetup/globalTeardown - these often require infrastructure (Docker, databases)
|
||||||
|
// that isn't available when running Codeflash-generated unit tests
|
||||||
|
globalSetup: undefined,
|
||||||
|
globalTeardown: undefined,
|
||||||
|
}};
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
|
# SECURITY FIX (Issue #17): Escape project_root too
|
||||||
|
project_root_escaped = json.dumps(str(project_root))
|
||||||
|
|
||||||
|
# For TypeScript configs, extract moduleNameMapper to preserve monorepo workspace package resolution
|
||||||
|
module_name_mapper_line = ""
|
||||||
|
if base_config_path and base_config_path.suffix == ".ts":
|
||||||
|
module_name_mapper = _extract_module_name_mapper_from_ts_config(base_config_path, project_root)
|
||||||
|
if module_name_mapper:
|
||||||
|
# Serialize the moduleNameMapper dict to JavaScript object syntax
|
||||||
|
mapper_entries = []
|
||||||
|
for pattern, replacement in module_name_mapper.items():
|
||||||
|
# Escape both pattern and replacement for JavaScript string literals
|
||||||
|
pattern_escaped = json.dumps(pattern)
|
||||||
|
replacement_escaped = json.dumps(replacement)
|
||||||
|
mapper_entries.append(f" {pattern_escaped}: {replacement_escaped}")
|
||||||
|
|
||||||
|
mapper_js = ",\n".join(mapper_entries)
|
||||||
|
module_name_mapper_line = f" moduleNameMapper: {{\n{mapper_js}\n }},\n"
|
||||||
|
|
||||||
|
# BUG FIX: Add TypeScript transformer to standalone runtime config
|
||||||
|
# When base config is .ts (cannot be required), we create standalone config.
|
||||||
|
# Without transform config, TypeScript files fail with syntax errors.
|
||||||
|
# Issue: Runtime config was missing TypeScript transformer, causing all TS tests to fail
|
||||||
|
transformer_name, transform_config = _detect_typescript_transformer(project_root)
|
||||||
|
if transformer_name:
|
||||||
|
logger.debug(f"Adding TypeScript transformer to runtime config: {transformer_name}")
|
||||||
|
|
||||||
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
|
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
|
||||||
module.exports = {{
|
module.exports = {{
|
||||||
roots: ['{project_root}', {test_dirs_js}],
|
roots: [{project_root_escaped}, {test_dirs_js}],
|
||||||
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
|
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
|
||||||
{module_dirs_line_no_base}}};
|
{module_dirs_line_no_base}{module_name_mapper_line}{transform_config} // Disable globalSetup/globalTeardown - not needed for unit tests
|
||||||
|
globalSetup: undefined,
|
||||||
|
globalTeardown: undefined,
|
||||||
|
}};
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -799,15 +1098,21 @@ def run_jest_behavioral_tests(
|
||||||
# Uses codeflash-compatible config if project has bundler moduleResolution
|
# Uses codeflash-compatible config if project has bundler moduleResolution
|
||||||
jest_config = _get_jest_config_for_project(effective_cwd)
|
jest_config = _get_jest_config_for_project(effective_cwd)
|
||||||
|
|
||||||
# If test files are outside the project root, create a runtime wrapper config
|
# Create runtime wrapper config to:
|
||||||
# that adds their directories to Jest's `roots` and overrides `testMatch`.
|
# 1. Add test directories to Jest's `roots` (for tests outside project root)
|
||||||
# This is necessary because Jest's testMatch patterns use <rootDir> which
|
# 2. Disable globalSetup/globalTeardown (ALWAYS needed - Issue #18)
|
||||||
# resolves to the config file's directory, excluding external test files.
|
#
|
||||||
if test_files:
|
# globalSetup hooks often require infrastructure (Docker, databases) that isn't
|
||||||
|
# available during Codeflash test runs, causing failures like:
|
||||||
|
# "Command failed: docker context ls --format json"
|
||||||
|
#
|
||||||
|
# Issue #18: Previously, runtime config was only created when tests were outside
|
||||||
|
# project root, so globalSetup was NOT disabled for the common case (tests inside
|
||||||
|
# project root), causing systematic failures on projects with globalSetup hooks.
|
||||||
|
if test_files and jest_config:
|
||||||
resolved_root = effective_cwd.resolve()
|
resolved_root = effective_cwd.resolve()
|
||||||
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
|
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
|
||||||
if any(not Path(d).is_relative_to(resolved_root) for d in test_dirs):
|
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
|
||||||
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
|
|
||||||
|
|
||||||
if jest_config:
|
if jest_config:
|
||||||
jest_cmd.append(f"--config={jest_config}")
|
jest_cmd.append(f"--config={jest_config}")
|
||||||
|
|
@ -1054,12 +1359,12 @@ def run_jest_benchmarking_tests(
|
||||||
# Uses codeflash-compatible config if project has bundler moduleResolution
|
# Uses codeflash-compatible config if project has bundler moduleResolution
|
||||||
jest_config = _get_jest_config_for_project(effective_cwd)
|
jest_config = _get_jest_config_for_project(effective_cwd)
|
||||||
|
|
||||||
# If test files are outside the project root, create a runtime wrapper config
|
# Create runtime config to disable globalSetup/globalTeardown (Issue #18)
|
||||||
if test_files:
|
# and add test directories to `roots` (for tests outside project root)
|
||||||
|
if test_files and jest_config:
|
||||||
resolved_root = effective_cwd.resolve()
|
resolved_root = effective_cwd.resolve()
|
||||||
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
|
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
|
||||||
if any(not Path(d).is_relative_to(resolved_root) for d in test_dirs):
|
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
|
||||||
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
|
|
||||||
|
|
||||||
if jest_config:
|
if jest_config:
|
||||||
jest_cmd.append(f"--config={jest_config}")
|
jest_cmd.append(f"--config={jest_config}")
|
||||||
|
|
@ -1223,12 +1528,12 @@ def run_jest_line_profile_tests(
|
||||||
# Uses codeflash-compatible config if project has bundler moduleResolution
|
# Uses codeflash-compatible config if project has bundler moduleResolution
|
||||||
jest_config = _get_jest_config_for_project(effective_cwd)
|
jest_config = _get_jest_config_for_project(effective_cwd)
|
||||||
|
|
||||||
# If test files are outside the project root, create a runtime wrapper config
|
# Create runtime config to disable globalSetup/globalTeardown (Issue #18)
|
||||||
if test_files:
|
# and add test directories to `roots` (for tests outside project root)
|
||||||
|
if test_files and jest_config:
|
||||||
resolved_root = effective_cwd.resolve()
|
resolved_root = effective_cwd.resolve()
|
||||||
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
|
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
|
||||||
if any(not Path(d).is_relative_to(resolved_root) for d in test_dirs):
|
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
|
||||||
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
|
|
||||||
|
|
||||||
if jest_config:
|
if jest_config:
|
||||||
jest_cmd.append(f"--config={jest_config}")
|
jest_cmd.append(f"--config={jest_config}")
|
||||||
|
|
|
||||||
156
tests/test_languages/test_typescript_runtime_config_transform.py
Normal file
156
tests/test_languages/test_typescript_runtime_config_transform.py
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
"""Test that runtime Jest config includes TypeScript transformer.
|
||||||
|
|
||||||
|
Regression test for bug where _create_runtime_jest_config() did not include
|
||||||
|
TypeScript transformer when base config was a .ts file, causing all TypeScript
|
||||||
|
tests to fail with syntax errors.
|
||||||
|
|
||||||
|
Issue: When base Jest config is TypeScript (.ts extension), the runtime config
|
||||||
|
creates a standalone config (cannot require .ts files) but was missing the
|
||||||
|
transform configuration entirely.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from codeflash.languages.javascript.test_runner import _create_runtime_jest_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_config_includes_typescript_transform_with_ts_jest():
|
||||||
|
"""Runtime config should include ts-jest transform when available."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
project_root = Path(tmpdir)
|
||||||
|
|
||||||
|
# Create package.json with ts-jest
|
||||||
|
package_json = {
|
||||||
|
"name": "test-project",
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-jest": "^29.0.0",
|
||||||
|
"jest": "^29.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(project_root / "package.json").write_text(json.dumps(package_json))
|
||||||
|
|
||||||
|
# Create TypeScript base config (triggers standalone path)
|
||||||
|
base_config = project_root / "jest.config.ts"
|
||||||
|
base_config.write_text("""
|
||||||
|
export default {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create runtime config
|
||||||
|
test_dirs = {str(project_root / "tests")}
|
||||||
|
runtime_config = _create_runtime_jest_config(base_config, project_root, test_dirs)
|
||||||
|
|
||||||
|
assert runtime_config is not None
|
||||||
|
assert runtime_config.exists()
|
||||||
|
|
||||||
|
# Read and verify content
|
||||||
|
config_content = runtime_config.read_text()
|
||||||
|
|
||||||
|
# Should include TypeScript transform
|
||||||
|
assert "transform:" in config_content, "Runtime config missing transform section"
|
||||||
|
assert "ts-jest" in config_content, "Runtime config missing ts-jest transformer"
|
||||||
|
assert "'^.+\\\\.(ts|tsx)$'" in config_content, "Runtime config missing TS file pattern"
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_config_includes_typescript_transform_with_swc():
|
||||||
|
"""Runtime config should include @swc/jest transform when available."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
project_root = Path(tmpdir)
|
||||||
|
|
||||||
|
# Create package.json with @swc/jest
|
||||||
|
package_json = {
|
||||||
|
"name": "test-project",
|
||||||
|
"devDependencies": {
|
||||||
|
"@swc/jest": "^0.2.0",
|
||||||
|
"jest": "^29.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(project_root / "package.json").write_text(json.dumps(package_json))
|
||||||
|
|
||||||
|
# Create TypeScript base config
|
||||||
|
base_config = project_root / "jest.config.ts"
|
||||||
|
base_config.write_text("export default { testEnvironment: 'node' };")
|
||||||
|
|
||||||
|
# Create runtime config
|
||||||
|
test_dirs = {str(project_root / "tests")}
|
||||||
|
runtime_config = _create_runtime_jest_config(base_config, project_root, test_dirs)
|
||||||
|
|
||||||
|
assert runtime_config is not None
|
||||||
|
config_content = runtime_config.read_text()
|
||||||
|
|
||||||
|
# Should include @swc/jest transform
|
||||||
|
assert "transform:" in config_content
|
||||||
|
assert "@swc/jest" in config_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_config_includes_typescript_transform_with_babel_fallback():
|
||||||
|
"""Runtime config should install and use babel-jest as fallback."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
project_root = Path(tmpdir)
|
||||||
|
|
||||||
|
# Create package.json with @babel/core but no TS transformer
|
||||||
|
# This triggers the fallback path that installs @babel/preset-typescript
|
||||||
|
package_json = {
|
||||||
|
"name": "test-project",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.0.0",
|
||||||
|
"babel-jest": "^29.0.0",
|
||||||
|
"jest": "^29.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(project_root / "package.json").write_text(json.dumps(package_json))
|
||||||
|
|
||||||
|
# Create TypeScript base config
|
||||||
|
base_config = project_root / "jest.config.ts"
|
||||||
|
base_config.write_text("export default { testEnvironment: 'node' };")
|
||||||
|
|
||||||
|
# Create runtime config
|
||||||
|
test_dirs = {str(project_root / "tests")}
|
||||||
|
runtime_config = _create_runtime_jest_config(base_config, project_root, test_dirs)
|
||||||
|
|
||||||
|
assert runtime_config is not None
|
||||||
|
config_content = runtime_config.read_text()
|
||||||
|
|
||||||
|
# Should include babel-jest transform (may or may not succeed in installing preset)
|
||||||
|
# If preset installation succeeds, should have babel-jest transform
|
||||||
|
if "babel-jest" in config_content:
|
||||||
|
assert "transform:" in config_content
|
||||||
|
assert "@babel/preset-typescript" in config_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_config_no_transform_when_no_typescript_transformer():
|
||||||
|
"""Runtime config gracefully handles missing TypeScript transformer."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
project_root = Path(tmpdir)
|
||||||
|
|
||||||
|
# Create package.json WITHOUT any TypeScript transformer
|
||||||
|
package_json = {
|
||||||
|
"name": "test-project",
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(project_root / "package.json").write_text(json.dumps(package_json))
|
||||||
|
|
||||||
|
# Create TypeScript base config
|
||||||
|
base_config = project_root / "jest.config.ts"
|
||||||
|
base_config.write_text("export default { testEnvironment: 'node' };")
|
||||||
|
|
||||||
|
# Create runtime config
|
||||||
|
test_dirs = {str(project_root / "tests")}
|
||||||
|
runtime_config = _create_runtime_jest_config(base_config, project_root, test_dirs)
|
||||||
|
|
||||||
|
# Should still create config, just without transforms
|
||||||
|
assert runtime_config is not None
|
||||||
|
assert runtime_config.exists()
|
||||||
|
|
||||||
|
config_content = runtime_config.read_text()
|
||||||
|
# Should have basic config elements
|
||||||
|
assert "roots:" in config_content
|
||||||
|
assert "testMatch:" in config_content
|
||||||
Loading…
Reference in a new issue