Публикация плагина в npm

Плагин SWC в экосистеме JavaScript представляет собой расширение трансформационного пайплайна компилятора, написанное на Rust и собранное в WebAssembly. При публикации в npm такой плагин становится распределяемым модулем, который может быть подключён через конфигурацию @swc/core и использован в сборках, аналогичных Babel-плагинам, но с иной моделью исполнения — без интерпретируемого AST-плагин API в JavaScript.

Ключевая особенность SWC-плагинов заключается в том, что их логика выполняется не в Node.js напрямую, а внутри WASM-рантайма SWC, что накладывает строгие требования к структуре пакета и способу его сборки.

Структура проекта npm-плагина

Типичный проект плагина содержит несколько обязательных слоёв:

  • Rust-библиотека с реализацией трансформации AST
  • WASM-таргет сборки
  • JavaScript-обёртка для интеграции с SWC
  • npm-метаданные и точки входа

Пример структуры:

swc-plugin-example/
├── Cargo.toml
├── src/
│   └── lib.rs
├── pkg/
│   ├── swc_plugin_bg.wasm
│   ├── swc_plugin.js
│   └── package.json
├── index.js
├── package.json
└── README.md

Папка pkg формируется автоматически инструментами сборки WASM и содержит артефакты, которые будут опубликованы в npm.

Подготовка Rust-окружения

SWC-плагин требует Rust toolchain с поддержкой wasm32-unknown-unknown:

rustup target add wasm32-unknown-unknown

Дополнительно используется набор макросов SWC:

