Write your swc plugin with Rust

Posted at Fri Jan 27 2023

Hi, how are you? Hope you're all good and healthy!

So last time I'd shared about Migrating Babel to swc post, and as promised, I'll share how to write your own swc plugin with Rust language.

If you haven't read that post, I'd suggest to read that first before reading this one to understand the motivation.

Background

As we know, swc is a modern developer tools written in Rust. It offers a very fast compilation and minifier that can replace Babel and Terser.

If you previously used Babel and written/used some babel plugins/presets, then it can't be used anymore since the swc plugin must also be written in Rust and compiled to WASM format.

However, in practical, you might not need to write any swc plugin at all. All common and most used presets like env, react, typescript all already supported by default, and also third-party plugins like loadable, emotion, jest are already available to install and use.

In any case you have to write your own swc plugin, then you might find this post useful for you!

Rust knowledge

I'd strongly recommend you to learn Rust first, it'll be really helpful on developing the plugin.

If you don't have prior experience with Rust language, some part of the plugin code and development process might seems strange and unfamiliar to you.

But don't worry, I'll try my best to cover important point in the code as much as possible!

Let's get started, shall we?

Getting started

  1. You need to install Rust, follow Rust installation here

  2. You can start generate a new swc plugin project by running:

cargo install swc_cli
swc plugin new --target-type wasm32-wasi my-first-plugin
rustup target add wasm32-wasi

You can also clone my swc-plugin-starter if you prefer; added commands in a Makefile, prettier and GitHub Action there.

Plugin workflow

Just like in Babel, swc plugin can traverse all the code with specified visitor.

You'll first develop the plugin by implementing the swc visitor, and you might want to know what kind of visitor type you need.

You can use https://play.swc.rs/ and type your input there, and it can output the Abstract Syntax Tree (AST), so that you have some idea of what kind of visitor you want.

Suppose you want a plugin that replace certain variable name, then you need Identifier visitor.

Screen Shot 2023-01-27 at 7 43 24 PM

You'll also write a test to try out your plugin transform result in your code.

Finally, you will build the plugin and it will produce a *.wasm file, which will be included in your npm publish.

After that, your npm plugin can be installed and used by swc projects.

So in summary:

  • Develop plugin, implement visitors
  • Write the test
  • Build and compile to WASM
  • npm publish
  • Install and use on an swc project

Plugin "hello world"

Let's open the generated src/lib.rs, you will find:

pub struct TransformVisitor;
impl VisitMut for TransformVisitor {
  ...
}

This block is where we can implement visitor methods we want. You can browse all available visitors here.

Some points:

  • VisitMut here is a Rust's Trait, similiar with Interface; to abstract shared behavior
  • TransformVisitor is our own struct, you can name it however you wish
  • impl means we want to implement a Trait to a Struct

So for example, if we want to look over all JavaScript "Identifier" (as in AST), you can do:

fn visit_mut_ident(&mut self, n: &mut Ident) {
  n.visit_mut_children_with(self);

  println!("Ident: {}", n.sym.to_string());
}

Tips: The whole visit_mut_ident block can be autocompleted by VSCode, just install rust-analyzer extension!

and to try out the plugin, let's write a simple test on bottom of the file:

test!(
    Default::default(),
    |_| as_folder(TransformVisitor),
    simple_transform_global_var,
    r#"let isDev = __DEV__;"#,
    r#"let isDev = false;"#
);

Try run:

cargo test

and you will get output like:

----- Actual -----
Ident: isDev
Ident: __DEV__

For now, you'll get failed test result as expected, since we're only doing println for now. Now that we already get all the identifier, we can start to do some actual transformation.

Let's try to replace all global variable __DEV__ with false, here's how to do it:

fn visit_mut_ident(&mut self, n: &mut Ident) {
  n.visit_mut_children_with(self);

  if n.sym.to_string() == "__DEV__" {
    n.sym = JsWord::from("false");
  }
}

In above example:

  • we convert the sym to String first and compare againts string literal (&str) "DEV"
  • we reassigned sym with new value from JsWord since sym's type is JsWord.

Now try to run cargo test again, the test will PASS this time.

Congrats! the plugin is now ready to be build and shipped.

Publishing plugin

Unlike regular Rust library, for swc plugin, we don't need to publish to crates.io, instead, we will publish to npm as usual.

  • Make sure the plugin passed the test as you expect (run cargo test)
  • Let's run the build
	cargo build-wasi --release # build wasm32-wasi target binary
	cargo build-wasm32 --release # build wasm32-unknown-unknown target binary
  1. After succeed, you will see the compiled WASM file in:

target/wasm32-wasi/release/<your-plugin-name>.wasm

  1. Finally, you can do npm publish as usual and Done!

Using the plugin

You can use the plugin by installing the plugin's npm package on your swc-based project:

npm i -D my-swc-plugin

and list it on your swc config (.swcrc or swc-loader):

{
  "jsc": {
    "experimental": {
      "plugins": [
        ["my-swc-plugin", {}]
      ]
    }
  }
}

The {} object is the plugin options as needed

You can copy below snippet to try your new swc plugin quickly:

package.json

{
  "scripts": {
    "build": "swc ./file.js -o out.js"
  },
  "devDependencies": {
    "@swc/cli": "^0.1.59",
    "@swc/core": "^1.3.29"
  }
}

file.js

let dev = __DEV__;

Try to run:

npm run build

and see the content of out.js, it should output something like:

var dev = false;

//# sourceMappingURL=out.js.map

and that's all! Now we've confirmed that the plugin worked! Congrats.

One more thing, swc plugin can also be used on Next.js project via next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    swcPlugins: [["my-swc-plugin", {}]],
  },
};

Closing

I think that's all for today's post, I hope you find it useful and if you have feedback/questions, feel free to ping me on my social media! Thanks for reading this far, see you on another post 👋🏻.

Avatar of Antony

Antony Budianto

Software Engineering, Web, and some random life thoughts.