Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions codeflash/languages/javascript/module_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,41 @@ def add_js_extension(module_path: str) -> str:
return module_path


def _convert_destructuring_to_imports(names_str: str) -> str:
"""Convert destructuring aliases to import aliases.

Converts:
a, b -> a, b
a: aliasA -> a as aliasA
a, b: aliasB -> a, b as aliasB

Args:
names_str: The destructuring pattern string (e.g., "a, b: aliasB")

Returns:
Import names string with aliases using 'as' syntax
"""
# Split by commas and process each name
parts = []
for name in names_str.split(","):
name = name.strip()
if ":" in name:
# Convert destructuring alias to import alias
# "a: aliasA" -> "a as aliasA"
original, alias = name.split(":", 1)
parts.append(f"{original.strip()} as {alias.strip()}")
else:
parts.append(name)
return ", ".join(parts)


# Replace destructured requires with named imports
def replace_destructured(match: re.Match) -> str:
names = match.group(2).strip()
module_path = add_js_extension(match.group(3))
return f"import {{ {names} }} from '{module_path}';"
# Convert destructuring aliases (a: b) to import aliases (a as b)
converted_names = _convert_destructuring_to_imports(names)
return f"import {{ {converted_names} }} from '{module_path}';"


# Replace property access requires with named imports with alias
Expand Down Expand Up @@ -244,12 +274,14 @@ def convert_commonjs_to_esm(code: str) -> str:
"""Convert CommonJS require statements to ES Module imports.

Converts:
const { foo, bar } = require('./module'); -> import { foo, bar } from './module';
const foo = require('./module'); -> import foo from './module';
const foo = require('./module').default; -> import foo from './module';
const foo = require('./module').bar; -> import { bar as foo } from './module';
const { foo, bar } = require('./module'); -> import { foo, bar } from './module';
const { foo: alias } = require('./module'); -> import { foo as alias } from './module';
const foo = require('./module'); -> import foo from './module';
const foo = require('./module').default; -> import foo from './module';
const foo = require('./module').bar; -> import { bar as foo } from './module';

Special handling:
- Destructuring aliases (a: b) are converted to import aliases (a as b)
- Local codeflash helper (./codeflash-jest-helper) is converted to npm package codeflash
because the local helper uses CommonJS exports which don't work in ESM projects

Expand Down
95 changes: 94 additions & 1 deletion tests/test_languages/test_javascript_module_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
import tempfile
from pathlib import Path

from codeflash.languages.javascript.module_system import ModuleSystem, detect_module_system, get_import_statement
from codeflash.languages.javascript.module_system import (
ModuleSystem,
convert_commonjs_to_esm,
convert_esm_to_commonjs,
detect_module_system,
get_import_statement,
)


class TestModuleSystemDetection:
Expand Down Expand Up @@ -192,3 +198,90 @@ def test_relative_path_parent_directory(self):
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
Loading