Пишем плагин к SWC на Rust

• build, rust

Введение

В недавнем посте про Babel обсуждали тему трансформации кода и какие инструменты для этого есть. Один из путей – кастомный лпагин к SWC.

Конечно, на эту тему есть официальная документация. Однако во-первых, документация подустарела и требует исправления, а во-вторых, хотелось показать, что всё не так сложно, как может показаться. Поехали?

Опишем задачу

Задачу выбрал выдуманную, тривиальную, но вполне реальную.

Часто бывает так, что в коде могут содержаться отладочные вызовы, параметры и так далее. В случае отдельной функции удалить это из бандла тривиально (должно хватить просто разметки функции, как “чистой”), а вот в случае параметров всё немного сложнее. Рассмотрим код:

run('smth', 'Секретная отладка');

Подобных вызовов в коде может быть порядочно, информация – не очень публичной, поэтому вполне резонно может появиться желание удалить это в процессе продовой сборки.

Стартуем создание плагина

Я буду корректировать доку. Информация актуальна на момент написания.

Начинаем с установки Rust. На сайте есть инструкция, лучше следовать ей.

Дальше устанавливаем swc_cli и добавляем новый таргет для сборки:

cargo install swc_cli
rustup target add wasm32-wasip1

Ставим @swc/cli и @swc/core. На всякий случай приведу версии, которые были у меня:

@swc/cli@0.8.1
@swc/core@1.15.43

Затем зовём swcx и создаём плагин:

npx swcx plugin new --target-type wasm32-wasip1 plugin-dir

Это не swc. а swcx – новая консольная тулза, сейчас в бете

Дальнейшие манипуляции будут в plugin-dir/src/lib.rs.

Правим код на Rust

В эпоху всевозможных ии и агентов сделать плагин с простыми манипуляциями будет не сильно сложно:

// Импорты
use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};
use swc_core::ecma::{
    ast::*,
    visit::{visit_mut_pass, VisitMut, VisitMutWith},
    transforms::testing::test,
};
 
pub struct TransformVisitor;
 
// Реализация обхода элементов
impl VisitMut for TransformVisitor {
    // Обход вызовов
    fn visit_mut_call_expr(&mut self, e: &mut CallExpr) {
        e.visit_mut_children_with(self);
 
        // Проверяем, что это нужная нам функция
        let is_run_call = match &e.callee {
            Callee::Expr(callee_expr) => match &**callee_expr {
                Expr::Ident(ident) => ident.sym == *"run",
                _ => false,
            },
            _ => false,
        };
 
        if is_run_call {
            if e.args.len() > 1 {
                // Удаляем второй аргумент
                e.args.truncate(1);
            }
        }
    }
}
 
// Входная точка плагина
#[plugin_transform]
pub fn process_transform(mut program: Program, _metadata: TransformPluginProgramMetadata) -> Program {
    program.visit_mut_with(&mut TransformVisitor);
    program
}
 
// Минимальный тест со снепшотом
test!(
    Default::default(),
    |_| visit_mut_pass(TransformVisitor),
    boo,
    r#"run(a, b);"#
);

Я не являюсь хоть сколько-нибудь знающим разработчиком на Rust, это просто минимальный рабочий пример.

Собираем и используем плагин

Собираем с помощью команды:

cargo build-wasip1 --release

Плагин соберётся по пути plugin-dir/target/wasm32-wasip1/release/plugin-dir.wasm. Путь зависит от настроек, которые вы указывали ранее.

Использование достаточно простое (и для простоты же используется синхронный transformSync), но почему-то в документации этого тоже нет:

// test.mjs
import { transformSync } from '@swc/core';
 
const transformCode = (code) => {
    const result = transformSync(code, {
        jsc: {
            experimental: {
                plugins: [
                    ['<путь до собранного wasm файла>', {}]
                ]
            }
        }
    });
 
    return result.code;
};
 
console.log(transformCode('run(a, b)'));
// -> run(a);

Итого

У меня нет хорошего бенчмарка – насколько данный подход быстрее того же самого Babel. По всей видимости, при каждом вызове загрузка плагина происходит заново? Но я подумаю, что с этим можно сделать :)

Цель поста – показать, что подобный код заводится без особых усилий и осиливается за 10 минут. Возможно, я кому-то подскажу и сэкономлю пару минут в процессе.

Да, добавляется сложность со сборкой и доставкой плагина, в этом плане путь на JavaScript проще. А в промышленных “Enterprise”-проектах отпугнёт секция “experimental” из примера. Получается, всё это – на свой страх и риск?

В каком-то смысле, но нужно же с чего-то начинать) А кто-то скажет, что скорость стоит того.

Обсудить в Telegram

Почитать ещё посты

  • Вышел Babel 8. А нужен ли он?
  • Автоматические размеры полей с field-sizing
  • text-fit: свойство, которое дождались
  • Декораторы отступов, или свойства, которые у нас уже давно были
  • Ищем баланс с помощью flex-wrap: balance