codeflash/tests/test_languages/test_javascript_module_system.py
Codeflash Bot f0fe3dd572 fix: Add .js extensions to relative imports in ESM TypeScript projects
## Problem
Generated tests for ESM TypeScript projects were importing from relative
paths without .js extensions (e.g., `import X from './module'`), causing
ERR_MODULE_NOT_FOUND errors when tests run.

Node.js ESM requires explicit .js extensions for relative imports, even
when the source files are .ts. This is a TypeScript/ESM specification
requirement.

## Solution
Added `add_js_extensions_to_relative_imports()` function that:
- Adds .js extensions to relative imports (./x or ../x) without extensions
- Preserves imports that already have extensions (.js, .ts, etc.)
- Leaves non-relative imports (node modules) unchanged
- Only runs for ESM projects (CommonJS doesn't need extensions)

Integrated into test processing pipeline after module system conversion.

## Testing
- Added 7 unit tests covering various import patterns
- All 35 module_system tests pass
- All 315 JavaScript language tests pass
- Verified fix resolves ERR_MODULE_NOT_FOUND for trace 17751b8f-fa61-48bc-bdee-b924f0c7afc4

## References
Trace IDs with this issue: 17751b8f-fa61-48bc-bdee-b924f0c7afc4,
3b985200-a906-4c54-a685-df40361d6b2c, 91795877-3ccf-482c-86bd-748834b76f6e,
0298c59c-8980-4aed-b05d-b94940a6544f, ec2864a4-0de0-4ce9-9ec8-b545c82a4f53

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-01 18:07:45 +00:00

363 lines
16 KiB
Python

"""Tests for JavaScript module system detection."""
import json
import tempfile
from pathlib import Path
from codeflash.languages.javascript.module_system import (
ModuleSystem,
convert_commonjs_to_esm,
convert_esm_to_commonjs,
detect_module_system,
get_import_statement,
)
class TestModuleSystemDetection:
"""Tests for module system detection."""
def test_detect_esm_from_package_json(self):
"""Test detection of ES modules from package.json."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
package_json = project_root / "package.json"
package_json.write_text(json.dumps({"type": "module"}))
result = detect_module_system(project_root)
assert result == ModuleSystem.ES_MODULE
def test_detect_commonjs_from_package_json(self):
"""Test detection of CommonJS from package.json."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
package_json = project_root / "package.json"
package_json.write_text(json.dumps({"type": "commonjs"}))
result = detect_module_system(project_root)
assert result == ModuleSystem.COMMONJS
def test_detect_esm_from_mjs_extension(self):
"""Test detection of ES modules from .mjs extension."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
file_path = project_root / "module.mjs"
file_path.write_text("export const foo = 'bar';")
result = detect_module_system(project_root, file_path)
assert result == ModuleSystem.ES_MODULE
def test_detect_commonjs_from_cjs_extension(self):
"""Test detection of CommonJS from .cjs extension."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
file_path = project_root / "module.cjs"
file_path.write_text("module.exports = { foo: 'bar' };")
result = detect_module_system(project_root, file_path)
assert result == ModuleSystem.COMMONJS
def test_detect_esm_from_typescript_extension(self):
"""Test detection of ES modules from TypeScript file extensions."""
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 = () => <div />;")
assert detect_module_system(project_root, tsx_file) == ModuleSystem.ES_MODULE
# Test .mts files
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."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
# Create package.json with explicit commonjs type
package_json = project_root / "package.json"
package_json.write_text(json.dumps({"type": "commonjs"}))
# TypeScript file should still be detected as ESM
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
def test_detect_esm_from_import_syntax(self):
"""Test detection of ES modules from import syntax."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
file_path = project_root / "module.js"
file_path.write_text("import { foo } from './bar';\nexport const baz = 1;")
result = detect_module_system(project_root, file_path)
assert result == ModuleSystem.ES_MODULE
def test_detect_commonjs_from_require_syntax(self):
"""Test detection of CommonJS from require syntax."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
file_path = project_root / "module.js"
file_path.write_text("const foo = require('./bar');\nmodule.exports = { baz: 1 };")
result = detect_module_system(project_root, file_path)
assert result == ModuleSystem.COMMONJS
def test_default_to_commonjs(self):
"""Test default to CommonJS when uncertain."""
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
result = detect_module_system(project_root)
assert result == ModuleSystem.COMMONJS
class TestImportStatementGeneration:
"""Tests for import statement generation."""
def test_commonjs_named_import(self):
"""Test CommonJS named import statement."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "lib" / "utils.js"
source = tmpdir / "tests" / "utils.test.js"
result = get_import_statement(ModuleSystem.COMMONJS, target, source, ["foo", "bar"])
assert result == "const { foo, bar } = require('../lib/utils');"
def test_esm_named_import(self):
"""Test ES module named import statement."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "lib" / "utils.js"
source = tmpdir / "tests" / "utils.test.js"
result = get_import_statement(ModuleSystem.ES_MODULE, target, source, ["foo", "bar"])
assert result == "import { foo, bar } from '../lib/utils';"
def test_commonjs_default_import(self):
"""Test CommonJS default import statement."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "lib" / "utils.js"
source = tmpdir / "tests" / "utils.test.js"
result = get_import_statement(ModuleSystem.COMMONJS, target, source)
assert result == "const utils = require('../lib/utils');"
def test_esm_default_import(self):
"""Test ES module default import statement."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "lib" / "utils.js"
source = tmpdir / "tests" / "utils.test.js"
result = get_import_statement(ModuleSystem.ES_MODULE, target, source)
assert result == "import utils from '../lib/utils';"
def test_relative_path_same_directory(self):
"""Test import from same directory."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "utils.js"
source = tmpdir / "index.js"
result = get_import_statement(ModuleSystem.COMMONJS, target, source, ["foo"])
assert result == "const { foo } = require('./utils');"
def test_relative_path_subdirectory(self):
"""Test import from subdirectory."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "lib" / "helpers" / "utils.js"
source = tmpdir / "tests" / "test.js"
result = get_import_statement(ModuleSystem.COMMONJS, target, source, ["foo"])
assert result == "const { foo } = require('../lib/helpers/utils');"
def test_relative_path_parent_directory(self):
"""Test import from parent directory."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
target = tmpdir / "utils.js"
source = tmpdir / "tests" / "unit" / "test.js"
result = get_import_statement(ModuleSystem.COMMONJS, target, source, ["foo"])
assert result == "const { foo } = require('../../utils');"
class TestModuleSystemConversion:
"""Tests for CommonJS <-> ESM conversion."""
def test_convert_simple_destructured_require(self):
"""Test converting simple destructured require to import."""
code = "const { foo, bar } = require('./module');"
result = convert_commonjs_to_esm(code)
assert result == "import { foo, bar } from './module';"
def test_convert_destructured_require_with_alias(self):
"""Test converting destructured require with alias to import with 'as'."""
code = "const { foo: aliasedFoo } = require('./module');"
result = convert_commonjs_to_esm(code)
assert result == "import { foo as aliasedFoo } from './module';"
def test_convert_mixed_destructured_require(self):
"""Test converting mixed destructured require (some aliased, some not)."""
code = "const { foo, bar: aliasedBar, baz } = require('./module');"
result = convert_commonjs_to_esm(code)
assert result == "import { foo, bar as aliasedBar, baz } from './module';"
def test_convert_destructured_with_whitespace(self):
"""Test that whitespace is handled correctly in destructuring."""
code = "const { foo : aliasedFoo , bar } = require('./module');"
result = convert_commonjs_to_esm(code)
assert result == "import { foo as aliasedFoo, bar } from './module';"
def test_convert_simple_require(self):
"""Test converting simple require to default import."""
code = "const module = require('./module');"
result = convert_commonjs_to_esm(code)
assert result == "import module from './module';"
def test_convert_property_access_require(self):
"""Test converting require with property access to named import."""
code = "const foo = require('./module').bar;"
result = convert_commonjs_to_esm(code)
assert result == "import { bar as foo } from './module';"
def test_convert_property_access_default(self):
"""Test converting require().default to default import."""
code = "const foo = require('./module').default;"
result = convert_commonjs_to_esm(code)
assert result == "import foo from './module';"
def test_convert_multiple_requires(self):
"""Test converting multiple requires in one code block."""
code = """const { db: dbCore, cache } = require('@budibase/backend-core');
const utils = require('./utils');
const { process } = require('./processor');"""
result = convert_commonjs_to_esm(code)
expected = """import { db as dbCore, cache } from '@budibase/backend-core';
import utils from './utils';
import { process } from './processor';"""
assert result == expected
def test_convert_esm_to_commonjs_named(self):
"""Test converting named imports to destructured require."""
code = "import { foo, bar } from './module';"
result = convert_esm_to_commonjs(code)
assert result == "const { foo, bar } = require('./module');"
def test_convert_esm_to_commonjs_default(self):
"""Test converting default import to simple require."""
code = "import module from './module';"
result = convert_esm_to_commonjs(code)
assert result == "const module = require('./module');"
def test_convert_esm_to_commonjs_with_alias(self):
"""Test converting import with 'as' to destructured require.
Note: ESM uses 'as' but the regex keeps it as-is in the output.
This is acceptable since the test is primarily for CommonJS -> ESM conversion.
"""
code = "import { foo as aliasedFoo } from './module';"
result = convert_esm_to_commonjs(code)
# The current implementation preserves 'as' syntax which works for our use case
assert result == "const { foo as aliasedFoo } = require('./module');"
def test_real_world_budibase_import(self):
"""Test the real-world case from Budibase that was failing."""
code = "const { queue, context, db: dbCore, cache, events } = require('@budibase/backend-core');"
result = convert_commonjs_to_esm(code)
expected = "import { queue, context, db as dbCore, cache, events } from '@budibase/backend-core';"
assert result == expected
class TestAddJsExtensionsToRelativeImports:
"""Tests for adding .js extensions to relative imports in ESM mode."""
def test_add_js_extension_to_relative_import(self):
"""Test adding .js extension to relative import without extension."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = "import TreeNode from '../../injector/topology-tree/tree-node';"
result = add_js_extensions_to_relative_imports(code)
expected = "import TreeNode from '../../injector/topology-tree/tree-node.js';"
assert result == expected
def test_add_js_extension_to_single_dot_import(self):
"""Test adding .js extension to same-directory import."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = "import { foo } from './module';"
result = add_js_extensions_to_relative_imports(code)
expected = "import { foo } from './module.js';"
assert result == expected
def test_skip_imports_with_existing_extensions(self):
"""Test that imports with extensions are left unchanged."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = "import TreeNode from '../../tree-node.js';"
result = add_js_extensions_to_relative_imports(code)
assert result == code
code2 = "import TreeNode from '../../tree-node.ts';"
result2 = add_js_extensions_to_relative_imports(code2)
assert result2 == code2
def test_skip_node_modules_imports(self):
"""Test that node_modules imports are left unchanged."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = "import assert from 'node:assert/strict';"
result = add_js_extensions_to_relative_imports(code)
assert result == code
code2 = "import { describe } from 'mocha';"
result2 = add_js_extensions_to_relative_imports(code2)
assert result2 == code2
def test_multiple_imports(self):
"""Test handling multiple imports in one code block."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = """import assert from 'node:assert/strict';
import TreeNode from '../../injector/topology-tree/tree-node';
import { helper } from './helper';"""
result = add_js_extensions_to_relative_imports(code)
expected = """import assert from 'node:assert/strict';
import TreeNode from '../../injector/topology-tree/tree-node.js';
import { helper } from './helper.js';"""
assert result == expected
def test_named_imports(self):
"""Test adding extensions to named imports."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = "import { foo, bar } from '../utils/helpers';"
result = add_js_extensions_to_relative_imports(code)
expected = "import { foo, bar } from '../utils/helpers.js';"
assert result == expected
def test_namespace_imports(self):
"""Test adding extensions to namespace imports."""
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
code = "import * as helpers from '../utils';"
result = add_js_extensions_to_relative_imports(code)
expected = "import * as helpers from '../utils.js';"
assert result == expected