Repository: swc-project/swc (rust)

Issue:
jsc.paths regression in 1.3.79
### Describe the bug

swc re-writes import paths by following symlinks, it seems. I believe this is a regression from one of the changes @realtimetodie made when we originally got SWC working under Bazel.

It now looks like:

```
require("../../../../../../../private/var/tmp/_bazel_alexeagle/43e97bf6681e7317438fd77d405a1c4b/sandbox/darwin-sandbox/181/execroot/aspect_rules_swc/examples/paths/src/modules/moduleA");
```

Minimal repro:

1. clone https://github.com/aspect-build/rules_swc
2. `npx @bazel/bazelisk build --subcommands examples/paths:compile`
3. Open `bazel-bin/examples/paths/src/index.js` and you'll see the `require` path is relative
4. update `LATEST_SWC_VERSION = "v1.3.78"` to v1.3.79 in `swc/repositories.bzl`
5. repeat step 2 to build again
6. Look at the same index.js file and the require path now contains an absolute path to the root of the filesystem

If you need the minimal repro to not involve Bazel, we can engineer one that creates a similar symlink tree to reproduce the problem without it.
> Note, this is my fourth attempt to file an issue here, see prior attempt #8264.

### Input code

```typescript
import { moduleA } from "@modules/moduleA";

moduleA();
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript"
    },
    "target": "es2021",
    "baseUrl": "./src",
    "paths": {
      "@modules/*": [
        "./modules/*"
      ]
    }
  },
  "module": {
    "type": "commonjs"
  },
  "sourceMaps": true
}
```


### Playground link (or link to the minimal reproduction)

https://github.com/aspect-build/rules_swc/pull/216/files

### SWC Info output

swc-darwin-arm64 --version
SWC 0.91.64

### Expected behavior

SWC 1.3.78 produces

```
const _moduleA = require("./modules/moduleA/index");
```

In https://github.com/swc-project/swc/issues/8258 you suggested that we compare with what tsc produces. As I understand it, typescript never attempts to re-write import/require statements. So the result is here

https://github.com/aspect-build/rules_swc/pull/216/files#diff-e3a9eafb3b943b9863d6cea0b348d4ef40bc1b7bb5867a38ac889fcacea0cbf4R3

```
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var moduleA_1 = require("@modules/moduleA");
(0, moduleA_1.moduleA)();
```

### Actual behavior

SWC 1.3.79 produces

```
const _moduleA = require("../../../../../../../shared/cache/bazel/user_base/09a27d1eeea3b44d1580773090924b4d/sandbox/linux-sandbox/629/execroot/aspect_rules_swc/examples/paths/src/modules/moduleA");
```

### Version

1.3.79

### Additional context

In 1.3.78 this worked, so https://github.com/swc-project/swc/pull/7852 is the likely culprit, as @kdy1 agrees https://github.com/swc-project/swc/issues/8258#issuecomment-1804883347

Pull request:
fix(es/module): Read link if an import is resolved as symlink
**Description:**



**Related issue:**

 - Closes #8265

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a14c6c8c7a..7ec6df694e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -173,6 +173,36 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
+[[package]]
+name = "assert_cmd"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "assert_fs"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48"
+dependencies = [
+ "anstyle",
+ "doc-comment",
+ "globwalk",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "tempfile",
+]
+
 [[package]]
 name = "ast_node"
 version = "0.9.5"
