Repository: swc-project/swc (rust)

Issue:
Can't use ESM with `jsc.paths`
### Describe the bug

Hey folks,

I've come into a bit of a pickle. Node 19 [drops support for `es-module-specifier-resolution`](https://stackoverflow.com/questions/74660824/nodejs-v19-drops-support-for-es-module-specifier-resolution-node-which-makes-i), which I have been using to support extension-less imports in my ESM codebase.

I realized this makes it impossible for me to continue using `jsc.paths` in my TypeScript project, as `ts` files automatically have their extensions removed, yet if I use `js` as an extension, SWC can't find the path and doesn't transform the import.

## Example

```
"paths": {
    "@my/pkg": ["libs/pkg/src/index.ts"]
}
```

results in:

```
import { fn } from 'libs/pkg/src/index';
```

and

```
"paths": {
    "@my/pkg": ["libs/pkg/src/index.js"]
}
```

results in:

```
import { fn } from '@my/pkg';
```



### Input code

```typescript
import { fn } from '@my/pkg';
```


### Config

```json
{
  "module": {
    "type": "es6"
  },
  "jsc": {
    "baseUrl": ".",
    "target": "es2020",
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "importMeta": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "keepClassNames": true,
    "paths": {
      "@my/pkg": ["libs/pkg/src/index.ts"],
    }
  }
}
```


### Playground link

_No response_

### Expected behavior

I think that SWC should provide a similar mechanism to how [TypeScript supports ESM](https://2ality.com/2021/06/typescript-esm-nodejs.html#typescript), where TS files can be imported as JS (i.e. `jsc.paths` should find and transform the path appropriately).

Better yet, now that you [_can_ import `ts` files in TS](https://github.com/microsoft/TypeScript/issues/53316), what I really wish is that you could just always use `ts` extensions for everything and SWC would be smart enough to know to replace them with `js` in the transpiled code instead of just removing the extension.

### Actual behavior

Right now, SWC defaults to removing TS extensions unless you manually import TS files as JS (which luckily TS supports), however, this breaks down when using `jsc.paths` as described above. Starting in Node 19, this will result in un-runnable code without a custom loader.

### Version

1.3.77

### Additional context

_No response_

Pull request:
fix(es/module): Fix logic for exact matches in `jsc.paths`
**Description:**

**Related issue:**

 - Closes #7829

Patch:
diff --git a/crates/swc_ecma_loader/src/resolvers/tsc.rs b/crates/swc_ecma_loader/src/resolvers/tsc.rs
index bf2f593693..b52942805d 100644
--- a/crates/swc_ecma_loader/src/resolvers/tsc.rs
+++ b/crates/swc_ecma_loader/src/resolvers/tsc.rs
@@ -259,27 +259,12 @@ where
                         return Ok(FileName::Real(tp.into()));
                     }
 
-                    if self.base_url_filename == *base {
-                        // Prevent infinite loop
-
-                        let replaced = self.base_url.join(&to[0]);
-                        if replaced.exists() {
-                            return Ok(FileName::Real(replaced));
-                        }
-
-                        return self
-                            .invoke_inner_resolver(base, &format!("./{}", &to[0]))
-                            .with_context(|| {
-                                format!(
-                                    "tried to resolve `{}` because `{}` was exactly matched",
-                                    to[0], from
-                                )
-                            });
-                    } else {
-                        return self
-                            .resolve(&self.base_url_filename, &format!("./{}", &to[0]))
-                            .context("failed to resolve using jsc.baseUrl as base");
+                    if let Ok(res) = self.resolve(&self.base_url_filename, &format!("./{}", &to[0]))
+                    {
+                        return Ok(res);
                     }
+
+                    return Ok(FileName::Real(self.base_url.join(&to[0])));
                 }
             }
         }


Test patch:
diff --git a/crates/swc/tests/fixture/issues-7xxx/7829/1/input/.swcrc b/crates/swc/tests/fixture/issues-7xxx/7829/1/input/.swcrc
new file mode 100644
index 0000000000..d4cbce916e
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-7xxx/7829/1/input/.swcrc
@@ -0,0 +1,24 @@
+{
+    "module": {
+        "type": "es6"
+    },
+    "jsc": {
+        "baseUrl": ".",
+        "target": "es2020",
+        "parser": {
+            "syntax": "typescript",
+            "decorators": true,
+            "importMeta": true
+        },
+        "transform": {
+            "legacyDecorator": true,
+            "decoratorMetadata": true
+        },
+        "keepClassNames": true,
+        "paths": {
+            "@my/pkg": [
+                "libs/pkg/src/index.ts"
+            ],
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-7xxx/7829/1/input/1.js b/crates/swc/tests/fixture/issues-7xxx/7829/1/input/1.js
new file mode 100644
index 0000000000..21dbd568a3
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-7xxx/7829/1/input/1.js
@@ -0,0 +1,4 @@
+import { fn } from '@my/pkg';
+
+
+console.log(fn)
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-7xxx/7829/1/output/1.js b/crates/swc/tests/fixture/issues-7xxx/7829/1/output/1.js
new file mode 100644
index 0000000000..90ba054224
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-7xxx/7829/1/output/1.js
@@ -0,0 +1,2 @@
+import { fn } from "./libs/pkg/src";
+console.log(fn);
diff --git a/crates/swc/tests/fixture/issues-7xxx/7829/2/input/.swcrc b/crates/swc/tests/fixture/issues-7xxx/7829/2/input/.swcrc
new file mode 100644
index 0000000000..a7f2c9c9ca
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-7xxx/7829/2/input/.swcrc
@@ -0,0 +1,24 @@
+{
+    "module": {
+        "type": "es6"
+    },
+    "jsc": {
+        "baseUrl": ".",
+        "target": "es2020",
+        "parser": {
+            "syntax": "typescript",
+            "decorators": true,
+            "importMeta": true
+        },
+        "transform": {
+            "legacyDecorator": true,
+            "decoratorMetadata": true
+        },
+        "keepClassNames": true,
+        "paths": {
+            "@my/pkg": [
+                "libs/pkg/src/index.js"
+            ],
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-7xxx/7829/2/input/1.js b/crates/swc/tests/fixture/issues-7xxx/7829/2/input/1.js
new file mode 100644
index 0000000000..21dbd568a3
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-7xxx/7829/2/input/1.js
@@ -0,0 +1,4 @@
+import { fn } from '@my/pkg';
+
+
+console.log(fn)
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-7xxx/7829/2/output/1.js b/crates/swc/tests/fixture/issues-7xxx/7829/2/output/1.js
new file mode 100644
index 0000000000..f8527ad825
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-7xxx/7829/2/output/1.js
@@ -0,0 +1,2 @@
+import { fn } from "./libs/pkg/src/index.js";
+console.log(fn);