Implementing a plugin
Important API Changes
Recent API changes that may affect your plugin development:
- Replace
chain!
macro with tuples: Use(
instead ofchain!(
. You can replace allchain!(
with(
using IDE features. - For
chain!
with 13+ arguments: Use nested tuples for items after the 13th element. - Replace
-> impl Fold
with-> impl Pass
in general. as_folder
is nowvisit_mut_pass
and returnsimpl VisitMut + Pass
instead ofimpl VisitMut + Fold
.- Use
Program.apply
andProgram.mutate
to apply Pass to program:fn apply(self, impl Pass) -> Self
fn mutate(&mut self, impl Pass)
- Replace
noop()
withnoop_pass()
. The new function lives inswc_ecma_ast
and is a real noop function.
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(),
|_| visit_mut_pass(TransformVisitor), // Note: Updated to use visit_mut_pass instead of as_folder
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| (tr(), properties(t, true)), // Note: Updated to use tuple syntax instead of chain!
&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