Repository: swc-project/swc (rust)

Issue:
Specifying plugin deletes filename from generated sourcemaps with transform*
<!--
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**
If `transformSync` is called with config have plugins, generated sourcemap deletes specified filename results sourmapped stacktrace points to anonymous function instead.

**Input code**

```
const { transformSync, plugins } = require('@swc/core');

const ret = transformSync(`
console.log('hello world')
`, {
  filename: 'test.ts',
  sourceMaps: true,
  // plugin: plugins([])
});

console.log(ret);
```

without `plugin` config:

```
{
    code: "console.log('hello world');\n",
    map: `{"version":3,"sources":["test.ts"],"sourcesContent":["\\nconsole.log('hello world')\\n"],"names":[],"mappings":"AACA,OAAO,CAAC,GAAG,EAAC,WAAa"}`
}
```

with `plugin` config:

```
{
  code: "console.log('hello world');\n",
  map: `{"version":3,"sources":["<anon>"],"sourcesContent":["\\nconsole.log('hello world')\\n"],"names":[],"mappings":"AACA,OAAO,CAAC,GAAG,EAAC,WAAa"}`
}
```

**Config**

```json
{
  // Please copy and paste your .swcrc file here
}
```

**Expected behavior**
A clear and concise description of what you expected to happen.

**Version**
The version of @swc/core: 1.2.74

**Additional context**
Add any other context about the problem here.

It doesn't matter actual plugin is specified or not, having empty plugin set (`plugins([])`) could repro issue.


Pull request description:
feat(transform): allow to specify filename when parse module
- closes #2023 

This PR attempts to support sourcemapped stack with provided filename for `transform*` interfaces. When there are plguins specified, it goes through path to parse into module / program first to pass into plugin. When those occur, it always assigns filename to `FileName::Anon` even though transform option specifies it.

PR pass through `filename` as optional param to compiler's `parse*` and use it if it's specified, otherwise falls back to default behavior. Note PR did not make any public interface exposure to exported `pasre*` in node-swc, instead just passes down to compiler's fn via transform. This is mostly I am not sure if it's desired usecases to parse* allows to specify by default.

Patch:
diff --git a/node-swc/src/index.ts b/node-swc/src/index.ts
index ff73a885cd..0ee431d2d2 100644
--- a/node-swc/src/index.ts
+++ b/node-swc/src/index.ts
@@ -43,22 +43,22 @@ export class Compiler {
     src: string,
     options: ParseOptions & { isModule: false }
   ): Promise<Script>;
