Skip to content

Commit d8a3114

Browse files
Merge branch 'fix/js-global-def-deps' of github.com:codeflash-ai/codeflash into fix/js-global-def-deps
2 parents c619d39 + 4190766 commit d8a3114

6 files changed

Lines changed: 985 additions & 93 deletions

File tree

codeflash/cli_cmds/init_javascript.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,26 @@ def detect_project_language(project_root: Path | None = None) -> ProjectLanguage
121121

122122

123123
def determine_js_package_manager(project_root: Path) -> JsPackageManager:
124-
"""Determine which JavaScript package manager is being used based on lock files."""
125-
if (project_root / "bun.lockb").exists() or (project_root / "bun.lock").exists():
126-
return JsPackageManager.BUN
127-
if (project_root / "pnpm-lock.yaml").exists():
128-
return JsPackageManager.PNPM
129-
if (project_root / "yarn.lock").exists():
130-
return JsPackageManager.YARN
131-
if (project_root / "package-lock.json").exists():
132-
return JsPackageManager.NPM
133-
# Default to npm if package.json exists but no lock file
124+
"""Determine which JavaScript package manager is being used based on lock files.
125+
126+
Searches the project_root directory and parent directories (for monorepo support)
127+
to find lock files that indicate which package manager is being used.
128+
"""
129+
# Search from project_root up to filesystem root for lock files
130+
# This supports monorepo setups where lock file is at workspace root
131+
current_dir = project_root.resolve()
132+
while current_dir != current_dir.parent:
133+
if (current_dir / "bun.lockb").exists() or (current_dir / "bun.lock").exists():
134+
return JsPackageManager.BUN
135+
if (current_dir / "pnpm-lock.yaml").exists():
136+
return JsPackageManager.PNPM
137+
if (current_dir / "yarn.lock").exists():
138+
return JsPackageManager.YARN
139+
if (current_dir / "package-lock.json").exists():
140+
return JsPackageManager.NPM
141+
current_dir = current_dir.parent
142+
143+
# Default to npm if package.json exists but no lock file found anywhere
134144
if (project_root / "package.json").exists():
135145
return JsPackageManager.NPM
136146
return JsPackageManager.UNKNOWN

codeflash/languages/javascript/test_runner.py

Lines changed: 254 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,254 @@
2222
from codeflash.models.models import TestFiles
2323

2424

