Repository: swc-project/swc (rust)

Issue:
Imported function is not bundled using spack
<!--
If you are using swc at work, please considering adding your company to https://swc.rs/users/
If then, your issue will be fixed more quickly.
-->

**Describe the bug**

An imported function is not being included in the bundle leading to a runtime `ReferenceError`.

**Input code**

MVP: https://github.com/tmpfs/spack-tree-shaking

**Config**

https://github.com/tmpfs/spack-tree-shaking/blob/main/spack.config.js

**Expected behavior**

I expect the code for the `memoize` function to be included in the generated bundle.

The bundle output in `dist/main.js` looks like this but `memoize` is imported from `lodash` in [util.js](https://github.com/tmpfs/spack-tree-shaking/blob/main/src/util.js):

```javascript
const getEnvironmentTypeMemo = memoize((url)=>{
});
const getEnvironmentType = (url = window.location.href)=>getEnvironmentTypeMemo(url)
;
const name = getEnvironmentType();
console.log(name);
```

**Version**
The version of @swc/core: 1.2.76

**Additional context**

Possibly an issue with the tree shaking algorithm.

Pull request description:
fix(bundler): Fix handling of reexports from cjs modules
swc_bundler:
 - Handle reexports from cjs modules. (Closes #2124)
 - Inject `__spack_require__` helper if required.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index fe1c647fc2..6084d5757f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2234,7 +2234,7 @@ dependencies = [
 
 [[package]]
 name = "swc_bundler"
-version = "0.54.0"
+version = "0.54.1"
 dependencies = [
  "ahash",
  "anyhow",
diff --git a/bundler/Cargo.toml b/bundler/Cargo.toml
index e5097f512f..7a6c0caacd 100644
--- a/bundler/Cargo.toml
+++ b/bundler/Cargo.toml
@@ -9,7 +9,7 @@ include = ["Cargo.toml", "build.rs", "src/**/*.rs", "src/**/*.js"]
 license = "Apache-2.0/MIT"
 name = "swc_bundler"
 repository = "https://github.com/swc-project/swc.git"
-version = "0.54.0"
+version = "0.54.1"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [features]
diff --git a/bundler/src/bundler/chunk/merge.rs b/bundler/src/bundler/chunk/merge.rs
index affbb3b8c2..4fb5c101e3 100644
--- a/bundler/src/bundler/chunk/merge.rs
+++ b/bundler/src/bundler/chunk/merge.rs
@@ -18,10 +18,11 @@ use indexmap::IndexSet;
 use petgraph::EdgeDirection;
 #[cfg(feature = "concurrent")]
 use rayon::iter::ParallelIterator;
+use std::sync::atomic::Ordering;
 use swc_atoms::js_word;
 use swc_common::{sync::Lock, FileName, SyntaxContext, DUMMY_SP};
 use swc_ecma_ast::*;
-use swc_ecma_utils::{find_ids, prepend, private_ident};
+use swc_ecma_utils::{find_ids, prepend, private_ident, quote_ident, ExprFactory};
 use swc_ecma_visit::{noop_fold_type, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
 use EdgeDirection::Outgoing;
 
@@ -906,6 +907,89 @@ where
                         ref src,
                         ..
                     })) => {
+                        if let Some(export_src) = src {
+                            if let Some((src, _)) = info
+                                .exports
+                                .reexports
+                                .iter()
+                                .find(|s| s.0.src.value == export_src.value)
+                            {
+                                let dep = self.scope.get_module(src.module_id).unwrap();
+                                if !dep.is_es6 {
+                                    dep.helpers.require.store(true, Ordering::SeqCst);
+
+                                    let mut vars = vec![];
+                                    let mod_var = private_ident!("_cjs_module_");
+
+                                    vars.push(VarDeclarator {
+                                        span: DUMMY_SP,
+                                        name: Pat::Ident(mod_var.clone().into()),
+                                        init: Some(Box::new(Expr::Call(CallExpr {
+                                            span: DUMMY_SP,
+                                            callee: Ident::new(
+                                                "load".into(),
+                                                DUMMY_SP.with_ctxt(dep.export_ctxt()),
+                                            )
+                                            .as_callee(),
+                                            args: Default::default(),
+                                            type_args: Default::default(),
+                                        }))),
+                                        definite: Default::default(),
+                                    });
+                                    for s in specifiers {
+                                        match s {
+                                            ExportSpecifier::Namespace(s) => {
+                                                vars.push(VarDeclarator {
+                                                    span: s.span,
+                                                    name: Pat::Ident(s.name.clone().into()),
+                                                    init: Some(Box::new(Expr::Ident(
+                                                        mod_var.clone(),
+                                                    ))),
+                                                    definite: Default::default(),
+                                                });
+                                            }
+                                            ExportSpecifier::Default(s) => {
+                                                vars.push(VarDeclarator {
+                                                    span: DUMMY_SP,
+                                                    name: Pat::Ident(s.exported.clone().into()),
+                                                    init: Some(Box::new(
+                                                        mod_var
+                                                            .clone()
+                                                            .make_member(quote_ident!("default")),
+                                                    )),
+                                                    definite: Default::default(),
+                                                });
+                                            }
+                                            ExportSpecifier::Named(s) => {
+                                                vars.push(VarDeclarator {
+                                                    span: s.span,
+                                                    name: Pat::Ident(
+                                                        s.exported.clone().unwrap().into(),
+                                                    ),
+                                                    init: Some(Box::new(
+                                                        mod_var.clone().make_member(s.orig.clone()),
+                                                    )),
+                                                    definite: Default::default(),
+                                                });
+                                            }
+                                        }
+                                    }
+
+                                    if !vars.is_empty() {
+                                        new.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(
+                                            VarDecl {
+                                                span: DUMMY_SP,
+                                                kind: VarDeclKind::Const,
+                                                declare: Default::default(),
+                                                decls: vars,
+                                            },
+                                        ))));
+                                    }
+                                    continue;
+                                }
+                            }
+                        }
+
                         for s in specifiers {
                             match s {
                                 ExportSpecifier::Named(ExportNamedSpecifier {


Test patch:
diff --git a/bundler/tests/fixture/issue-2124/named-export/input/entry.ts b/bundler/tests/fixture/issue-2124/named-export/input/entry.ts
new file mode 100644
index 0000000000..24e1030b4d
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/named-export/input/entry.ts
@@ -0,0 +1,8 @@
+import { memoize } from './util';
+
+// Import directly from `lodash` instead and the module code is
+// included in the bundle
+//import {memoize} from './lodash';
+
+const name = memoize();
+console.log(name);
\ No newline at end of file
diff --git a/bundler/tests/fixture/issue-2124/named-export/input/lodash.ts b/bundler/tests/fixture/issue-2124/named-export/input/lodash.ts
new file mode 100644
index 0000000000..5cbf7c6be6
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/named-export/input/lodash.ts
@@ -0,0 +1,21 @@
+// This is a minimal reproduction from lodash@4.17.21
+
+function lodash(value) {
+    console.log('lodash')
+}
+
+function memoize() {
+    console.log('memoize')
+}
+
+lodash.memoize = memoize
+
+// Either of these lines cause this module
+// not to be included in the bundle. Member expression
+// on `module` or `exports`.
+module.exports = lodash;
+exports.memoize = memoize;
+
+  // NOTE: Indirection on `exports` will work
+  //const exporter = exports;
+  //exporter.memoize = memoize;
\ No newline at end of file
diff --git a/bundler/tests/fixture/issue-2124/named-export/input/util.ts b/bundler/tests/fixture/issue-2124/named-export/input/util.ts
new file mode 100644
index 0000000000..3e780ad3d7
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/named-export/input/util.ts
@@ -0,0 +1,3 @@
+
+// This indirection is necessary to reproduce the issue
+export { memoize } from './lodash';
\ No newline at end of file
diff --git a/bundler/tests/fixture/issue-2124/named-export/output/entry.inlined.ts b/bundler/tests/fixture/issue-2124/named-export/output/entry.inlined.ts
new file mode 100644
index 0000000000..9f7e4ebd92
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/named-export/output/entry.inlined.ts
@@ -0,0 +1,27 @@
+function __spack_require__(mod) {
+    var cache;
+    if (cache) {
+        return cache;
+    }
+    var module = {
+        exports: {
+        }
+    };
+    mod(module, module.exports);
+    cache = module.exports;
+    return cache;
+}
+var load = __spack_require__.bind(void 0, function(module, exports) {
+    function lodash(value) {
+        console.log('lodash');
+    }
+    function memoize() {
+        console.log('memoize');
+    }
+    lodash.memoize = memoize;
+    module.exports = lodash;
+    exports.memoize = memoize;
+});
+const _cjs_module_ = load(), memoize = _cjs_module_.memoize;
+const name = memoize();
+console.log(name);
diff --git a/bundler/tests/fixture/issue-2124/named-export/output/entry.ts b/bundler/tests/fixture/issue-2124/named-export/output/entry.ts
new file mode 100644
index 0000000000..9f7e4ebd92
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/named-export/output/entry.ts
@@ -0,0 +1,27 @@
+function __spack_require__(mod) {
+    var cache;
+    if (cache) {
+        return cache;
+    }
+    var module = {
+        exports: {
+        }
+    };
+    mod(module, module.exports);
+    cache = module.exports;
+    return cache;
+}
+var load = __spack_require__.bind(void 0, function(module, exports) {
+    function lodash(value) {
+        console.log('lodash');
+    }
+    function memoize() {
+        console.log('memoize');
+    }
+    lodash.memoize = memoize;
+    module.exports = lodash;
+    exports.memoize = memoize;
+});
+const _cjs_module_ = load(), memoize = _cjs_module_.memoize;
+const name = memoize();
+console.log(name);
diff --git a/bundler/tests/fixture/issue-2124/namespace-export/input/entry.ts b/bundler/tests/fixture/issue-2124/namespace-export/input/entry.ts
new file mode 100644
index 0000000000..3e765fc87c
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/namespace-export/input/entry.ts
@@ -0,0 +1,8 @@
+import { util } from './util';
+
+// Import directly from `lodash` instead and the module code is
+// included in the bundle
+//import {memoize} from './lodash';
+
+const name = util.memoize();
+console.log(name);
\ No newline at end of file
diff --git a/bundler/tests/fixture/issue-2124/namespace-export/input/lodash.ts b/bundler/tests/fixture/issue-2124/namespace-export/input/lodash.ts
new file mode 100644
index 0000000000..5cbf7c6be6
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/namespace-export/input/lodash.ts
@@ -0,0 +1,21 @@
+// This is a minimal reproduction from lodash@4.17.21
+
+function lodash(value) {
+    console.log('lodash')
+}
+
+function memoize() {
+    console.log('memoize')
+}
+
+lodash.memoize = memoize
+
+// Either of these lines cause this module
+// not to be included in the bundle. Member expression
+// on `module` or `exports`.
+module.exports = lodash;
+exports.memoize = memoize;
+
+  // NOTE: Indirection on `exports` will work
+  //const exporter = exports;
+  //exporter.memoize = memoize;
\ No newline at end of file
diff --git a/bundler/tests/fixture/issue-2124/namespace-export/input/util.ts b/bundler/tests/fixture/issue-2124/namespace-export/input/util.ts
new file mode 100644
index 0000000000..98c4ebec65
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/namespace-export/input/util.ts
@@ -0,0 +1,3 @@
+
+// This indirection is necessary to reproduce the issue
+export * as util from './lodash';
\ No newline at end of file
diff --git a/bundler/tests/fixture/issue-2124/namespace-export/output/entry.inlined.ts b/bundler/tests/fixture/issue-2124/namespace-export/output/entry.inlined.ts
new file mode 100644
index 0000000000..5510057300
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/namespace-export/output/entry.inlined.ts
@@ -0,0 +1,31 @@
+function __spack_require__(mod) {
+    var cache;
+    if (cache) {
+        return cache;
+    }
+    var module = {
+        exports: {
+        }
+    };
+    mod(module, module.exports);
+    cache = module.exports;
+    return cache;
+}
+var load = __spack_require__.bind(void 0, function(module, exports) {
+    function lodash(value) {
+        console.log('lodash');
+    }
+    function memoize() {
+        console.log('memoize');
+    }
+    lodash.memoize = memoize;
+    module.exports = lodash;
+    exports.memoize = memoize;
+    const mod = function() {
+        return {
+        };
+    }();
+});
+const _cjs_module_ = load();
+const name = _cjs_module_.memoize();
+console.log(name);
diff --git a/bundler/tests/fixture/issue-2124/namespace-export/output/entry.ts b/bundler/tests/fixture/issue-2124/namespace-export/output/entry.ts
new file mode 100644
index 0000000000..d3f3c96e26
--- /dev/null
+++ b/bundler/tests/fixture/issue-2124/namespace-export/output/entry.ts
@@ -0,0 +1,31 @@
+function __spack_require__(mod) {
+    var cache;
+    if (cache) {
+        return cache;
+    }
+    var module = {
+        exports: {
+        }
+    };
+    mod(module, module.exports);
+    cache = module.exports;
+    return cache;
+}
+var load = __spack_require__.bind(void 0, function(module, exports) {
+    function lodash(value) {
+        console.log('lodash');
+    }
+    function memoize() {
+        console.log('memoize');
+    }
+    lodash.memoize = memoize;
+    module.exports = lodash;
+    exports.memoize = memoize;
+    const mod = function() {
+        return {
+        };
+    }();
+});
+const _cjs_module_ = load(), util = _cjs_module_;
+const name = util.memoize();
+console.log(name);
diff --git a/node/bundler/tests/pass/issue-2124/named-export/input/entry.ts b/node/bundler/tests/pass/issue-2124/named-export/input/entry.ts
new file mode 100644
index 0000000000..24e1030b4d
--- /dev/null
+++ b/node/bundler/tests/pass/issue-2124/named-export/input/entry.ts
@@ -0,0 +1,8 @@
+import { memoize } from './util';
+
+// Import directly from `lodash` instead and the module code is
+// included in the bundle
+//import {memoize} from './lodash';
+
+const name = memoize();
+console.log(name);
\ No newline at end of file
diff --git a/node/bundler/tests/pass/issue-2124/named-export/input/lodash.ts b/node/bundler/tests/pass/issue-2124/named-export/input/lodash.ts
new file mode 100644
index 0000000000..5cbf7c6be6
--- /dev/null
+++ b/node/bundler/tests/pass/issue-2124/named-export/input/lodash.ts
@@ -0,0 +1,21 @@
+// This is a minimal reproduction from lodash@4.17.21
+
+function lodash(value) {
+    console.log('lodash')
+}
+
+function memoize() {
+    console.log('memoize')
+}
+
+lodash.memoize = memoize
+
+// Either of these lines cause this module
+// not to be included in the bundle. Member expression
+// on `module` or `exports`.
+module.exports = lodash;
+exports.memoize = memoize;
+
+  // NOTE: Indirection on `exports` will work
+  //const exporter = exports;
+  //exporter.memoize = memoize;
\ No newline at end of file
diff --git a/node/bundler/tests/pass/issue-2124/named-export/input/util.ts b/node/bundler/tests/pass/issue-2124/named-export/input/util.ts
new file mode 100644
index 0000000000..3e780ad3d7
--- /dev/null
+++ b/node/bundler/tests/pass/issue-2124/named-export/input/util.ts
@@ -0,0 +1,3 @@
+
+// This indirection is necessary to reproduce the issue
+export { memoize } from './lodash';
\ No newline at end of file
diff --git a/node/bundler/tests/pass/issue-2124/named-export/output/entry.js b/node/bundler/tests/pass/issue-2124/named-export/output/entry.js
new file mode 100644
index 0000000000..25e843883d
--- /dev/null
+++ b/node/bundler/tests/pass/issue-2124/named-export/output/entry.js
@@ -0,0 +1,36 @@
+function __spack_require__(mod) {
+    var cache;
+    if (cache) {
+        return cache;
+    }
+    var module = {
+        exports: {
+        }
+    };
+    mod(module, module.exports);
+    cache = module.exports;
+    return cache;
+}
+var load = __spack_require__.bind(void 0, function(module, exports) {
+    // This is a minimal reproduction from lodash@4.17.21
+    function lodash(value) {
+        console.log('lodash');
+    }
+    function memoize() {
+        console.log('memoize');
+    }
+    lodash.memoize = memoize;
+    // Either of these lines cause this module
+    // not to be included in the bundle. Member expression
+    // on `module` or `exports`.
+    module.exports = lodash;
+    exports.memoize = memoize; // NOTE: Indirection on `exports` will work
+     //const exporter = exports;
+     //exporter.memoize = memoize;
+});
+const _cjs_module_ = load(), memoize = _cjs_module_.memoize;
+// Import directly from `lodash` instead and the module code is
+// included in the bundle
+//import {memoize} from './lodash';
+const name = memoize();
+console.log(name);