[dependencies]
swc_core = { version = "0.x", features = ["ecma_plugin_transform"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Ключевая зависимость — swc_core, которая предоставляет AST-типы и инфраструктуру плагинов.

Реализация трансформации AST

Основная логика плагина реализуется через функцию трансформации модуля:

use swc_core::ecma::ast::*;
use swc_core::plugin::plugin_transform;


pub fn process_transform(program: Program) -> Program {
    match program {
        Program::Module(module) => {
            let transformed = Module {
                body: module.body,
                ..module
            };
            Program::Module(transformed)
        }
        other => other,
    }
}

Функция помечается макросом #[plugin_transform], который генерирует WASM-совместимый интерфейс.

Внутри этой функции можно:

  • изменять AST узлы
  • удалять или добавлять выражения
  • трансформировать импорты
  • оптимизировать код

Компиляция в WebAssembly

Сборка осуществляется через wasm-pack или cargo с целевым таргетом:

cargo build --release --target wasm32-unknown-unknown

Однако SWC требует дополнительной упаковки через @swc/plugin-wasm-pack или аналогичный pipeline.

Результатом становится директория pkg, содержащая:

  • .wasm бинарник
  • JS glue-код
  • сгенерированный package.json

JavaScript-обёртка для npm

Для интеграции с Node.js создаётся слой экспорта:

const plugin = require("./pkg/swc_plugin.js");

module.exports = plugin.default;

Этот файл становится точкой входа npm-пакета и проксирует WASM-реализацию.

Конфигурация package.json

Критически важная часть публикации — корректное описание пакета:

{
  "name": "swc-plugin-example",
  "version": "1.0.0",
  "main": "index.js",
  "files": [
    "pkg",
    "index.js"
  ],
  "keywords": [
    "swc",
    "plugin",
    "transform",
    "wasm"
  ],
  "license": "MIT",
  "type": "commonjs"
}

Важно включать только необходимые артефакты, иначе размер пакета резко увеличивается из-за WASM бинарника.

Подключение плагина в SWC конфигурации

После публикации в npm плагин подключается через конфигурацию SWC:

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

Второй аргумент массива представляет параметры плагина, сериализуемые в JSON и передаваемые в Rust-runtime.

Работа с параметрами плагина

SWC позволяет передавать конфигурацию в плагин через сериализацию:

Rust-сторона:

use serde::Deserialize;

#[derive(Deserialize)]
struct PluginOptions {
    enable_feature: bool,
}

Использование:

pub fn process_transform(program: Program, config: PluginOptions) -> Program {
    if config.enable_feature {
        // трансформация
    }
    program
}

JS-конфигурация:

["swc-plugin-example", { "enable_feature": true }]

Управление версиями и совместимостью

SWC-плагины чувствительны к версии @swc/core. Несовместимость ABI может приводить к падению загрузки WASM.

Практика версионирования:

  • фиксирование swc_core версии в Cargo.toml
  • синхронизация с peerDependency @swc/core
  • семантическое версионирование npm пакета
{
  "peerDependencies": {
    "@swc/core": "^1.3.0"
  }
}

Оптимизация размера npm-пакета

WASM-файлы могут занимать значительный объём, поэтому применяются техники оптимизации:

  • wasm-opt (Binaryen)
  • удаление debug symbols
  • включение lto = true в Cargo
[profile.release]
lto = true
opt-level = "z"

Дополнительно исключаются исходники Rust из npm публикации через .npmignore или поле files.

Публикация в npm registry

Перед публикацией требуется сборка WASM артефактов:

npm run build

Далее публикация:

npm publish --access public

Критически важно:

  • проверка наличия pkg/
  • отсутствие локальных путей
  • корректный main entry
  • наличие лицензии

CI/CD для автоматизации публикации

В автоматизированных пайплайнах обычно используется GitHub Actions:

name: publish

on:
  push:
    tags:
      - "v*"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable

      - name: Add WASM target
        run: rustup target add wasm32-unknown-unknown

      - name: Build
        run: cargo build --release --target wasm32-unknown-unknown

      - name: Publish
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Типичные ошибки при публикации

Несовпадение версий SWC

При обновлении @swc/core без пересборки WASM возникает ошибка загрузки плагина.

Отсутствие WASM файла в пакете

Если pkg не включён в files, npm публикует пустую или неработоспособную сборку.

Нарушение ABI

SWC ABI между Rust и JS строго фиксирован. Любое расхождение приводит к runtime panic.

Неправильная точка входа

Если main указывает не на JS-обёртку, а на WASM файл напрямую, загрузка плагина невозможна.

Интеграционные особенности SWC runtime

SWC загружает плагины через WASM-линковку, передавая:

  • AST сериализованный в бинарный формат
  • конфигурацию JSON
  • контекст трансформации

Плагин возвращает модифицированный AST, который далее проходит стадию генерации кода.

Модель исполнения строго функциональная: отсутствует доступ к Node.js API, файловой системе или сети, что делает плагины детерминированными и изолированными.

Паттерны проектирования плагинов

На практике используются несколько архитектурных подходов:

  • чистые трансформации AST без состояния
  • конфигурируемые оптимизаторы
  • наборы микро-плагинов, объединённых в один crate
  • условные трансформации на основе feature flags

Композиция логики предпочтительнее монолитных трансформеров, так как SWC выполняет плагины в ограниченном runtime.

Отладка WASM-плагинов

Отладка осуществляется через:

  • console_error_panic_hook
  • логирование через web_sys (ограниченно)
  • тестирование через swc_ecma_parser локально

Пример включения panic hook:

console_error_panic_hook::set_once();

Это позволяет получать стек вызовов при падении WASM.

Тестирование плагина перед публикацией

Тестирование выполняется через Rust unit tests:

#[test]
fn test_transform() {
    let input = "const a = 1;";
    let output = transform(input);
    assert_eq!(output.contains("const a"), true);
}

Также применяется snapshot testing AST:

  • входной код
  • сериализованный AST
  • сравнение результатов трансформации

Особенности публикации для monorepo

При использовании monorepo структура усложняется:

  • отдельный crate для плагина
  • отдельный npm пакет-обёртка
  • shared workspace dependencies

Важно избегать дублирования swc_core версий между пакетами.

Итоговая модель жизненного цикла плагина

Жизненный цикл SWC npm-плагина включает последовательность:

  1. разработка Rust трансформации
  2. сборка в WASM
  3. генерация JS glue-кода
  4. упаковка npm структуры
  5. публикация в registry
  6. подключение через SWC конфигурацию
  7. выполнение в WASM runtime при сборке проекта