25+
def _detect_bundler_module_resolution(project_root: Path) -> bool:
26+
"""Detect if the project uses moduleResolution: 'bundler' in tsconfig.
27+
28+
TypeScript 5+ supports 'bundler' moduleResolution which requires
29+
module: 'preserve' or ES2015+. This can cause issues with ts-jest
30+
in some configurations.
31+
32+
This function also resolves extended tsconfigs to find bundler setting
33+
in parent configs.
34+
35+
Args:
36+
project_root: Root of the project to check.
37+
38+
Returns:
39+
True if the project uses bundler moduleResolution.
40+
41+
"""
42+
tsconfig_path = project_root / "tsconfig.json"
43+
if not tsconfig_path.exists():
44+
return False
45+
46+
visited_configs: set[Path] = set()
47+
48+
def check_tsconfig(config_path: Path) -> bool:
49+
"""Recursively check tsconfig and its extends for bundler moduleResolution."""
50+
if config_path in visited_configs:
51+
return False
52+
visited_configs.add(config_path)
53+
54+
if not config_path.exists():
55+
return False
56+
57+
try:
58+
content = config_path.read_text()
59+
tsconfig = json.loads(content)
60+
61+
# Check direct moduleResolution setting
62+
compiler_options = tsconfig.get("compilerOptions", {})
63+
module_resolution = compiler_options.get("moduleResolution", "").lower()
64+
if module_resolution == "bundler":
65+
return True
66+
67+
# Check extended config if present
68+
extends = tsconfig.get("extends")
69+
if extends:
70+
# Resolve the extended config path
71+
if extends.startswith("."):
72+
# Relative path
73+
extended_path = (config_path.parent / extends).resolve()
74+
if not extended_path.suffix:
75+
extended_path = extended_path.with_suffix(".json")
76+
else:
77+
# Package reference (e.g., "@n8n/typescript-config/modern/tsconfig.json")
78+
# Try to find it in node_modules
79+
node_modules_path = project_root / "node_modules" / extends
80+
if not node_modules_path.suffix:
81+
node_modules_path = node_modules_path.with_suffix(".json")
82+
if node_modules_path.exists():
83+
extended_path = node_modules_path
84+
else:
85+
# Try parent directories for monorepo support
86+
current = project_root.parent
87+
extended_path = None
88+
while current != current.parent:
89+
candidate = current / "node_modules" / extends
90+
if not candidate.suffix:
91+
candidate = candidate.with_suffix(".json")
92+
if candidate.exists():
93+
extended_path = candidate
94+
break
95+
# Also check packages directory for workspace packages
96+
packages_candidate = current / "packages" / extends
97+
if not packages_candidate.suffix:
98+
packages_candidate = packages_candidate.with_suffix(".json")
99+
if packages_candidate.exists():
100+
extended_path = packages_candidate
101+
break
102+
current = current.parent
103+
104+
if extended_path and extended_path.exists():
105+
return check_tsconfig(extended_path)
106+
107+
return False
108+
except Exception as e:
109+
logger.debug(f"Failed to read {config_path}: {e}")
110+
return False
111+
112+
return check_tsconfig(tsconfig_path)
113+
114+
115+
def _create_codeflash_tsconfig(project_root: Path) -> Path:
116+
"""Create a codeflash-compatible tsconfig for projects using bundler moduleResolution.
117+
118+
This creates a tsconfig that inherits from the project's tsconfig but overrides
119+
moduleResolution to 'Node' for compatibility with ts-jest.
120+
121+
Args:
122+
project_root: Root of the project.
123+
124+
Returns:
125+
Path to the created tsconfig.codeflash.json file.
126+
127+
"""
128+
codeflash_tsconfig_path = project_root / "tsconfig.codeflash.json"
129+
130+
# If it already exists, use it
131+
if codeflash_tsconfig_path.exists():
132+
logger.debug(f"Using existing {codeflash_tsconfig_path}")
133+
return codeflash_tsconfig_path
134+
135+
# Read the original tsconfig to preserve most settings
136+
original_tsconfig_path = project_root / "tsconfig.json"
137+
try:
138+
original_content = original_tsconfig_path.read_text()
139+
original_tsconfig = json.loads(original_content)
140+
except Exception:
141+
original_tsconfig = {}
142+
143+
# Create a new tsconfig that extends the original but fixes moduleResolution
144+
codeflash_tsconfig = {
145+
"extends": "./tsconfig.json",
146+
"compilerOptions": {
147+
# Override bundler to Node for ts-jest compatibility
148+
"moduleResolution": "Node",
149+
# Ensure module is set to a compatible value
150+
"module": "ESNext",
151+
# These are generally safe defaults for testing
152+
"esModuleInterop": True,
153+
"skipLibCheck": True,
154+
"isolatedModules": True,
155+
},
156+
}
157+
158+
# Preserve include/exclude from original if not in extends
159+
if "include" in original_tsconfig:
160+
codeflash_tsconfig["include"] = original_tsconfig["include"]
161+
if "exclude" in original_tsconfig:
162+
codeflash_tsconfig["exclude"] = original_tsconfig["exclude"]
163+
164+
try:
165+
codeflash_tsconfig_path.write_text(json.dumps(codeflash_tsconfig, indent=2))
166+
logger.debug(f"Created {codeflash_tsconfig_path} with Node moduleResolution")
167+
except Exception as e:
168+
logger.warning(f"Failed to create codeflash tsconfig: {e}")
169+
170+
return codeflash_tsconfig_path
171+
172+
173+
def _create_codeflash_jest_config(project_root: Path, original_jest_config: Path | None) -> Path | None:
174+
"""Create a Jest config that uses the codeflash tsconfig for ts-jest.
175+
176+
Args:
177+
project_root: Root of the project.
178+
original_jest_config: Path to the original Jest config, or None.
179+
180+
Returns:
181+
Path to the codeflash Jest config, or None if creation failed.
182+
183+
"""
184+
codeflash_jest_config_path = project_root / "jest.codeflash.config.js"
185+
186+
# If it already exists, use it
187+
if codeflash_jest_config_path.exists():
188+
logger.debug(f"Using existing {codeflash_jest_config_path}")
189+
return codeflash_jest_config_path
190+
191+
# Create a wrapper Jest config that uses tsconfig.codeflash.json
192+
if original_jest_config:
193+
# Extend the original config
194+
jest_config_content = f"""// Auto-generated by codeflash for bundler moduleResolution compatibility
195+
const originalConfig = require('./{original_jest_config.name}');
196+
197+
const tsJestOptions = {{
198+
isolatedModules: true,
199+
tsconfig: 'tsconfig.codeflash.json',
200+
}};
201+
202+
module.exports = {{
203+
...originalConfig,
204+
transform: {{
205+
...originalConfig.transform,
206+
'^.+\\\\.tsx?$': ['ts-jest', tsJestOptions],
207+
}},
208+
globals: {{
209+
...originalConfig.globals,
210+
'ts-jest': tsJestOptions,
211+
}},
212+
}};
213+
"""
214+
else:
215+
# Create a minimal Jest config for TypeScript
216+
jest_config_content = """// Auto-generated by codeflash for bundler moduleResolution compatibility
217+
const tsJestOptions = {
218+
isolatedModules: true,
219+
tsconfig: 'tsconfig.codeflash.json',
220+
};
221+
222+
module.exports = {
223+
verbose: true,
224+
testEnvironment: 'node',
225+
testRegex: '\\\\.(test|spec)\\\\.(js|ts|tsx)$',
226+
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
227+
transform: {
228+
'^.+\\\\.tsx?$': ['ts-jest', tsJestOptions],
229+
},
230+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
231+
};
232+
"""
233+
234+
try:
235+
codeflash_jest_config_path.write_text(jest_config_content)
236+
logger.debug(f"Created {codeflash_jest_config_path} with codeflash tsconfig")
237+
return codeflash_jest_config_path
238+
except Exception as e:
239+
logger.warning(f"Failed to create codeflash Jest config: {e}")
240+
return None
241+
242+
243+
def _get_jest_config_for_project(project_root: Path) -> Path | None:
244+
"""Get the appropriate Jest config for the project.
245+
246+
If the project uses bundler moduleResolution, creates and returns a
247+
codeflash-compatible Jest config. Otherwise, returns the project's
248+
existing Jest config.
249+
250+
Args:
251+
project_root: Root of the project.
252+
253+
Returns:
254+
Path to the Jest config to use, or None if not found.
255+
256+
"""
257+
# First check for existing Jest config
258+
original_jest_config = _find_jest_config(project_root)
259+
260+
# Check if project uses bundler moduleResolution
261+
if _detect_bundler_module_resolution(project_root):
262+
logger.info("Detected bundler moduleResolution - creating compatible config")
263+
# Create codeflash-compatible tsconfig
264+
_create_codeflash_tsconfig(project_root)
265+
# Create codeflash Jest config that uses it
266+
codeflash_jest_config = _create_codeflash_jest_config(project_root, original_jest_config)
267+
if codeflash_jest_config:
268+
return codeflash_jest_config
269+
270+
return original_jest_config
271+
272+
25273
def _find_node_project_root(file_path: Path) -> Path | None:
26274
"""Find the Node.js project root by looking for package.json.
27275
@@ -301,7 +549,8 @@ def run_jest_behavioral_tests(
301549
]
302550

303551
# Add Jest config if found - needed for TypeScript transformation
304-
jest_config = _find_jest_config(effective_cwd)
552+
# Uses codeflash-compatible config if project has bundler moduleResolution
553+
jest_config = _get_jest_config_for_project(effective_cwd)
305554
if jest_config:
306555
jest_cmd.append(f"--config={jest_config}")
307556

@@ -535,7 +784,8 @@ def run_jest_benchmarking_tests(
535784
]
536785

537786
# Add Jest config if found - needed for TypeScript transformation
538-
jest_config = _find_jest_config(effective_cwd)
787+
# Uses codeflash-compatible config if project has bundler moduleResolution
788+
jest_config = _get_jest_config_for_project(effective_cwd)
539789
if jest_config:
540790
jest_cmd.append(f"--config={jest_config}")
541791

@@ -674,7 +924,8 @@ def run_jest_line_profile_tests(
674924
]
675925

676926
# Add Jest config if found - needed for TypeScript transformation
677-
jest_config = _find_jest_config(effective_cwd)
927+
# Uses codeflash-compatible config if project has bundler moduleResolution
928+
jest_config = _get_jest_config_for_project(effective_cwd)
678929
if jest_config:
679930
jest_cmd.append(f"--config={jest_config}")
680931

0 commit comments

Comments
 (0)