Repository: swc-project/swc (rust)

Issue:
`jsc.baseUrl` is not resolving the require/import path
### Discussed in https://github.com/swc-project/swc/discussions/8665

x-ref: #8660

<div type='discussions-op-text'>

<sup>Originally posted by **devjiwonchoi** February 26, 2024</sup>
Hello, fellow engineers and appreciatable SWC maintainers,

I wish to understand if the following behavior is intended and if so, the reason for it:

### TL;DR

Why is `jsc.baseUrl` not resolving the require path as either relative or absolute?

### Context

Using `transform` or `transformSync` from `@swc/core`.

Based on this error message:

```sh
Error: failed to handle: base_dir(`.`) must be absolute. Please ensure that `jsc.baseUrl` is specified correctly.
```

I've understood that the `baseUrl` path must be absolute which differs from `tsconfig.json`, so I'm passing `process.cwd()`.

### Code

```ts
import { foo } from "src/foo"
console.log(foo)
```

```ts
await transform(code, { jsx: { baseUrl: process.cwd() } })
```

Expected:

```js
import { foo } from "./src/foo" // or a resolved absolute path?
console.log(foo)
```

Output:

```ts
import { foo } from "src/foo"
console.log(foo)
```

## Repro

https://github.com/devjiwonchoi/repro-swc-base-url-paths

```sh
pnpm i
pnpm tsx .
```


Pull request:
fix(es/module): Allow `FileName::Anon` from node resolver
**Description:**


**Related issue:**
 
 - Closes #8674

Patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 1047aea7d5..b08ed4d14b 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -19,6 +19,10 @@ env:
   # https://github.com/actions/setup-node/issues/899#issuecomment-1819151595
   SKIP_YARN_COREPACK_CHECK: 1
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: "${{ github.event_name == 'pull_request' }}"
+
 jobs:
   cargo-fmt:
     name: Cargo fmt
diff --git a/crates/swc_ecma_loader/src/resolvers/node.rs b/crates/swc_ecma_loader/src/resolvers/node.rs
index e08a8bd1b4..eb44710a40 100644
--- a/crates/swc_ecma_loader/src/resolvers/node.rs
+++ b/crates/swc_ecma_loader/src/resolvers/node.rs
@@ -413,12 +413,27 @@ impl NodeModulesResolver {
         Ok(None)
     }
 
