How to Build and Run SWC Plugin Transformations with WebAssembly

SWC executes custom transform plugins as WebAssembly binaries using a Wasmer-based runtime, where plugins are scaffolded with swc plugin new, compiled to wasm32-unknown-unknown or wasm32-wasip1, and loaded via compile_wasm_plugins before being executed through the abstract Runtime trait.

SWC is a Rust-based compiler that supports extensible transformations through WebAssembly plugins. This architecture allows developers to write custom AST transforms in Rust, compile them to portable Wasm modules, and execute them safely within SWC's multi-threaded pipeline. Understanding how to build and run SWC plugin transformations with WebAssembly enables you to create reusable, high-performance code transformations that work across JavaScript and Rust environments.

Architecture of the SWC Plugin System

WebAssembly support in SWC centers on the Wasmer runtime. The system uses three core components: the plugin compiler (compile_wasm_plugins), the runtime abstraction (Runtime trait), and the transform executor (RustPlugins).

When SWC initializes, it preloads Wasm bytes into a shared cache. During transformation, the RustPlugins Pass serializes the AST into PluginSerializedBytes, passes it to the Wasm module via the runtime, and deserializes the result back into a Program AST.

Step 1: Scaffold a New Plugin Project

Use the CLI to generate a template project with the correct Cargo configuration.

swc plugin new plugins/my-plugin \
  --target-type wasm32-unknown-unknown \
  --name my-plugin

This command creates:

Step 2: Implement the Transform Logic

Edit src/lib.rs to implement your AST manipulation using the #[plugin_transform] macro.

use swc_core::ecma::{
    ast::{Ident, Program},
    visit::{VisitMut, VisitMutWith},
};
use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};

pub struct RenameVisitor;

impl VisitMut for RenameVisitor {
    fn visit_mut_ident(&mut self, i: &mut Ident) {
        i.sym = "__renamed".into();
    }
}

#[plugin_transform]
pub fn rename_all(program: Program, _metadata: TransformPluginProgramMetadata) -> Program {
    program.visit_mut_with(&mut RenameVisitor);
    program
}

The macro handles low-level pointer interop required by the Wasm host, allowing you to work with standard Rust types while the runtime manages memory safety across the host-guest boundary.

Step 3: Compile to WebAssembly

Build the plugin using the generated cargo alias defined in .cargo/config.toml.

cd plugins/my-plugin
cargo build-wasm32 --release

This produces target/wasm32-unknown-unknown/release/my_plugin.wasm. For WASI support, use cargo build-wasip1 instead.

How SWC Loads and Executes Plugins at Runtime

Understanding the runtime internals helps debug performance and compatibility issues.

Plugin Compilation and Caching

In crates/swc/src/plugin.rs, the compile_wasm_plugins function resolves plugin paths, loads raw Wasm bytes, and stores them in PLUGIN_MODULE_CACHE. This cache prevents redundant compilation across multiple transformation calls.

The Runtime Trait

The Runtime trait in crates/swc_plugin_runner/src/runtime.rs abstracts the Wasm execution environment. The default Wasmer implementation provides:

  • Module instantiation
  • Memory allocation
  • Serialized data transfer
  • Transform invocation

Execution Flow

RustPlugins::apply_inner performs the actual transformation:

  1. Serializes the current Program AST into PluginSerializedBytes
  2. Calls create_plugin_transform_executor with the cached module
  3. The Wasmer runtime calls the exported transform function in the Wasm module
  4. Deserializes the returned bytes back into a Program AST

Running Transformations with Your Plugin

Command Line Usage

Run the compiled Wasm plugin directly via the SWC CLI:

swc -p ./my_plugin.wasm input.js -o output.js

JavaScript/Node.js API

Load and execute the plugin via @swc/core by passing the Wasm bytes to the experimental plugins array:

const swc = require("@swc/core");
const fs = require("fs");

const pluginWasm = fs.readFileSync("./my_plugin.wasm");

const result = swc.transformSync("let x = 1;", {
  jsc: {
    experimental: {
      plugins: [
        [pluginWasm, {}] // Tuple of [wasm_bytes, config_json]
      ]
    }
  }
});

console.log(result.code); // "let __renamed = 1;"

Direct Rust API

For Rust-based tooling, manually configure the plugin pipeline:

use swc_core::plugin::{PluginConfig, compile_wasm_plugins};
use swc_plugin_runner::wasmer::WasmerRuntime;

// Preload plugins into cache
let plugin_cfg = PluginConfig(
    "path/to/my_plugin.wasm".into(), 
    serde_json::json!({})
);
compile_wasm_plugins(None, &[plugin_cfg.clone()], &WasmerRuntime)?;

Key Source Files

Reference these files when debugging or extending functionality:

Summary

  • Scaffold new plugins using swc plugin new to generate correct Cargo configuration and boilerplate in src/lib.rs
  • Implement transforms using the #[plugin_transform] macro, which handles Wasm host interop automatically
  • Compile to wasm32-unknown-unknown or wasm32-wasip1 using the provided cargo aliases
  • Cache occurs via compile_wasm_plugins in crates/swc/src/plugin.rs, storing modules in PLUGIN_MODULE_CACHE
  • Execute through the Runtime trait in crates/swc_plugin_runner/src/runtime.rs, which uses Wasmer to instantiate and call the Wasm module
  • Serialize the AST before passing to the plugin, then deserialize the transformed result back into the pipeline via RustPlugins::apply_inner

Frequently Asked Questions

What is the difference between wasm32-unknown-unknown and wasm32-wasip1 targets?

The wasm32-unknown-unknown target produces a pure WebAssembly module without system dependencies, making it smaller and faster to load. The wasm32-wasip1 target includes WASI syscalls for file system or clock access, which is necessary if your plugin needs system capabilities. Most SWC plugins use wasm32-unknown-unknown unless they specifically require I/O operations.

How does SWC handle plugin errors and panics?

SWC's Wasmer runtime isolates each plugin in a separate WebAssembly instance. If a plugin panics or returns an error, the runtime catches the trap and returns a Rust Result error, preventing the host process from crashing. The error propagates through RustPlugins::apply_inner and can be handled by the calling transformation pipeline.

Can I distribute SWC plugins via npm?

Yes. The scaffold command generates a package.json configured to run the cargo build during prepublishOnly. You can publish the compiled .wasm file as a binary attachment, allowing JavaScript consumers to load it directly via fs.readFileSync and pass it to @swc/core's experimental plugins array.

Why does SWC use Wasmer instead of Wasmtime or other runtimes?

The Runtime trait in crates/swc_plugin_runner/src/runtime.rs abstracts the underlying Wasm engine, but the default implementation uses Wasmer for its lightweight footprint and excellent Rust integration. This choice enables near-native performance while maintaining a stable ABI for plugin authors, though the trait design allows for future runtime swapping if needed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →