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:
Cargo.tomlconfigured as acdylib.cargo/config.tomlwith aliases forwasm32-unknown-unknownandwasm32-wasip1src/lib.rswith a starter#[plugin_transform]functionpackage.jsonfor npm distribution
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:
- Serializes the current
ProgramAST intoPluginSerializedBytes - Calls
create_plugin_transform_executorwith the cached module - The Wasmer runtime calls the exported
transformfunction in the Wasm module - Deserializes the returned bytes back into a
ProgramAST
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:
- [
crates/swc/src/plugin.rs](https://github.com/swc-project/swc/blob/main/crates/swc/src/plugin.rs) – Plugin loading andRustPluginsimplementation - [
crates/swc_plugin_runner/src/runtime.rs](https://github.com/swc-project/swc/blob/main/crates/swc_plugin_runner/src/runtime.rs) –Runtimetrait and Wasmer integration - [
crates/swc_cli_impl/src/commands/plugin.rs](https://github.com/swc-project/swc/blob/main/crates/swc_cli_impl/src/commands/plugin.rs) – Scaffolding logic forswc plugin new
Summary
- Scaffold new plugins using
swc plugin newto generate correct Cargo configuration and boilerplate insrc/lib.rs - Implement transforms using the
#[plugin_transform]macro, which handles Wasm host interop automatically - Compile to
wasm32-unknown-unknownorwasm32-wasip1using the provided cargo aliases - Cache occurs via
compile_wasm_pluginsincrates/swc/src/plugin.rs, storing modules inPLUGIN_MODULE_CACHE - Execute through the
Runtimetrait incrates/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →