Repository: swc-project/swc (rust)

Issue:
swc appears to handle closures incorrectly in some cases
### Describe the bug

SWC produces invalid code for this case.

In my browser, this code correctly prints:

```
before set timeout, j is: 1
in timeout: j is 1
```

SWC compiles it into code that prints:

```
before set timeout, j is: 1
in timeout: j is 4
```

### Input code

```typescript
const myFunction = () => {
  for (let j = 0; j <= 3; j++) {
    for (const _ of []) {
    }

    if (j === 1) {
      console.log("before set timeout, j is:", j)

      setTimeout(() => {
        console.log("in timeout: j is", j)
      }, 50)
    }
  }

  return null
}

myFunction()
```


### Config

```json
I'm using next.js, so I don't have a swcrc that I know of.
```


### Playground link

_No response_

### Expected behavior

I expect SWC to compile code that prints the same output as the browser.

### Actual behavior

_No response_

### Version

Whatever's bundled with nextjs@latest (sorry)

### Additional context

_No response_

Pull request description:
fix(es/block-scoping): Pop scope correctly on early returns

**Description:**
swc_ecma_transforms_compat:
 - `block_scoping`: Pop scope even on fast-path.

**Related issue (if exists):**
 - Closes #3347.

Patch:
diff --git a/crates/swc_common/src/errors/diagnostic.rs b/crates/swc_common/src/errors/diagnostic.rs
index c1e4815e53..e2735b1f1b 100644
--- a/crates/swc_common/src/errors/diagnostic.rs
+++ b/crates/swc_common/src/errors/diagnostic.rs
@@ -18,6 +18,10 @@ use std::fmt;
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub struct Diagnostic {
     pub level: Level,
     pub message: Vec<(String, Style)>,
@@ -32,6 +36,10 @@ pub struct Diagnostic {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub enum DiagnosticId {
     Error(String),
     Lint(String),
@@ -43,6 +51,10 @@ pub enum DiagnosticId {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub struct SubDiagnostic {
     pub level: Level,
     pub message: Vec<(String, Style)>,
diff --git a/crates/swc_common/src/errors/mod.rs b/crates/swc_common/src/errors/mod.rs
index d60821468f..091961d061 100644
--- a/crates/swc_common/src/errors/mod.rs
+++ b/crates/swc_common/src/errors/mod.rs
@@ -44,6 +44,10 @@ mod styled_buffer;
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub enum Applicability {
     MachineApplicable,
     HasPlaceholders,
@@ -56,6 +60,10 @@ pub enum Applicability {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub struct CodeSuggestion {
     /// Each substitute can have multiple variants due to multiple
     /// applicable suggestions
@@ -103,6 +111,10 @@ pub struct CodeSuggestion {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub struct Substitution {
     pub parts: Vec<SubstitutionPart>,
 }
@@ -112,6 +124,10 @@ pub struct Substitution {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub struct SubstitutionPart {
     pub span: Span,
     pub snippet: String,
@@ -786,6 +802,10 @@ impl Handler {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub enum Level {
     Bug,
     Fatal,
diff --git a/crates/swc_common/src/errors/snippet.rs b/crates/swc_common/src/errors/snippet.rs
index 9b79363de0..9c91ef2bdd 100644
--- a/crates/swc_common/src/errors/snippet.rs
+++ b/crates/swc_common/src/errors/snippet.rs
@@ -180,6 +180,10 @@ pub struct StyledString {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub enum Style {
     MainHeaderMsg,
     HeaderMsg,
diff --git a/crates/swc_common/src/plugin.rs b/crates/swc_common/src/plugin.rs
index a357de35f3..d2290eb2d1 100644
--- a/crates/swc_common/src/plugin.rs
+++ b/crates/swc_common/src/plugin.rs
@@ -15,9 +15,10 @@ use std::any::type_name;
     derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
 )]
 /// Enum for possible errors while running transform via plugin.
-/// Mostly this enum is being used to bubble up error occurred inside of plugin
-/// runtime which we can't transparently pass forward Error itself. Host (SWC)
-/// will try to attach context if possible then raise it as Error.
+/// This error indicates internal operation failure either in plugin_runner
+/// or plugin_macro. Plugin's transform fn itself does not allow to return
+/// error - instead it should use provided `handler` to emit corresponding error
+/// to the host.
 pub enum PluginError {
     /// Occurs when failed to convert size passed from host / guest into usize
     /// or similar for the conversion. This is an internal error rasied via
@@ -30,10 +31,6 @@ pub enum PluginError {
     /// raw bytes: when serialze failed, there's nothing we can pass between
     /// runtime.
     Serialize(String),
-    /// When plugin fails to transform for some reason.
-    /// This is incomplete yet as it is unclear what kind of data plugin want to
-    /// forward into host.
-    Transform,
 }
 
 /// Wraps internal representation of serialized data. Consumers should not
diff --git a/crates/swc_common/src/syntax_pos.rs b/crates/swc_common/src/syntax_pos.rs
index 63d6964321..75e6cb4873 100644
--- a/crates/swc_common/src/syntax_pos.rs
+++ b/crates/swc_common/src/syntax_pos.rs
@@ -188,6 +188,10 @@ impl FileName {
     feature = "diagnostic-serde",
     derive(serde::Serialize, serde::Deserialize)
 )]
+#[cfg_attr(
+    feature = "plugin-base",
+    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
+)]
 pub struct MultiSpan {
     primary_spans: Vec<Span>,
     span_labels: Vec<(Span, String)>,
diff --git a/crates/swc_ecma_transforms_compat/src/es2015/block_scoping.rs b/crates/swc_ecma_transforms_compat/src/es2015/block_scoping.rs
index a69a92470e..dee8a1f37b 100644
--- a/crates/swc_ecma_transforms_compat/src/es2015/block_scoping.rs
+++ b/crates/swc_ecma_transforms_compat/src/es2015/block_scoping.rs
@@ -110,6 +110,7 @@ impl BlockScoping {
             let mut v = FunctionFinder { found: false };
             body_stmt.visit_with(&mut v);
             if !v.found {
+                self.scope.pop();
                 return;
             }
         }
diff --git a/crates/swc_plugin/src/context.rs b/crates/swc_plugin/src/context.rs
new file mode 100644
index 0000000000..deb19dd029
--- /dev/null
+++ b/crates/swc_plugin/src/context.rs
@@ -0,0 +1,46 @@
+use swc_common::{errors::Diagnostic, plugin::Serialized};
+
+extern "C" {
+    fn __emit_diagnostics(bytes_ptr: i32, bytes_ptr_len: i32);
+    fn __free(bytes_ptr: i32, size: i32) -> i32;
+}
+
+/// Emitter for the Diagnostic in plugin's context by borrowing host's
+/// environment context.
+pub struct Diagnostics {}
+
+impl Diagnostics {
+    pub fn emit(&self, diagnostic: Diagnostic) {
+        let diag = Serialized::serialize(&diagnostic).expect("Should able to serialize Diagnostic");
+        let diag_ref = diag.as_ref();
+
+        let ptr = diag_ref.as_ptr() as _;
+        let len = diag_ref.len() as i32;
+
+        unsafe {
+            __emit_diagnostics(ptr, len);
+            __free(ptr, len);
+        }
+    }
+}
+
+/// A struct contains set of utility fn to access host environment inside
+/// of plugin. This is being used for certain operations such as sending
+/// diagnostics back to the host.
+pub struct HostContext {
+    pub diagnostics: Diagnostics,
+}
+
+impl HostContext {
+    pub fn new() -> HostContext {
+        HostContext {
+            diagnostics: Diagnostics {},
+        }
+    }
+}
+
+impl Default for HostContext {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/crates/swc_plugin/src/lib.rs b/crates/swc_plugin/src/lib.rs
index 161da0c649..f9bf9ae548 100644
--- a/crates/swc_plugin/src/lib.rs
+++ b/crates/swc_plugin/src/lib.rs
@@ -10,6 +10,14 @@ pub mod ast {
     pub use swc_ecma_visit::*;
 }
 
+pub mod errors {
+    pub use swc_common::errors::{Diagnostic, Level};
+}
+
+mod context;
+pub mod environment {
+    pub use crate::context::*;
+}
 // We don't set target cfg as it'll block macro expansions
 // in ide (i.e rust-analyzer) or non-wasm target `cargo check`
 pub use swc_plugin_macro::plugin_module;
diff --git a/crates/swc_plugin_macro/src/lib.rs b/crates/swc_plugin_macro/src/lib.rs
index 0712a9c322..637fe0d10a 100644
--- a/crates/swc_plugin_macro/src/lib.rs
+++ b/crates/swc_plugin_macro/src/lib.rs
@@ -102,14 +102,9 @@ fn handle_func(func: ItemFn) -> TokenStream {
             }
             let config = config.expect("Should be a string");
 
+            let host_context = swc_plugin::environment::HostContext::new();
             // Take original plugin fn ident, then call it with interop'ed args
-            let transformed_program = #ident(program, config);
-            if transformed_program.is_err() {
-                let err = transformed_program.expect_err("Should be an error");
-                return construct_error_ptr(err);
-            }
-
-            let transformed_program = transformed_program.expect("Should be a transformed program");
+            let transformed_program = #ident(program, config, &host_context);
 
             // Serialize transformed result, return back to the host.
             let serialized_result = swc_plugin::Serialized::serialize(&transformed_program);
diff --git a/crates/swc_plugin_runner/src/lib.rs b/crates/swc_plugin_runner/src/lib.rs
index 13c9c60a6b..c99c06bf45 100644
--- a/crates/swc_plugin_runner/src/lib.rs
+++ b/crates/swc_plugin_runner/src/lib.rs
@@ -9,6 +9,7 @@ use parking_lot::Mutex;
 use resolve::PluginCache;
 use swc_common::{
     collections::AHashMap,
+    errors::{Diagnostic, HANDLER},
     plugin::{PluginError, Serialized},
 };
 use wasmer::{
@@ -19,27 +20,46 @@ use wasmer_wasi::{is_wasi_module, WasiState};
 
 pub mod resolve;
 
+fn copy_bytes_into_host(memory: &Memory, bytes_ptr: i32, bytes_ptr_len: i32) -> Vec<u8> {
+    let ptr: WasmPtr<u8, Array> = WasmPtr::new(bytes_ptr as _);
+
+    // Deref & read through plugin's wasm memory space via returned ptr
+    let derefed_ptr = ptr
+        .deref(memory, 0, bytes_ptr_len as u32)
+        .expect("Should able to deref from given ptr");
+
+    derefed_ptr
+        .iter()
+        .enumerate()
+        .take(bytes_ptr_len as usize)
+        .map(|(_size, cell)| cell.get())
+        .collect::<Vec<u8>>()
+}
+
 /// Set plugin's transformed result into host's enviroment.
 /// This is an `imported` fn - when we instantiate plugin module, we inject this
 /// fn into pluging's export space. Once transform completes, plugin will call
 /// this to set its result back to host.
 fn set_transform_result(env: &HostEnvironment, bytes_ptr: i32, bytes_ptr_len: i32) {
     if let Some(memory) = env.memory_ref() {
-        let ptr: WasmPtr<u8, Array> = WasmPtr::new(bytes_ptr as _);
-
-        // Deref & read through plugin's wasm memory space via returned ptr
-        let derefed_ptr = ptr
-            .deref(memory, 0, bytes_ptr_len as u32)
-            .expect("Should able to deref from given ptr");
-
-        let transformed_raw_bytes = derefed_ptr
-            .iter()
-            .enumerate()
-            .take(bytes_ptr_len as usize)
-            .map(|(_size, cell)| cell.get())
-            .collect::<Vec<u8>>();
+        (*env.transform_result.lock()) = copy_bytes_into_host(memory, bytes_ptr, bytes_ptr_len);
+    }
+}
 
-        (*env.transform_result.lock()) = transformed_raw_bytes;
+fn emit_diagnostics(env: &HostEnvironment, bytes_ptr: i32, bytes_ptr_len: i32) {
+    if let Some(memory) = env.memory_ref() {
+        if HANDLER.is_set() {
+            HANDLER.with(|handler| {
+                let diagnostics_bytes = copy_bytes_into_host(memory, bytes_ptr, bytes_ptr_len);
+                let serialized = Serialized::new_for_plugin(&diagnostics_bytes[..], bytes_ptr_len);
+                let diagnostic = Serialized::deserialize::<Diagnostic>(&serialized)
+                    .expect("Should able to be deserialized into diagnsotic");
+
+                let mut builder =
+                    swc_common::errors::DiagnosticBuilder::new_diagnostic(handler, diagnostic);
+                builder.emit();
+            })
+        }
     }
 }
 
@@ -143,12 +163,21 @@ fn load_plugin(
             let set_transform_result_fn_decl = Function::new_native_with_env(
                 &wasmer_store,
                 HostEnvironment {
-                    memory: LazyInit::new(),
+                    memory: LazyInit::default(),
                     transform_result: transform_result.clone(),
                 },
                 set_transform_result,
             );
 
+            let emit_diagnostics_fn_decl = Function::new_native_with_env(
+                &wasmer_store,
+                HostEnvironment {
+                    memory: LazyInit::default(),
+                    transform_result: transform_result.clone(),
+                },
+                emit_diagnostics,
+            );
+
             // Plugin binary can be either wasm32-wasi or wasm32-unknown-unknown
             let import_object = if is_wasi_module(&module) {
                 // Create the `WasiEnv`.
@@ -167,6 +196,7 @@ fn load_plugin(
                 // communicate with host
                 let mut env = Exports::new();
                 env.insert("__set_transform_result", set_transform_result_fn_decl);
+                env.insert("__emit_diagnostics", emit_diagnostics_fn_decl);
                 import_object.register("env", env);
                 import_object
             }
@@ -175,7 +205,8 @@ fn load_plugin(
             else {
                 imports! {
                     "env" => {
-                        "__set_transform_result" => set_transform_result_fn_decl
+                        "__set_transform_result" => set_transform_result_fn_decl,
+                        "__emit_diagnostics" => emit_diagnostics_fn_decl
                     }
                 }
             };
@@ -280,7 +311,6 @@ impl PluginTransformTracker {
                 PluginError::Deserialize((msg, ..)) | PluginError::Serialize(msg) => {
                     Err(anyhow!("{}", msg))
                 }
-                PluginError::Transform => Err(anyhow!("Failed to apply transform via plugin")),
                 _ => Err(anyhow!(
                     "Unexpected error occurred while running plugin transform"
                 )),


Test patch:
diff --git a/crates/swc/tests/tsc-references/for-inStatementsArray_es5.1.normal.js b/crates/swc/tests/tsc-references/for-inStatementsArray_es5.1.normal.js
index 82bb583bff..34bc8aa66d 100644
--- a/crates/swc/tests/tsc-references/for-inStatementsArray_es5.1.normal.js
+++ b/crates/swc/tests/tsc-references/for-inStatementsArray_es5.1.normal.js
@@ -17,8 +17,8 @@ for(var x1 in a){
         }
     }
 }
-var i = void 0;
-var j = void 0;
+var i;
+var j;
 for(i in a){
     for(j in b){
         var a12 = a[i];
diff --git a/crates/swc/tests/tsc-references/for-inStatementsArray_es5.2.minified.js b/crates/swc/tests/tsc-references/for-inStatementsArray_es5.2.minified.js
index 59403e1e25..a8657d9b9c 100644
--- a/crates/swc/tests/tsc-references/for-inStatementsArray_es5.2.minified.js
+++ b/crates/swc/tests/tsc-references/for-inStatementsArray_es5.2.minified.js
@@ -1,9 +1,8 @@
 for(var x in a){
-    var a, b, s;
+    var a, b, i, j, s;
     a[x], a[x], a[+x], b[x], b[x], b[+x];
 }
 for(var x1 in a)for(var y in a)for(var z in a)a[x1], a[y], a[z];
-var i = void 0, j = void 0;
 for(i in a)for(j in b)a[i], a[j];
 for(var s in a)a[s];
 for(s in a)a[s];
diff --git a/crates/swc/tests/tsc-references/objectRestForOf_es5.1.normal.js b/crates/swc/tests/tsc-references/objectRestForOf_es5.1.normal.js
index ea413e5f7e..9c882bd8d1 100644
--- a/crates/swc/tests/tsc-references/objectRestForOf_es5.1.normal.js
+++ b/crates/swc/tests/tsc-references/objectRestForOf_es5.1.normal.js
@@ -81,8 +81,8 @@ try {
         }
     }
 }
-var xx = void 0;
-var rrestOff = void 0;
+var xx;
+var rrestOff;
 var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
 try {
     for(var _iterator1 = array[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
diff --git a/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.1.normal.js b/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.1.normal.js
index d776075776..c13ac8dbd6 100644
--- a/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.1.normal.js
+++ b/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.1.normal.js
@@ -5,7 +5,7 @@
 var list = [];
 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
 try {
-    for(var _iterator = list[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
+    var _loop = function(_iterator, _step) {
         var comp = _step.value;
         comp.sp.y = comp.sp.r.find(function(k) {
             return k.c == (comp.xp ? '1' : '0');
@@ -31,7 +31,8 @@ try {
                 }
             }
         }
-    }
+    };
+    for(var _iterator = list[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true)_loop(_iterator, _step);
 } catch (err) {
     _didIteratorError = true;
     _iteratorError = err;
diff --git a/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.2.minified.js b/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.2.minified.js
index 33088f77b7..4fbadf96a8 100644
--- a/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.2.minified.js
+++ b/crates/swc/tests/tsc-references/optionalChainingInLoop_es5.2.minified.js
@@ -1,6 +1,6 @@
-var _iteratorNormalCompletion = !0, _didIteratorError = !1, _iteratorError = void 0;
+var list = [], _iteratorNormalCompletion = !0, _didIteratorError = !1, _iteratorError = void 0;
 try {
-    for(var _step, _iterator = [][Symbol.iterator](); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = !0){
+    for(var _step, _loop = function(_iterator, _step) {
         var comp = _step.value;
         comp.sp.y = comp.sp.r.find(function(k) {
             return k.c == (comp.xp ? "1" : "0");
@@ -20,7 +20,7 @@ try {
                 if (_didIteratorError1) throw _iteratorError1;
             }
         }
-    }
+    }, _iterator = list[Symbol.iterator](); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = !0)_loop(_iterator, _step);
 } catch (err) {
     _didIteratorError = !0, _iteratorError = err;
 } finally{
diff --git a/crates/swc/tests/vercel/loader-only/issue-3347/1/input/index.js b/crates/swc/tests/vercel/loader-only/issue-3347/1/input/index.js
new file mode 100644
index 0000000000..0003c40d0f
--- /dev/null
+++ b/crates/swc/tests/vercel/loader-only/issue-3347/1/input/index.js
@@ -0,0 +1,18 @@
+const myFunction = () => {
+    for (let j = 0; j <= 3; j++) {
+        for (const _ of []) {
+        }
+
+        if (j === 1) {
+            console.log("before set timeout, j is:", j)
+
+            setTimeout(() => {
+                console.log("in timeout: j is", j)
+            }, 50)
+        }
+    }
+
+    return null
+}
+
+myFunction()
\ No newline at end of file
diff --git a/crates/swc/tests/vercel/loader-only/issue-3347/1/output/index.js b/crates/swc/tests/vercel/loader-only/issue-3347/1/output/index.js
new file mode 100644
index 0000000000..b7ca43b8a6
--- /dev/null
+++ b/crates/swc/tests/vercel/loader-only/issue-3347/1/output/index.js
@@ -0,0 +1,32 @@
+var myFunction = function() {
+    var _loop = function(j) {
+        var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
+        try {
+            for(var _iterator = [][Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
+                var _ = _step.value;
+            }
+        } catch (err) {
+            _didIteratorError = true;
+            _iteratorError = err;
+        } finally{
+            try {
+                if (!_iteratorNormalCompletion && _iterator.return != null) {
+                    _iterator.return();
+                }
+            } finally{
+                if (_didIteratorError) {
+                    throw _iteratorError;
+                }
+            }
+        }
+        if (j === 1) {
+            console.log("before set timeout, j is:", j);
+            setTimeout(function() {
+                console.log("in timeout: j is", j);
+            }, 50);
+        }
+    };
+    for(var j = 0; j <= 3; j++)_loop(j);
+    return null;
+};
+myFunction();
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-3347/exec.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-3347/exec.js
new file mode 100644
index 0000000000..0003c40d0f
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-3347/exec.js
@@ -0,0 +1,18 @@
+const myFunction = () => {
+    for (let j = 0; j <= 3; j++) {
+        for (const _ of []) {
+        }
+
+        if (j === 1) {
+            console.log("before set timeout, j is:", j)
+
+            setTimeout(() => {
+                console.log("in timeout: j is", j)
+            }, 50)
+        }
+    }
+
+    return null
+}
+
+myFunction()
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs b/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs
index b837cb7f50..93d2e0e129 100644
--- a/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs
+++ b/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs
@@ -1,5 +1,7 @@
+use std::{fs::read_to_string, path::PathBuf};
 use swc_common::{chain, Mark};
 use swc_ecma_parser::Syntax;
+use swc_ecma_transforms_base::resolver::resolver;
 use swc_ecma_transforms_compat::{
     es2015,
     es2015::{block_scoping, for_of::for_of},
@@ -1008,3 +1010,13 @@ for (let b = 0; b < a; b++) {
 expect(expected).toEqual([0,1,2,3,4]);
 "
 );
+
+#[testing::fixture("tests/block-scoping/**/exec.js")]
+fn exec(input: PathBuf) {
+    let input = read_to_string(&input).unwrap();
+    compare_stdout(
+        Default::default(),
+        |_| chain!(resolver(), block_scoping()),
+        &input,
+    );
+}
diff --git a/crates/swc_plugin_runner/tests/integration.rs b/crates/swc_plugin_runner/tests/integration.rs
index 5404b21f43..afc7596172 100644
--- a/crates/swc_plugin_runner/tests/integration.rs
+++ b/crates/swc_plugin_runner/tests/integration.rs
@@ -4,7 +4,7 @@ use std::{
     path::{Path, PathBuf},
     process::{Command, Stdio},
 };
-use swc_common::{plugin::Serialized, FileName};
+use swc_common::{errors::HANDLER, plugin::Serialized, FileName};
 use swc_ecma_ast::{CallExpr, Callee, EsVersion, Expr, Lit, MemberExpr, Program, Str};
 use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax};
 use swc_ecma_visit::{Visit, VisitWith};
@@ -100,6 +100,34 @@ fn internal() -> Result<(), Error> {
     })
     .expect("Should able to run single plugin transform");
 
+    // run single plugin with handler
+    testing::run_test2(false, |cm, handler| {
+        let fm = cm.new_source_file(FileName::Anon, "console.log(foo)".into());
+
+        let lexer = Lexer::new(
+            Syntax::Es(EsConfig {
+                ..Default::default()
+            }),
+            EsVersion::latest(),
+            StringInput::from(&*fm),
+            None,
+        );
+        let mut parser = Parser::new_from(lexer);
+
+        let program = parser.parse_program().unwrap();
+
+        let program = Serialized::serialize(&program).expect("Should serializable");
+        let config = Serialized::serialize(&"{}".to_string()).expect("Should serializable");
+
+        let _res = HANDLER.set(&handler, || {
+            swc_plugin_runner::apply_js_plugin("internal-test", &path, &mut None, config, program)
+                .expect("Plugin should apply transform")
+        });
+
+        Ok(())
+    })
+    .expect("Should able to run single plugin transform with handler");
+
     // Run multiple plugins.
     testing::run_test(false, |cm, _handler| {
         let fm = cm.new_source_file(FileName::Anon, "console.log(foo)".into());
diff --git a/tests/rust-plugins/swc_internal_plugin/src/lib.rs b/tests/rust-plugins/swc_internal_plugin/src/lib.rs
index 88ff944d2f..6751850c70 100644
--- a/tests/rust-plugins/swc_internal_plugin/src/lib.rs
+++ b/tests/rust-plugins/swc_internal_plugin/src/lib.rs
@@ -1,4 +1,4 @@
-use swc_plugin::{ast::*, plugin_module, PluginError, DUMMY_SP};
+use swc_plugin::{ast::*, plugin_module, DUMMY_SP, errors::{Diagnostic, Level}, environment::{HostContext}};
 
 struct ConsoleOutputReplacer;
 
@@ -36,10 +36,11 @@ impl VisitMut for ConsoleOutputReplacer {
 ///     i32 /* 0 means success */
 ///
 /// if plugin need to handle low-level ptr directly. However, there are important steps
-/// manually need to be performed like transformed results back to host. Refer swc_plugin_macro
+/// manually need to be performed like sending transformed results back to host. Refer swc_plugin_macro
 /// how does it work internally.
 #[plugin_module]
-pub fn process(program: Program, _plugin_config: String) -> Result<Program, PluginError> {
-    let transformed_program = program.fold_with(&mut as_folder(ConsoleOutputReplacer));
-    Ok(transformed_program)
+pub fn process(program: Program, _plugin_config: String, ctx: &HostContext) -> Program {
+    let test_diag = Diagnostic::new(Level::Error, "test_diag_error_from_plugin");
+    ctx.diagnostics.emit(test_diag);
+    program.fold_with(&mut as_folder(ConsoleOutputReplacer))
 }