Skip to content
Docs
Plugin
ECMAScript
Getting started

Implementing a plugin

Setup environment

Install required toolchain

As plugin is written in the rust programming language and built as a .wasm file, you need to install rust toolchain and wasm target.

Install rust

You can follow instructions at 'Install Rust' page from the official rust website (opens in a new tab)

Add wasm target to rust

SWC supports two kinds of .wasm files. Those are

  • wasm32-wasi
  • wasm32-unknown-unknown

In this guide, we will use wasm-wasi as a target.

Install swc_cli

You can install a rust-based CLI for SWC by doing

cargo install swc_cli

Configuring IDE

If you are going to use vscode, it's recommended to install rust-analyzer extension. rust-analyzer is a language server (opens in a new tab) for the rust programming language, which provides good features for code completion, code navigation, and code analysis.

Implementing simple plugin

Create a project

SWC CLI supports creating a new plugin project.

Run

swc plugin new --target-type wasm32-wasi my-first-plugin
# You should to run this
rustup target add wasm32-wasi

to create a new plugin, and open my-first-plugin with your preferred rust IDE.

Implementing a visitor

The generated code has

impl VisitMut for TransformVisitor {
    // Implement necessary visit_mut_* methods for actual custom transform.
    // A comprehensive list of possible visitor methods can be found here:
    // https://rustdoc.swc.rs/swc_ecma_visit/trait.VisitMut.html
}

which is used to transform code. The trait VisitMut (opens in a new tab) supports mutating AST nodes, and as it supports all AST types, it has lots of methods.


We will use

foo === bar;

as the input. From the SWC Playground (opens in a new tab), you can get actual representation of this code.

{
  "type": "Module",
  "span": {
    "start": 0,
    "end": 12,
    "ctxt": 0
  },
  "body": [
    {
      "type": "ExpressionStatement",
      "span": {
        "start": 0,
        "end": 12,
        "ctxt": 0
      },
      "expression": {
        "type": "BinaryExpression",
        "span": {
          "start": 0,
          "end": 11,
          "ctxt": 0
        },
        "operator": "===",
        "left": {
          "type": "Identifier",
          "span": {
            "start": 0,
            "end": 3,
            "ctxt": 0
          },
          "value": "foo",
          "optional": false
        },
        "right": {
          "type": "Identifier",
          "span": {
            "start": 8,
            "end": 11,
            "ctxt": 0
          },
          "value": "bar",
          "optional": false
        }
      }
    }
  ],
  "interpreter": null
}

Let's implement a method for BinExpr. You can do it like

use swc_core::{
    ast::*,
    visit::{VisitMut, VisitMutWith},
};
 
impl VisitMut for TransformVisitor {
    fn visit_mut_bin_expr(&mut self, e: &mut BinExpr) {
        e.visit_mut_children_with(self);
    }
}

Note that visit_mut_children_with is required if you want to call the method handler for children. e.g. visit_mut_ident for foo and bar will be called by e.visit_mut_children_with(self); above.

Let's narrow down it using the binary operator.

use swc_core::{
    ast::*,
    visit::{VisitMut, VisitMutWith},
    common::Spanned,
};
 
impl VisitMut for TransformVisitor {
    fn visit_mut_bin_expr(&mut self, e: &mut BinExpr) {
        e.visit_mut_children_with(self);
 
        if e.op == op!("===") {
            e.left = Box::new(Ident::new_no_ctxt("kdy1".into(), e.left.span()).into());
        }
    }
}

op!("===") is a macro call, and it returns various types of operators. It returns BinaryOp (opens in a new tab) in this case, because we provided "===", which is a binary operator. See the rustdoc for op! macro (opens in a new tab) for more details.

If we run this plugin, we will get

kdy1 === bar;

Testing your transform

You can simply run cargo test to test your plugins. SWC also provides a utility to ease fixture testing.

You can easily verify the input and output of the transform.

test!(
    Default::default(),
    |_| as_folder(TransformVisitor),
    boo,
    r#"foo === bar;"#
);

Then, once you run UPDATE=1 cargo test, the snapshot will be updated.

You can take a look at the real fixture test for typescript type stripper (opens in a new tab).

#[testing::fixture("tests/fixture/**/input.ts")]
#[testing::fixture("tests/fixture/**/input.tsx")]
fn fixture(input: PathBuf) {
    let output = input.with_file_name("output.js");
    test_fixture(
        Syntax::Typescript(TsConfig {
            tsx: input.to_string_lossy().ends_with(".tsx"),
            ..Default::default()
        }),
        &|t| chain!(tr(), properties(t, true)),
        &input,
        &output,
    );
}
 

Things to note:

  • The glob provided to testing::fixture is relative to the cargo project directory.
  • The output file is output.js, and it's stored in a same directory as the input file.
  • test_fixture drives the test.
  • You can determine the syntax of the input file by passing the syntax to test_fixture.
  • You then provide your visitor implementation as the second argument to test_fixture.
  • Then you provide the input file path and the output file path.

Logging

SWC uses tracing for logging. By default, SWC testing library configures the log level to debug by default, and this can be controlled by using an environment variable named RUST_LOG. e.g. RUST_LOG=trace cargo test will print all logs, including trace logs.

If you want, you can remove logging for your plugin by using cargo features of tracing. See the documentation for it (opens in a new tab).

Publishing your plugin

Please see plugin publishing guide