@@ -358,7 +388,7 @@ dependencies = [
  "anyhow",
  "chrono",
  "either",
- "itertools",
+ "itertools 0.10.5",
  "nom",
  "once_cell",
  "quote",
@@ -369,6 +399,17 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "bstr"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
+dependencies = [
+ "memchr",
+ "regex-automata 0.4.3",
+ "serde",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.13.0"
@@ -849,7 +890,7 @@ dependencies = [
  "clap 4.4.6",
  "criterion-plot",
  "is-terminal",
- "itertools",
+ "itertools 0.10.5",
  "num-traits",
  "once_cell",
  "oorandom",
@@ -870,7 +911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
 dependencies = [
  "cast",
- "itertools",
+ "itertools 0.10.5",
 ]
 
 [[package]]
@@ -1124,6 +1165,12 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
 
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -1161,6 +1208,12 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
 
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
 [[package]]
 name = "either"
 version = "1.8.1"
@@ -1515,6 +1568,30 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
+[[package]]
+name = "globset"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "ignore",
+ "walkdir",
+]
+
 [[package]]
 name = "h2"
 version = "0.3.18"
@@ -1767,6 +1844,23 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
 
+[[package]]
+name = "ignore"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
+dependencies = [
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.9.3"
@@ -1855,6 +1949,15 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.6"
@@ -2080,7 +2183,7 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
 dependencies = [
- "regex-automata",
+ "regex-automata 0.1.10",
 ]
 
 [[package]]
@@ -2752,6 +2855,34 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
+[[package]]
+name = "predicates"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "itertools 0.11.0",
+ "predicates-core",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
 [[package]]
 name = "preset_env_base"
 version = "0.4.8"
@@ -3012,6 +3143,12 @@ dependencies = [
  "regex-syntax 0.6.29",
 ]
 
+[[package]]
+name = "regex-automata"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+
 [[package]]
 name = "regex-syntax"
 version = "0.6.29"
@@ -3823,6 +3960,8 @@ name = "swc_cli_impl"
 version = "0.4.71"
 dependencies = [
  "anyhow",
+ "assert_cmd",
+ "assert_fs",
  "atty",
  "clap 3.2.25",
  "glob",
@@ -5454,6 +5593,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "termtree"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
+
 [[package]]
 name = "testing"
 version = "0.35.11"
@@ -6124,6 +6269,15 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "waker-fn"
 version = "1.1.0"
diff --git a/crates/swc_cli_impl/Cargo.toml b/crates/swc_cli_impl/Cargo.toml
index 92a8b59468..36e5c03dd4 100644
--- a/crates/swc_cli_impl/Cargo.toml
+++ b/crates/swc_cli_impl/Cargo.toml
@@ -8,11 +8,19 @@ name        = "swc_cli_impl"
 repository  = "https://github.com/swc-project/swc.git"
 version     = "0.4.71"
 
+[[bin]]
+name = "swc"
+path = "src/main.rs"
+
+[lib]
+crate-type = ["rlib"]
+path       = "src/lib.rs"
+
 [features]
 default = []
 plugin = [
-    "swc_core/plugin_transform_host_native",
-    "swc_core/plugin_transform_host_native_filesystem_cache",
+  "swc_core/plugin_transform_host_native",
+  "swc_core/plugin_transform_host_native_filesystem_cache",
 ]
 
 [dependencies]
@@ -37,3 +45,7 @@ swc_core = { version = "0.86.71", features = [
     "common_concurrent",
     "base_concurrent",
 ], path = "../swc_core" }
+
+[dev-dependencies]
+assert_cmd = "2.0.12"
+assert_fs  = "1.0.13"
diff --git a/crates/swc_cli_impl/src/main.rs b/crates/swc_cli_impl/src/main.rs
new file mode 100644
index 0000000000..cd2a7ec01b
--- /dev/null
+++ b/crates/swc_cli_impl/src/main.rs
@@ -0,0 +1,5 @@
+//! Used for testing
+
+fn main() -> anyhow::Result<()> {
+    swc_cli_impl::run()
+}
diff --git a/crates/swc_ecma_transforms_module/src/path.rs b/crates/swc_ecma_transforms_module/src/path.rs
index e148d27d3e..874f699cd8 100644
--- a/crates/swc_ecma_transforms_module/src/path.rs
+++ b/crates/swc_ecma_transforms_module/src/path.rs
@@ -1,6 +1,7 @@
 use std::{
     borrow::Cow,
     env::current_dir,
+    fs::read_link,
     io,
     path::{Component, Path, PathBuf},
     sync::Arc,
@@ -212,7 +213,7 @@ where
         let orig_filename = module_specifier.split('/').last();
 
         let target = self.resolver.resolve(base, module_specifier);
-        let target = match target {
+        let mut target = match target {
             Ok(v) => v,
             Err(err) => {
                 warn!("import rewriter: failed to resolve: {}", err);
@@ -220,6 +221,15 @@ where
             }
         };
 
+        // Bazel uses symlink
+        //
+        // https://github.com/swc-project/swc/issues/8265
+        if let FileName::Real(resolved) = &target {
+            if let Ok(orig) = read_link(resolved) {
+                target = FileName::Real(orig);
+            }
+        }
+
         info!("Resolved to {}", target);
 
         let mut target = match target {


Test patch:
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/.swcrc
new file mode 100644
index 0000000000..5607dbd871
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/.swcrc
@@ -0,0 +1,17 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript"
+        },
+        "target": "es2021",
+        "baseUrl": "./src",
+        "paths": {
+            "@modules/*": [
+                "./modules/*"
+            ]
+        }
+    },
+    "module": {
+        "type": "commonjs"
+    },
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/index.ts b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/index.ts
new file mode 100644
index 0000000000..2c314ba73f
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/index.ts
@@ -0,0 +1,3 @@
+import { moduleA } from "@modules/moduleA";
+
+moduleA();
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/modules/moduleA/index.ts b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/modules/moduleA/index.ts
new file mode 100644
index 0000000000..9302fb8472
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/modules/moduleA/index.ts
@@ -0,0 +1,6 @@
+import { moduleB } from "@modules/moduleB";
+
+export const moduleA = () => {
+  console.log("This is module A");
+  moduleB();
+};
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/modules/moduleB/index.ts b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/modules/moduleB/index.ts
new file mode 100644
index 0000000000..2e97749483
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/input/src/modules/moduleB/index.ts
@@ -0,0 +1,1 @@
+export const moduleB = () => console.log("This is module B!");
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/index.ts b/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/index.ts
new file mode 100644
index 0000000000..d5613ac482
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/index.ts
@@ -0,0 +1,6 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+const _moduleA = require("./modules/moduleA");
+(0, _moduleA.moduleA)();
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/modules/moduleA/index.ts b/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/modules/moduleA/index.ts
new file mode 100644
index 0000000000..5555bec28e
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/modules/moduleA/index.ts
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+Object.defineProperty(exports, "moduleA", {
+    enumerable: true,
+    get: function() {
+        return moduleA;
+    }
+});
+const _moduleB = require("../moduleB");
+const moduleA = ()=>{
+    console.log("This is module A");
+    (0, _moduleB.moduleB)();
+};
diff --git a/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/modules/moduleB/index.ts b/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/modules/moduleB/index.ts
new file mode 100644
index 0000000000..dc843d5c69
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8265/1/output/src/modules/moduleB/index.ts
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+Object.defineProperty(exports, "moduleB", {
+    enumerable: true,
+    get: function() {
+        return moduleB;
+    }
+});
+const moduleB = ()=>console.log("This is module B!");
diff --git a/crates/swc_cli_impl/tests/fixture-manual/8265/.swcrc b/crates/swc_cli_impl/tests/fixture-manual/8265/.swcrc
new file mode 100644
index 0000000000..71f15470df
--- /dev/null
+++ b/crates/swc_cli_impl/tests/fixture-manual/8265/.swcrc
@@ -0,0 +1,18 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript"
+        },
+        "target": "es2021",
+        "baseUrl": "./src",
+        "paths": {
+            "@modules/*": [
+                "./modules/*"
+            ]
+        }
+    },
+    "module": {
+        "type": "commonjs"
+    },
+    "sourceMaps": true
+}
\ No newline at end of file
diff --git a/crates/swc_cli_impl/tests/fixture-manual/8265/src/index.ts b/crates/swc_cli_impl/tests/fixture-manual/8265/src/index.ts
new file mode 100644
index 0000000000..2c314ba73f
--- /dev/null
+++ b/crates/swc_cli_impl/tests/fixture-manual/8265/src/index.ts
@@ -0,0 +1,3 @@
+import { moduleA } from "@modules/moduleA";
+
+moduleA();
diff --git a/crates/swc_cli_impl/tests/fixture-manual/8265/src/modules/moduleA/index.ts b/crates/swc_cli_impl/tests/fixture-manual/8265/src/modules/moduleA/index.ts
new file mode 100644
index 0000000000..9302fb8472
--- /dev/null
+++ b/crates/swc_cli_impl/tests/fixture-manual/8265/src/modules/moduleA/index.ts
@@ -0,0 +1,6 @@
+import { moduleB } from "@modules/moduleB";
+
+export const moduleA = () => {
+  console.log("This is module A");
+  moduleB();
+};
diff --git a/crates/swc_cli_impl/tests/fixture-manual/8265/src/modules/moduleB/index.ts b/crates/swc_cli_impl/tests/fixture-manual/8265/src/modules/moduleB/index.ts
new file mode 100644
index 0000000000..2e97749483
--- /dev/null
+++ b/crates/swc_cli_impl/tests/fixture-manual/8265/src/modules/moduleB/index.ts
@@ -0,0 +1,1 @@
+export const moduleB = () => console.log("This is module B!");
diff --git a/crates/swc_cli_impl/tests/issues.rs b/crates/swc_cli_impl/tests/issues.rs
new file mode 100644
index 0000000000..279635b4e8
--- /dev/null
+++ b/crates/swc_cli_impl/tests/issues.rs
@@ -0,0 +1,78 @@
+use std::{
+    fs::{self, create_dir_all},
+    path::Path,
+    process::{Command, Stdio},
+};
+
+use anyhow::{Context, Result};
+use assert_cmd::prelude::*;
+use assert_fs::TempDir;
+
+fn cli() -> Result<Command> {
+    let mut cmd = Command::cargo_bin("swc").context("Failed to get swc binary")?;
+    cmd.stderr(Stdio::inherit());
+    Ok(cmd)
+}
+
+#[test]
+fn issue_8265_1() -> Result<()> {
+    let pwd = Path::new("tests/fixture-manual/8265").canonicalize()?;
+    let tmp = TempDir::new()?;
+
+    create_dir_all(tmp.path().join("src/modules/moduleA"))?;
+    create_dir_all(tmp.path().join("src/modules/moduleB"))?;
+
+    symlink(&pwd.join(".swcrc"), &tmp.path().join(".swcrc"));
+    symlink(&pwd.join("src/index.ts"), &tmp.path().join("src/index.ts"));
+    symlink(
+        &pwd.join("src/modules/moduleA/index.ts"),
+        &tmp.path().join("src/modules/moduleA/index.ts"),
+    );
+    symlink(
+        &pwd.join("src/modules/moduleB/index.ts"),
+        &tmp.path().join("src/modules/moduleB/index.ts"),
+    );
+
+    print_ls_alr(&tmp);
+
+    let mut cmd = cli()?;
+    cmd.current_dir(&tmp)
+        .arg("compile")
+        .arg("--source-maps")
+        .arg("false")
+        .arg("--config-file")
+        .arg(".swcrc")
+        .arg("--out-file")
+        .arg("src/index.js")
+        .arg("src/index.ts");
+
+    cmd.assert().success();
+
+    let content = fs::read_to_string(tmp.join("src/index.js"))?;
+    assert!(
+        content.contains("require(\"./modules/moduleA\")"),
+        "{}",
+        content
+    );
+
+    Ok(())
+}
+
+/// ln -s $a $b
+fn symlink(a: &Path, b: &Path) {
+    #[cfg(unix)]
+    {
+        std::os::unix::fs::symlink(a, b).unwrap();
+    }
+
+    #[cfg(windows)]
+    {
+        std::os::windows::fs::symlink_file(a, b).unwrap();
+    }
+}
+
+fn print_ls_alr(path: &Path) {
+    let mut cmd = Command::new("ls");
+    cmd.arg("-alR").arg(path);
+    cmd.status().unwrap();
+}