-  parse(src: string, options?: ParseOptions): Promise<Module>;
-  async parse(src: string, options?: ParseOptions): Promise<Program> {
+  parse(src: string, options?: ParseOptions, filename?: string): Promise<Module>;
+  async parse(src: string, options?: ParseOptions, filename?: string): Promise<Program> {
     options = options || { syntax: "ecmascript" };
     options.syntax = options.syntax || "ecmascript";
 
-    const res = await bindings.parse(src, toBuffer(options));
+    const res = await bindings.parse(src, toBuffer(options), filename);
     return JSON.parse(res);
   }
 
   parseSync(src: string, options: ParseOptions & { isModule: false }): Script;
-  parseSync(src: string, options?: ParseOptions): Module;
-  parseSync(src: string, options?: ParseOptions): Program {
+  parseSync(src: string, options?: ParseOptions, filename?: string): Module;
+  parseSync(src: string, options?: ParseOptions, filename?: string): Program {
     options = options || { syntax: "ecmascript" };
     options.syntax = options.syntax || "ecmascript";
 
-    return JSON.parse(bindings.parseSync(src, toBuffer(options)));
+    return JSON.parse(bindings.parseSync(src, toBuffer(options), filename));
   }
 
   parseFile(
@@ -121,7 +121,7 @@ export class Compiler {
     if (plugin) {
       const m =
         typeof src === "string"
-          ? await this.parse(src, options?.jsc?.parser)
+          ? await this.parse(src, options?.jsc?.parser, options.filename)
           : src;
       return this.transform(plugin(m), newOptions);
     }
@@ -142,7 +142,7 @@ export class Compiler {
 
     if (plugin) {
       const m =
-        typeof src === "string" ? this.parseSync(src, options?.jsc?.parser) : src;
+        typeof src === "string" ? this.parseSync(src, options?.jsc?.parser, options.filename) : src;
       return this.transformSync(plugin(m), newOptions);
     }
 
diff --git a/node/binding/src/parse.rs b/node/binding/src/parse.rs
index b8000c73ba..e39e9c836c 100644
--- a/node/binding/src/parse.rs
+++ b/node/binding/src/parse.rs
@@ -3,7 +3,7 @@ use crate::{
     util::{CtxtExt, MapErr},
 };
 use anyhow::Context as _;
-use napi::{CallContext, Env, JsObject, JsString, Task};
+use napi::{CallContext, Either, Env, JsObject, JsString, JsUndefined, Task};
 use std::{
     path::{Path, PathBuf},
     sync::Arc,
@@ -87,14 +87,19 @@ impl Task for ParseFileTask {
     }
 }
 
-#[js_function(2)]
+#[js_function(3)]
 pub fn parse(ctx: CallContext) -> napi::Result<JsObject> {
     let c = get_compiler(&ctx);
     let src = ctx.get::<JsString>(0)?.into_utf8()?;
     let options: ParseOptions = ctx.get_deserialized(1)?;
+    let filename = ctx.get::<Either<JsString, JsUndefined>>(2)?;
+    let filename = if let Either::A(value) = filename {
+        FileName::Real(value.into_utf8()?.as_str()?.to_owned().into())
+    } else {
+        FileName::Anon
+    };
 
-    let fm =
-        c.cm.new_source_file(FileName::Anon, src.as_str()?.to_string());
+    let fm = c.cm.new_source_file(filename, src.as_str()?.to_string());
 
     ctx.env
         .spawn(ParseTask {
@@ -105,16 +110,22 @@ pub fn parse(ctx: CallContext) -> napi::Result<JsObject> {
         .map(|t| t.promise_object())
 }
 
-#[js_function(2)]
+#[js_function(3)]
 pub fn parse_sync(cx: CallContext) -> napi::Result<JsString> {
     let c = get_compiler(&cx);
 
     c.run(|| {
         let src = cx.get::<JsString>(0)?.into_utf8()?.as_str()?.to_owned();
         let options: ParseOptions = cx.get_deserialized(1)?;
+        let filename = cx.get::<Either<JsString, JsUndefined>>(2)?;
+        let filename = if let Either::A(value) = filename {
+            FileName::Real(value.into_utf8()?.as_str()?.to_owned().into())
+        } else {
+            FileName::Anon
+        };
 
         let program = {
-            let fm = c.cm.new_source_file(FileName::Anon, src);
+            let fm = c.cm.new_source_file(filename, src);
             c.parse_js(
                 fm,
                 options.target,


Test patch:
diff --git a/node-swc/__tests__/transform/sourcemap_test.js b/node-swc/__tests__/transform/sourcemap_test.js
index 3351c7355f..f73791ba34 100644
--- a/node-swc/__tests__/transform/sourcemap_test.js
+++ b/node-swc/__tests__/transform/sourcemap_test.js
@@ -15,7 +15,7 @@ console.log('foo')
 
 
     expect(out.map).toBeTruthy();
-    validate(out.code, out.map, {'input.js': raw});
+    validate(out.code, out.map, { 'input.js': raw });
 
     // await sourceMap.SourceMapConsumer.with(JSON.parse(out.map), null, async (consumer) => {
     //     consumer.eachMapping((mapping) => {
@@ -33,6 +33,33 @@ console.log('foo')
 
 })
 
+it('should handle sourcemap correctly when parse module', async () => {
+    const raw = `
+class Foo extends Array {
+}
+console.log('foo')
+    `;
+    const out1 = swc.transformSync(raw, {
+        filename: 'input.js',
+        sourceMaps: true,
+        plugin: swc.plugins([m => m])
+    });
+
+
+    expect(out1.map).toBeTruthy();
+    expect(JSON.parse(out1.map).sources).toEqual(['input.js']);
+    validate(out1.code, out1.map, { 'input.js': raw });
+
+    const out2 = swc.transformSync(raw, {
+        sourceMaps: true,
+        plugin: swc.plugins([m => m])
+    });
+
+    expect(out2.map).toBeTruthy();
+    expect(JSON.parse(out2.map).sources).toEqual(['<anon>']);
+    validate(out2.code, out2.map, { 'input.js': raw });
+})
+
 it('should handle input sourcemap correctly', async () => {
     const raw = `class Foo extends Array {}`;
     const out1 = swc.transformSync(raw, {
@@ -48,7 +75,7 @@ it('should handle input sourcemap correctly', async () => {
     });
 
     expect(out1.map).toBeTruthy();
-    validate(out1.code, out1.map, {'input.js': raw});
+    validate(out1.code, out1.map, { 'input.js': raw });
     console.log(out1.code);
 
     const out2 = swc.transformSync(out1.code, {
@@ -65,8 +92,8 @@ it('should handle input sourcemap correctly', async () => {
 
     console.log(out2.code);
     expect(out2.map).toBeTruthy();
-    validate(out2.code, out2.map, {'input2.js': out1.code});
-    validate(out2.code, out2.map, {'input.js': raw});
+    validate(out2.code, out2.map, { 'input2.js': out1.code });
+    validate(out2.code, out2.map, { 'input.js': raw });
 
     await sourceMap.SourceMapConsumer.with(JSON.parse(out1.map), null, async (consumer1) => {
         await sourceMap.SourceMapConsumer.with(JSON.parse(out2.map), null, async (consumer2) => {