diff --git a/codeflash/languages/javascript/module_system.py b/codeflash/languages/javascript/module_system.py index 1f7b57c8a..32d6c875d 100644 --- a/codeflash/languages/javascript/module_system.py +++ b/codeflash/languages/javascript/module_system.py @@ -46,8 +46,10 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s """Detect the module system used by a JavaScript/TypeScript project. Detection strategy: - 1. Check file extension for explicit module type (.mjs, .cjs, .ts, .tsx, .mts) - - TypeScript files always use ESM syntax regardless of package.json + 1. Check file extension for explicit module type (.mjs, .cjs, .mts, .cts) + - .mjs and .mts always use ES Modules + - .cjs and .cts always use CommonJS + - .ts and .tsx defer to package.json "type" field 2. Check package.json for explicit "type" field (only if explicitly set) 3. Analyze import/export statements in the file content 4. Default to CommonJS if uncertain @@ -61,33 +63,41 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s """ # Strategy 1: Check file extension first for explicit module type indicators - # TypeScript files always use ESM syntax (import/export) if file_path: suffix = file_path.suffix.lower() + # Explicit JavaScript module system extensions if suffix == ".mjs": logger.debug("Detected ES Module from .mjs extension") return ModuleSystem.ES_MODULE if suffix == ".cjs": logger.debug("Detected CommonJS from .cjs extension") return ModuleSystem.COMMONJS - if suffix in (".ts", ".tsx", ".mts"): - # TypeScript always uses ESM syntax (import/export) - # even if package.json doesn't have "type": "module" - logger.debug("Detected ES Module from TypeScript file extension") + + # Explicit TypeScript module system extensions + if suffix == ".mts": + logger.debug("Detected ES Module from .mts extension") return ModuleSystem.ES_MODULE + if suffix == ".cts": + logger.debug("Detected CommonJS from .cts extension") + return ModuleSystem.COMMONJS + + # For .ts/.tsx files, defer to package.json "type" field + # TypeScript source uses ESM syntax (import/export), but the module system + # at runtime depends on package.json and tsconfig compilation settings # Strategy 2: Check package.json for explicit type field package_json = project_root / "package.json" + pkg_type_from_json = None if package_json.exists(): try: with package_json.open("r") as f: pkg = json.load(f) - pkg_type = pkg.get("type") # Don't default - only use if explicitly set + pkg_type_from_json = pkg.get("type") # Don't default - only use if explicitly set - if pkg_type == "module": + if pkg_type_from_json == "module": logger.debug("Detected ES Module from package.json type field") return ModuleSystem.ES_MODULE - if pkg_type == "commonjs": + if pkg_type_from_json == "commonjs": logger.debug("Detected CommonJS from package.json type field") return ModuleSystem.COMMONJS # If type is not explicitly set, continue to file content analysis @@ -95,6 +105,16 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s except Exception as e: logger.warning("Failed to parse package.json: %s", e) + # For TypeScript files (.ts, .tsx), if package.json doesn't specify a type, + # default to CommonJS since that's the Node.js default. + # We skip file content analysis for TypeScript because TypeScript source + # always uses ESM syntax (import/export), but the actual module system + # depends on how TypeScript compiles and how Node.js loads the files. + if file_path and file_path.suffix.lower() in (".ts", ".tsx"): + if pkg_type_from_json is None: + logger.debug("TypeScript file without explicit package.json type field - defaulting to CommonJS") + return ModuleSystem.COMMONJS + # Strategy 3: Analyze file content for import/export patterns if file_path and file_path.exists(): try: diff --git a/tests/test_languages/test_javascript_module_system.py b/tests/test_languages/test_javascript_module_system.py index f4a0b0c16..1eb56c3f2 100644 --- a/tests/test_languages/test_javascript_module_system.py +++ b/tests/test_languages/test_javascript_module_system.py @@ -57,36 +57,39 @@ def test_detect_commonjs_from_cjs_extension(self): assert result == ModuleSystem.COMMONJS def test_detect_esm_from_typescript_extension(self): - """Test detection of ES modules from TypeScript file extensions.""" + """Test detection of explicit TypeScript module extensions (.mts, .cts).""" with tempfile.TemporaryDirectory() as tmpdir: project_root = Path(tmpdir) - # Test .ts files - ts_file = project_root / "module.ts" - ts_file.write_text("export const foo = 'bar';") - assert detect_module_system(project_root, ts_file) == ModuleSystem.ES_MODULE - - # Test .tsx files - tsx_file = project_root / "component.tsx" - tsx_file.write_text("export const Component = () =>
;") - assert detect_module_system(project_root, tsx_file) == ModuleSystem.ES_MODULE - - # Test .mts files + # .mts files should always be ESM mts_file = project_root / "module.mts" mts_file.write_text("export const foo = 'bar';") assert detect_module_system(project_root, mts_file) == ModuleSystem.ES_MODULE - def test_typescript_ignores_package_json_commonjs(self): - """Test that TypeScript files are detected as ESM even with CommonJS package.json.""" + # .cts files should always be CommonJS + cts_file = project_root / "module.cts" + cts_file.write_text("export const foo = 'bar';") + assert detect_module_system(project_root, cts_file) == ModuleSystem.COMMONJS + + # .ts/.tsx files without package.json should default to CommonJS + ts_file = project_root / "module.ts" + ts_file.write_text("export const foo = 'bar';") + assert detect_module_system(project_root, ts_file) == ModuleSystem.COMMONJS + + def test_typescript_respects_package_json_type(self): + """Test that TypeScript files respect package.json type field.""" with tempfile.TemporaryDirectory() as tmpdir: project_root = Path(tmpdir) - # Create package.json with explicit commonjs type + ts_file = project_root / "module.ts" + ts_file.write_text("export const foo = 'bar';") + + # With explicit "type": "commonjs" package_json = project_root / "package.json" package_json.write_text(json.dumps({"type": "commonjs"})) + assert detect_module_system(project_root, ts_file) == ModuleSystem.COMMONJS - # TypeScript file should still be detected as ESM - ts_file = project_root / "module.ts" - ts_file.write_text("export const foo = 'bar';") + # With explicit "type": "module" + package_json.write_text(json.dumps({"type": "module"})) assert detect_module_system(project_root, ts_file) == ModuleSystem.ES_MODULE def test_detect_esm_from_import_syntax(self): diff --git a/tests/test_languages/test_typescript_commonjs_module_detection.py b/tests/test_languages/test_typescript_commonjs_module_detection.py new file mode 100644 index 000000000..2896838de --- /dev/null +++ b/tests/test_languages/test_typescript_commonjs_module_detection.py @@ -0,0 +1,160 @@ +""" +Test for Issue #10: TypeScript files in CommonJS packages should not be detected as ESM + +When a TypeScript file exists in a package without "type": "module" in package.json, +the module system should be detected as CommonJS, not ESM. +""" + +import json +import tempfile +from pathlib import Path + +import pytest + +from codeflash.languages.javascript.module_system import ModuleSystem, detect_module_system + + +def test_typescript_file_in_commonjs_package(): + """TypeScript file in package without 'type' field should be CommonJS""" + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + + # Create package.json without "type" field (defaults to CommonJS) + package_json = project_root / "package.json" + package_json.write_text(json.dumps({ + "name": "test-package", + "version": "1.0.0" + })) + + # Create TypeScript file with ESM source syntax + ts_file = project_root / "src" / "index.ts" + ts_file.parent.mkdir(parents=True, exist_ok=True) + ts_file.write_text(""" +import { foo } from './foo'; + +export function bar() { + return foo(); +} +""") + + # Should detect as CommonJS, not ESM + result = detect_module_system(project_root, ts_file) + + assert result == ModuleSystem.COMMONJS, ( + f"Expected CommonJS for TypeScript file in package without 'type' field, got {result}" + ) + + +def test_typescript_file_in_explicit_commonjs_package(): + """TypeScript file in package with 'type': 'commonjs' should be CommonJS""" + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + + # Create package.json with explicit "type": "commonjs" + package_json = project_root / "package.json" + package_json.write_text(json.dumps({ + "name": "test-package", + "version": "1.0.0", + "type": "commonjs" + })) + + # Create TypeScript file + ts_file = project_root / "src" / "index.ts" + ts_file.parent.mkdir(parents=True, exist_ok=True) + ts_file.write_text(""" +import { foo } from './foo'; +export function bar() { return foo(); } +""") + + # Should detect as CommonJS + result = detect_module_system(project_root, ts_file) + + assert result == ModuleSystem.COMMONJS, ( + f"Expected CommonJS for TypeScript file in explicit CommonJS package, got {result}" + ) + + +def test_typescript_file_in_esm_package(): + """TypeScript file in package with 'type': 'module' should be ESM""" + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + + # Create package.json with "type": "module" + package_json = project_root / "package.json" + package_json.write_text(json.dumps({ + "name": "test-package", + "version": "1.0.0", + "type": "module" + })) + + # Create TypeScript file + ts_file = project_root / "src" / "index.ts" + ts_file.parent.mkdir(parents=True, exist_ok=True) + ts_file.write_text(""" +import { foo } from './foo'; +export function bar() { return foo(); } +""") + + # Should detect as ESM + result = detect_module_system(project_root, ts_file) + + assert result == ModuleSystem.ES_MODULE, ( + f"Expected ESM for TypeScript file in ESM package, got {result}" + ) + + +def test_mts_file_always_esm(): + """.mts files should always be ESM regardless of package.json""" + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + + # Create package.json without "type" field + package_json = project_root / "package.json" + package_json.write_text(json.dumps({ + "name": "test-package", + "version": "1.0.0" + })) + + # Create .mts file (explicit ESM extension) + mts_file = project_root / "src" / "index.mts" + mts_file.parent.mkdir(parents=True, exist_ok=True) + mts_file.write_text(""" +import { foo } from './foo'; +export function bar() { return foo(); } +""") + + # Should detect as ESM (explicit extension) + result = detect_module_system(project_root, mts_file) + + assert result == ModuleSystem.ES_MODULE, ( + f"Expected ESM for .mts file, got {result}" + ) + + +def test_cts_file_always_commonjs(): + """.cts files should always be CommonJS regardless of package.json""" + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + + # Create package.json with "type": "module" + package_json = project_root / "package.json" + package_json.write_text(json.dumps({ + "name": "test-package", + "version": "1.0.0", + "type": "module" + })) + + # Create .cts file (explicit CommonJS extension) + cts_file = project_root / "src" / "index.cts" + cts_file.parent.mkdir(parents=True, exist_ok=True) + cts_file.write_text(""" +import { foo } from './foo'; +export function bar() { return foo(); } +""") + + # Should detect as CommonJS (explicit extension) + result = detect_module_system(project_root, cts_file) + + assert result == ModuleSystem.COMMONJS, ( + f"Expected CommonJS for .cts file, got {result}" + )