1111import re
1212from typing import TYPE_CHECKING
1313
14+ from codeflash .languages .current import is_typescript
15+
1416if TYPE_CHECKING :
1517 from pathlib import Path
1618
@@ -44,9 +46,10 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s
4446 """Detect the module system used by a JavaScript/TypeScript project.
4547
4648 Detection strategy:
47- 1. Check package.json for "type" field
48- 2. If file_path provided, check file extension (.mjs = ESM, .cjs = CommonJS)
49- 3. Analyze import statements in the file
49+ 1. Check file extension for explicit module type (.mjs, .cjs, .ts, .tsx, .mts)
50+ - TypeScript files always use ESM syntax regardless of package.json
51+ 2. Check package.json for explicit "type" field (only if explicitly set)
52+ 3. Analyze import/export statements in the file content
5053 4. Default to CommonJS if uncertain
5154
5255 Args:
@@ -57,58 +60,65 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s
5760 ModuleSystem constant (COMMONJS, ES_MODULE, or UNKNOWN).
5861
5962 """
60- # Strategy 1: Check package.json
63+ # Strategy 1: Check file extension first for explicit module type indicators
64+ # TypeScript files always use ESM syntax (import/export)
65+ if file_path :
66+ suffix = file_path .suffix .lower ()
67+ if suffix == ".mjs" :
68+ logger .debug ("Detected ES Module from .mjs extension" )
69+ return ModuleSystem .ES_MODULE
70+ if suffix == ".cjs" :
71+ logger .debug ("Detected CommonJS from .cjs extension" )
72+ return ModuleSystem .COMMONJS
73+ if suffix in (".ts" , ".tsx" , ".mts" ):
74+ # TypeScript always uses ESM syntax (import/export)
75+ # even if package.json doesn't have "type": "module"
76+ logger .debug ("Detected ES Module from TypeScript file extension" )
77+ return ModuleSystem .ES_MODULE
78+
79+ # Strategy 2: Check package.json for explicit type field
6180 package_json = project_root / "package.json"
6281 if package_json .exists ():
6382 try :
6483 with package_json .open ("r" ) as f :
6584 pkg = json .load (f )
66- pkg_type = pkg .get ("type" , "commonjs" )
85+ pkg_type = pkg .get ("type" ) # Don't default - only use if explicitly set
6786
6887 if pkg_type == "module" :
6988 logger .debug ("Detected ES Module from package.json type field" )
7089 return ModuleSystem .ES_MODULE
7190 if pkg_type == "commonjs" :
7291 logger .debug ("Detected CommonJS from package.json type field" )
7392 return ModuleSystem .COMMONJS
93+ # If type is not explicitly set, continue to file content analysis
7494
7595 except Exception as e :
7696 logger .warning ("Failed to parse package.json: %s" , e )
7797
78- # Strategy 2: Check file extension
79- if file_path :
80- suffix = file_path .suffix
81- if suffix == ".mjs" :
82- logger .debug ("Detected ES Module from .mjs extension" )
83- return ModuleSystem .ES_MODULE
84- if suffix == ".cjs" :
85- logger .debug ("Detected CommonJS from .cjs extension" )
86- return ModuleSystem .COMMONJS
87-
88- # Strategy 3: Analyze file content
89- if file_path .exists ():
90- try :
91- content = file_path .read_text ()
98+ # Strategy 3: Analyze file content for import/export patterns
99+ if file_path and file_path .exists ():
100+ try :
101+ content = file_path .read_text ()
92102
93- # Look for ES module syntax
94- has_import = "import " in content and "from " in content
95- has_export = "export " in content or "export default" in content or "export {" in content
103+ # Look for ES module syntax
104+ has_import = "import " in content and "from " in content
105+ has_export = "export " in content or "export default" in content or "export {" in content
96106
97- # Look for CommonJS syntax
98- has_require = "require(" in content
99- has_module_exports = "module.exports" in content or "exports." in content
107+ # Look for CommonJS syntax
108+ has_require = "require(" in content
109+ has_module_exports = "module.exports" in content or "exports." in content
100110
101- # Determine based on what we found
102- if (has_import or has_export ) and not (has_require or has_module_exports ):
103- logger .debug ("Detected ES Module from import/export statements" )
104- return ModuleSystem .ES_MODULE
111+ # Determine based on what we found
112+ if (has_import or has_export ) and not (has_require or has_module_exports ):
113+ logger .debug ("Detected ES Module from import/export statements" )
114+ return ModuleSystem .ES_MODULE
105115
106- if (has_require or has_module_exports ) and not (has_import or has_export ):
107- logger .debug ("Detected CommonJS from require/module.exports" )
108- return ModuleSystem .COMMONJS
116+ if (has_require or has_module_exports ) and not (has_import or has_export ):
117+ logger .debug ("Detected CommonJS from require/module.exports" )
118+ return ModuleSystem .COMMONJS
109119
110- except Exception as e :
111- logger .warning ("Failed to analyze file %s: %s" , file_path , e )
120+ except Exception as e :
121+ logger .warning ("Failed to analyze file %s: %s" , file_path , e )
112122
113123 # Default to CommonJS (more common and backward compatible)
114124 logger .debug ("Defaulting to CommonJS" )
@@ -199,11 +209,41 @@ def add_js_extension(module_path: str) -> str:
199209 return module_path
200210
201211
212+ def _convert_destructuring_to_imports (names_str : str ) -> str :
213+ """Convert destructuring aliases to import aliases.
214+
215+ Converts:
216+ a, b -> a, b
217+ a: aliasA -> a as aliasA
218+ a, b: aliasB -> a, b as aliasB
219+
220+ Args:
221+ names_str: The destructuring pattern string (e.g., "a, b: aliasB")
222+
223+ Returns:
224+ Import names string with aliases using 'as' syntax
225+ """
226+ # Split by commas and process each name
227+ parts = []
228+ for name in names_str .split ("," ):
229+ name = name .strip ()
230+ if ":" in name :
231+ # Convert destructuring alias to import alias
232+ # "a: aliasA" -> "a as aliasA"
233+ original , alias = name .split (":" , 1 )
234+ parts .append (f"{ original .strip ()} as { alias .strip ()} " )
235+ else :
236+ parts .append (name )
237+ return ", " .join (parts )
238+
239+
202240# Replace destructured requires with named imports
203241def replace_destructured (match : re .Match ) -> str :
204242 names = match .group (2 ).strip ()
205243 module_path = add_js_extension (match .group (3 ))
206- return f"import {{ { names } }} from '{ module_path } ';"
244+ # Convert destructuring aliases (a: b) to import aliases (a as b)
245+ converted_names = _convert_destructuring_to_imports (names )
246+ return f"import {{ { converted_names } }} from '{ module_path } ';"
207247
208248
209249# Replace property access requires with named imports with alias
@@ -234,12 +274,14 @@ def convert_commonjs_to_esm(code: str) -> str:
234274 """Convert CommonJS require statements to ES Module imports.
235275
236276 Converts:
237- const { foo, bar } = require('./module'); -> import { foo, bar } from './module';
238- const foo = require('./module'); -> import foo from './module';
239- const foo = require('./module').default; -> import foo from './module';
240- const foo = require('./module').bar; -> import { bar as foo } from './module';
277+ const { foo, bar } = require('./module'); -> import { foo, bar } from './module';
278+ const { foo: alias } = require('./module'); -> import { foo as alias } from './module';
279+ const foo = require('./module'); -> import foo from './module';
280+ const foo = require('./module').default; -> import foo from './module';
281+ const foo = require('./module').bar; -> import { bar as foo } from './module';
241282
242283 Special handling:
284+ - Destructuring aliases (a: b) are converted to import aliases (a as b)
243285 - Local codeflash helper (./codeflash-jest-helper) is converted to npm package codeflash
244286 because the local helper uses CommonJS exports which don't work in ESM projects
245287
@@ -299,36 +341,89 @@ def replace_default(match) -> str:
299341 return default_import .sub (replace_default , code )
300342
301343
302- def ensure_module_system_compatibility (code : str , target_module_system : str ) -> str :
344+ def uses_ts_jest (project_root : Path ) -> bool :
345+ """Check if the project uses ts-jest for TypeScript transformation.
346+
347+ ts-jest handles module interoperability internally, allowing mixed
348+ CommonJS/ESM imports without explicit conversion.
349+
350+ Args:
351+ project_root: The project root directory.
352+
353+ Returns:
354+ True if ts-jest is being used, False otherwise.
355+
356+ """
357+ # Check for ts-jest in devDependencies or dependencies
358+ package_json = project_root / "package.json"
359+ if package_json .exists ():
360+ try :
361+ with package_json .open ("r" ) as f :
362+ pkg = json .load (f )
363+ dev_deps = pkg .get ("devDependencies" , {})
364+ deps = pkg .get ("dependencies" , {})
365+ if "ts-jest" in dev_deps or "ts-jest" in deps :
366+ return True
367+ except Exception as e :
368+ logger .debug (f"Failed to read package.json for ts-jest detection: { e } " ) # noqa: G004
369+
370+ # Also check for jest.config with ts-jest preset
371+ for config_file in ["jest.config.js" , "jest.config.cjs" , "jest.config.ts" , "jest.config.mjs" ]:
372+ config_path = project_root / config_file
373+ if config_path .exists ():
374+ try :
375+ content = config_path .read_text ()
376+ if "ts-jest" in content :
377+ return True
378+ except Exception as e :
379+ logger .debug (f"Failed to read { config_file } : { e } " ) # noqa: G004
380+
381+ return False
382+
383+
384+ def ensure_module_system_compatibility (code : str , target_module_system : str , project_root : Path | None = None ) -> str :
303385 """Ensure code uses the correct module system syntax.
304386
305- Detects the current module system in the code and converts if needed.
306- Handles mixed-style code (e.g., ESM imports with CommonJS require for npm packages).
387+ If the project uses ts-jest, no conversion is performed because ts-jest
388+ handles module interoperability internally. Otherwise, converts between
389+ CommonJS and ES Modules as needed.
307390
308391 Args:
309392 code: JavaScript code to check and potentially convert.
310393 target_module_system: Target ModuleSystem (COMMONJS or ES_MODULE).
394+ project_root: Project root directory for ts-jest detection.
311395
312396 Returns:
313- Code with correct module system syntax .
397+ Converted code, or unchanged if ts-jest handles interop .
314398
315399 """
400+ # If ts-jest is installed, skip conversion - it handles interop natively
401+ if is_typescript () and project_root and uses_ts_jest (project_root ):
402+ logger .debug (
403+ f"Skipping module system conversion (target was { target_module_system } ). " # noqa: G004
404+ "ts-jest handles interop natively."
405+ )
406+ return code
407+
316408 # Detect current module system in code
317409 has_require = "require(" in code
410+ has_module_exports = "module.exports" in code or "exports." in code
318411 has_import = "import " in code and "from " in code
412+ has_export = "export " in code
413+
414+ is_commonjs = has_require or has_module_exports
415+ is_esm = has_import or has_export
319416
320- if target_module_system == ModuleSystem .ES_MODULE :
321- # Convert any require() statements to imports for ESM projects
322- # This handles mixed code (ESM imports + CommonJS requires for npm packages)
323- if has_require :
324- logger .debug ("Converting CommonJS requires to ESM imports" )
325- return convert_commonjs_to_esm (code )
326- elif target_module_system == ModuleSystem .COMMONJS :
327- # Convert any import statements to requires for CommonJS projects
328- if has_import :
329- logger .debug ("Converting ESM imports to CommonJS requires" )
330- return convert_esm_to_commonjs (code )
417+ # Convert if needed
418+ if target_module_system == ModuleSystem .ES_MODULE and is_commonjs and not is_esm :
419+ logger .debug ("Converting CommonJS to ES Module syntax" )
420+ return convert_commonjs_to_esm (code )
331421
422+ if target_module_system == ModuleSystem .COMMONJS and is_esm and not is_commonjs :
423+ logger .debug ("Converting ES Module to CommonJS syntax" )
424+ return convert_esm_to_commonjs (code )
425+
426+ logger .debug ("No module system conversion needed" )
332427 return code
333428
334429
@@ -355,12 +450,8 @@ def ensure_vitest_imports(code: str, test_framework: str) -> str:
355450
356451 # Check if the code uses test functions that need to be imported
357452 test_globals = ["describe" , "test" , "it" , "expect" , "vi" , "beforeEach" , "afterEach" , "beforeAll" , "afterAll" ]
358- needs_import = any (f"{ global_name } (" in code or f"{ global_name } (" in code for global_name in test_globals )
359-
360- if not needs_import :
361- return code
362453
363- # Determine which globals are actually used in the code
454+ # Combine detection and collection into a single pass
364455 used_globals = [g for g in test_globals if f"{ g } (" in code or f"{ g } (" in code ]
365456 if not used_globals :
366457 return code
@@ -373,9 +464,14 @@ def ensure_vitest_imports(code: str, test_framework: str) -> str:
373464 insert_index = 0
374465 for i , line in enumerate (lines ):
375466 stripped = line .strip ()
376- if stripped and not stripped .startswith ("//" ) and not stripped .startswith ("/*" ) and not stripped .startswith ("*" ):
467+ if (
468+ stripped
469+ and not stripped .startswith ("//" )
470+ and not stripped .startswith ("/*" )
471+ and not stripped .startswith ("*" )
472+ ):
377473 # Check if this line is an import/require - insert after imports
378- if stripped .startswith ("import " ) or stripped . startswith ( "const " ) or stripped . startswith ( "let " ):
474+ if stripped .startswith (( "import " , "const " , "let " ) ):
379475 continue
380476 insert_index = i
381477 break
0 commit comments