Skip to content

Announcing WebAssembly Plugins for SWC

February 13th, 2022 by DongYoon KangLee Robinson

SWC is an extensible Rust-based platform for the next generation of fast and extensible developer tools. To enable developers to further extend the platform, we're announcing a new WebAssembly plugin interface to write high-performance plugins.

Why WebAssembly?

SWC currently has a JavaScript plugin interface, but it has a few tradeoffs. These JavaScript plugins are more difficult to integrate with Rust code, and can often lead to performance bottlenecks.

When designing our new plugin interface, we oulined a few goals:

  1. Plugin binaries should be able to be loaded into SWC's native binary without using the JavaScript binding interop.
  2. Provide excellent performance while loading and running custom transformations.
  3. Ensure API stability to allow plugin binary to work across different versions of SWC.
  4. Allow developers to write plugins without needing to worry about native platform target binaries.

Options Evaluated

With these goals defined, we evaluated four different options:

  • JavaScript-based plugins, including Babel plugins
  • Plugins based on abi_stable and native dynamic libraries
  • WASM plugins based on wasmtime
  • WASM plugins based on wasmer

After evaluating options, we choose to use WASM plugins based on wasmer. You can view the full pros / cons list here.

Benchmarks

Performance vs. JavaScript Plugins

TODO

Performance vs. Initial Protoype

TODO

Create your first WASM plugin

You can create your first WebAssembly plugin for SWC using the CLI.

First, install the CLI package through cargo:

$ cargo install swc_cli

We've provided helpful code generation to scaffold a new project for creating plugins. Create a new plugin project using the SWC CLI:

$ swc plugin new my-plugin --target-type=wasm32-wasi

Here's an example plugin that replaces console.log(${text}) with console.log('changed_via_plugin'):

use swc_plugin::{ast::*, errors::HANDLER, plugin_transform, syntax_pos::DUMMY_SP};

struct ConsoleOutputReplacer;

/// An example plugin replaces any `console.log(${text})` into
/// `console.log('changed_via_plugin')`.
impl VisitMut for ConsoleOutputReplacer {
    fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
        if let Callee::Expr(expr) = &call.callee {
            if let Expr::Member(MemberExpr { obj, .. }) = &**expr {
                if let Expr::Ident(ident) = &**obj {
                    if ident.sym == *"console" {
                        call.args[0].expr = Box::new(Expr::Lit(Lit::Str(Str {
                            span: DUMMY_SP,
                            has_escape: false,
                            kind: StrKind::default(),
                            value: JsWord::from("changed_via_plugin"),
                        })));
                    }
                }
            }
        }
    }
}

Learn more

WASM Plugins are currently experimental, as we expect we'll have to iterate on the API and address bug fixes. If you have suggestions or bug reports, please comment on the discussion.

Check out the documentation for more information.