-    fn resolve_filename(&self, base: &FileName, target: &str) -> Result<FileName, Error> {
+    fn resolve_filename(&self, base: &FileName, module_specifier: &str) -> Result<FileName, Error> {
         debug!(
             "Resolving {} from {:#?} for {:#?}",
-            target, base, self.target_env
+            module_specifier, base, self.target_env
         );
 
+        {
+            // Handle absolute path
+
+            let path = Path::new(module_specifier);
+
+            if let Ok(file) = self
+                .resolve_as_file(path)
+                .or_else(|_| self.resolve_as_directory(path, false))
+            {
+                if let Ok(file) = self.wrap(file) {
+                    return Ok(file);
+                }
+            }
+        }
+
         let base = match base {
             FileName::Real(v) => v,
             _ => bail!("node-resolver supports only files"),
@@ -437,10 +452,10 @@ impl NodeModulesResolver {
             if let Some(pkg_base) = find_package_root(base) {
                 if let Some(item) = BROWSER_CACHE.get(&pkg_base) {
                     let value = item.value();
-                    if value.module_ignores.contains(target) {
-                        return Ok(FileName::Custom(target.into()));
+                    if value.module_ignores.contains(module_specifier) {
+                        return Ok(FileName::Custom(module_specifier.into()));
                     }
-                    if let Some(rewrite) = value.module_rewrites.get(target) {
+                    if let Some(rewrite) = value.module_rewrites.get(module_specifier) {
                         return self.wrap(Some(rewrite.to_path_buf()));
                     }
                 }
@@ -449,21 +464,21 @@ impl NodeModulesResolver {
 
         // Handle builtin modules for nodejs
         if let TargetEnv::Node = self.target_env {
-            if target.starts_with("node:") {
-                return Ok(FileName::Custom(target.into()));
+            if module_specifier.starts_with("node:") {
+                return Ok(FileName::Custom(module_specifier.into()));
             }
 
-            if is_core_module(target) {
-                return Ok(FileName::Custom(format!("node:{}", target)));
+            if is_core_module(module_specifier) {
+                return Ok(FileName::Custom(format!("node:{}", module_specifier)));
             }
         }
 
         // Aliases allow browser shims to be renamed so we can
         // map `stream` to `stream-browserify` for example
-        let target = if let Some(alias) = self.alias.get(target) {
+        let target = if let Some(alias) = self.alias.get(module_specifier) {
             &alias[..]
         } else {
-            target
+            module_specifier
         };
 
         let target_path = Path::new(target);
diff --git a/crates/swc_ecma_loader/src/resolvers/tsc.rs b/crates/swc_ecma_loader/src/resolvers/tsc.rs
index 357ea3961a..b8b1ef2d7c 100644
--- a/crates/swc_ecma_loader/src/resolvers/tsc.rs
+++ b/crates/swc_ecma_loader/src/resolvers/tsc.rs
@@ -320,13 +320,9 @@ where
         }
 
         let path = self.base_url.join(module_specifier);
-        #[cfg(windows)]
-        let path_string: String = path.to_string_lossy().replace("\\", "/");
-        #[cfg(not(windows))]
-        let path_string: String = path.to_string_lossy().to_string();
 
         // https://www.typescriptlang.org/docs/handbook/modules/reference.html#baseurl
-        if let Ok(v) = self.invoke_inner_resolver(base, path_string.as_str()) {
+        if let Ok(v) = self.invoke_inner_resolver(base, &path.to_string_lossy()) {
             return Ok(v);
         }
 
diff --git a/crates/swc_ecma_transforms_module/src/path.rs b/crates/swc_ecma_transforms_module/src/path.rs
index 3df6d962f8..a7a23766a8 100644
--- a/crates/swc_ecma_transforms_module/src/path.rs
+++ b/crates/swc_ecma_transforms_module/src/path.rs
@@ -280,13 +280,16 @@ where
                 v.parent()
                     .ok_or_else(|| anyhow!("failed to get parent of {:?}", v))?,
             ),
-            FileName::Anon => {
-                if cfg!(target_arch = "wasm32") {
-                    panic!("Please specify `filename`")
-                } else {
-                    Cow::Owned(current_dir().expect("failed to get current directory"))
+            FileName::Anon => match &self.config.base_dir {
+                Some(v) => Cow::Borrowed(&**v),
+                None => {
+                    if cfg!(target_arch = "wasm32") {
+                        panic!("Please specify `filename`")
+                    } else {
+                        Cow::Owned(current_dir().expect("failed to get current directory"))
+                    }
                 }
-            }
+            },
             _ => {
                 unreachable!(
                     "Node path provider does not support using `{:?}` as a base file name",


Test patch:
diff --git a/crates/swc/tests/fixture/issues-8xxx/8674/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8674/input/.swcrc
new file mode 100644
index 0000000000..0620ec21b5
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8674/input/.swcrc
@@ -0,0 +1,6 @@
+{
+    "$schema": "http://json.schemastore.org/swcrc",
+    "jsc": {
+        "baseUrl": "."
+    }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8674/input/index.ts b/crates/swc/tests/fixture/issues-8xxx/8674/input/index.ts
new file mode 100644
index 0000000000..87a1949d33
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8674/input/index.ts
@@ -0,0 +1,2 @@
+import { foo } from "src/foo"
+console.log(foo)
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8674/input/src/foo.ts b/crates/swc/tests/fixture/issues-8xxx/8674/input/src/foo.ts
new file mode 100644
index 0000000000..3deda8047e
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8674/input/src/foo.ts
@@ -0,0 +1,1 @@
+export { }
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8674/output/index.ts b/crates/swc/tests/fixture/issues-8xxx/8674/output/index.ts
new file mode 100644
index 0000000000..ec453b47e0
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8674/output/index.ts
@@ -0,0 +1,2 @@
+import { foo } from "./src/foo";
+console.log(foo);
diff --git a/crates/swc/tests/fixture/issues-8xxx/8674/output/src/foo.ts b/crates/swc/tests/fixture/issues-8xxx/8674/output/src/foo.ts
new file mode 100644
index 0000000000..2234b9cae1
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8674/output/src/foo.ts
@@ -0,0 +1,1 @@
+export { };
diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs
index a8381e71bb..c95aea9c2c 100644
--- a/crates/swc/tests/projects.rs
+++ b/crates/swc/tests/projects.rs
@@ -1,4 +1,5 @@
 use std::{
+    env::current_dir,
     fs::create_dir_all,
     path::{Path, PathBuf},
 };
@@ -1123,6 +1124,37 @@ fn issue_7513_2() {
     );
 }
 
+#[test]
+fn issue_8674_1() {
+    static INPUT: &str = "import { foo } from 'src/foo'";
+
+    let base_url = current_dir()
+        .unwrap()
+        .join("../../node-swc/tests/issue-8674")
+        .canonicalize()
+        .unwrap();
+
+    dbg!(&base_url);
+
+    let output = str_with_opt(
+        INPUT,
+        Options {
+            config: Config {
+                jsc: JscConfig {
+                    base_url,
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    )
+    .unwrap();
+    println!("{}", output);
+
+    assert_eq!(output.to_string(), "import { foo } from \"./src/foo\";\n");
+}
+
 #[testing::fixture("tests/minify/**/input.js")]
 fn minify(input_js: PathBuf) {
     let input_dir = input_js.parent().unwrap();
diff --git a/crates/swc_ecma_loader/tests/tsc_resolver.rs b/crates/swc_ecma_loader/tests/tsc_resolver.rs
index 9cef9faf5a..a8cebfa6a9 100644
--- a/crates/swc_ecma_loader/tests/tsc_resolver.rs
+++ b/crates/swc_ecma_loader/tests/tsc_resolver.rs
@@ -193,8 +193,10 @@ struct TestResolver(AHashMap<String, String>);
 
 impl Resolve for TestResolver {
     fn resolve(&self, _: &FileName, src: &str) -> Result<Resolution, Error> {
+        let src = src.replace('\\', "/");
+
         self.0
-            .get(src)
+            .get(&src)
             .cloned()
             .map(FileName::Custom)
             .map(|v| Resolution {
diff --git a/node-swc/__tests__/transform/issue_8674_test.mjs b/node-swc/__tests__/transform/issue_8674_test.mjs
new file mode 100644
index 0000000000..e823bb3f8f
--- /dev/null
+++ b/node-swc/__tests__/transform/issue_8674_test.mjs
@@ -0,0 +1,29 @@
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import swc from "../../..";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+it("should transpile import path correctly", async () => {
+    const baseUrl = path.resolve(__dirname, "../../tests/issue-8674");
+    console.log("baseUrl", baseUrl);
+    process.chdir(baseUrl);
+
+    const { code } = await swc.transform(
+        `
+        import { foo } from "src/foo"
+        console.log(foo)
+    `,
+        {
+            jsc: {
+                baseUrl,
+            },
+        }
+    );
+
+    expect(code).toMatchInlineSnapshot(`
+        "import { foo } from "./src/foo";
+        console.log(foo);
+        "
+    `);
+});
diff --git a/node-swc/tests/issue-8674/src/bar.ts b/node-swc/tests/issue-8674/src/bar.ts
new file mode 100644
index 0000000000..4548a26ba1
--- /dev/null
+++ b/node-swc/tests/issue-8674/src/bar.ts
@@ -0,0 +1,1 @@
+export default 'bar'
diff --git a/node-swc/tests/issue-8674/src/foo.ts b/node-swc/tests/issue-8674/src/foo.ts
new file mode 100644
index 0000000000..7e942cf45c
--- /dev/null
+++ b/node-swc/tests/issue-8674/src/foo.ts
@@ -0,0 +1,1 @@
+export default 'foo'