Решил я тут на днях попробовать соорудить что нибудь на wasm, поскольку ранее начитался про него и выбрал Rust. Это рассказ про то как я затащил wasm на фронтенд без боли.

В чём заключается упомянутая боль?

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

Излишний конвейер

Чтобы прийти к возможности просто "импортировать" rust нужно преодолеть много разных препятствий. К счастью это уже решено за нас и нужно лишь использовать.

  1. Соберите ваш крейт под wasm32 используя cargo build --target wasm32-unknown-unknown

  2. Сгенерируйте JS и объявления типов TS при помощи wasm-bindgen

  3. Оптимизируйте wasm для продакшена если это требуется используя binaryen

Это инструменты с которыми я знаком, их вероятно больше. Как бы вы разместили их у себя в проекте? Вам нужно скачать каждый инструмент локально, указать мусорную dist папку для хранения артефактов сборки и произвести всю магию по очереди. Почему я должен думать об этом. Похоже создатели wasm-pack тоже так подумали.

wasm-pack и мусорный workspace

Отличный инструмент, он сделает всё выше перечисленное одной командой. Но увы мне он не понравился, вам всё ещё нужно требовать его установку от других, объявить скрипт сборки в вашем package.json. Одной из проблем wasm-pack является то, что он создаёт готовый к публикации npm пакет, требуя readme, лицензию и прочую лишнюю (если вы не собираетесь публиковать) информацию. Чтобы лучше описать проблему, я распишу структуру проекта

/my-project
--/Cargo.toml (cargo workspaces root)
--/package.json (yarn workspaces root)
--/app (приложение)
----/..
----/package.json
--/super-crate (крейт wasm)
----/Cargo.toml
----/..

И куда вы дадите wasm-pack собрать ваш крейт? dist в super-crate? Хорошо, если вы публикуете пакет, но точно не когда вы собираетесь им постоянно пользоваться и пересобирать. Так что у вас всегда будет мусорный dist-workspace для собранных артефактов. И прежде чем смириться с этим я вспомнил...

У меня есть Yarn

А у вас?). Yarn это не только отличный пакетный менеджер nodejs, но и гибкий инструмент управления большим проектом. Если вы не знаете о нём, то рекомендую ознакомиться с этим инструментом.

Yarn использует стратегию PnP для разрешения зависимостей и не использует node_modules, а вместо них использует кэш, который может быть расположен глобально или у вас в проекте. К тому же вы можете коммитить кэш в удалённый репозиторий, чтобы ускорить установку пакетов в CI пайплайне.

Эта информация пригодится чуть позже, самое главное - yarn поддерживает плагины, а значит его функционал можно расширить. Можно было бы упаковать условный "wasm-pack" в такой плагин, а результат сборки хранить в кэше минуя dist.

Расскажу поверхностно, потому что yarn плагины потянут на целую статью

О плагинах

Прежде чем раскрыть магию я должен рассказть о том что вы можете расширять плагинами. Я привёл самое основное, что пригодится

  • Команда (Command) - позволяет расширить yarn добавлением новых команд.

  • Резольвер (Resolver) - расширяет механизм разрешения зависимостей. Именно резольвер преобразует "react": "^18.2.0" в вашем package.json в конкретное дерево зависимостей в yarn.lock

  • Фетчер (Fetcher) - отвечает за загрузку пакета из удалённого репозитория или локальной директории. В случае с yarn должен упаковать пакет в tgz архив для хранения в кэше.

Более подробно можно почитать на сайте yarn.

Роль каждого в решении проблемы

Как же эти три термина помогут решить проблему?

Резольвер

Прочитает package.json, найдёт нужный крейт в проекте и свяжет путь к нему с дескриптором

{
  "name": "app",
  "main": "src/index.ts",
  "dependencies": {
    "@crate/super-crate": "crate:*"
  }
}

В данном случае имя крейта извлекается из дескриптора - "super-crate". Cкоуп @crate я ввел для семантики, чтобы отличать обычные пакеты от крейтов.

На выходе получается путь к целевому крейту, если он указан как member в Cargo.toml в корне проекта.

Фетчер

Получит путь к крейту из резольвера и соберёт его в кэш. Тут поподробней опишу действия

  1. Создаётся временная директория для артефактов сборки (yarn предгает xfs, с возможностью создать временную директорию нативно из yarn).

  2. Запускается cargo build с флагом --out-dir чтобы направить артефакты во временную директорию.

  3. Выполняется bindgen, чтобы создать типы .d.ts и привязки js.

  4. Опционально запускается оптимизатор wasm-opt из binaryen.

  5. Результат упаковывается в tgz и отправляется в кэш.

  6. Временная директория удаляется.

Команда (rebuild)

Всё выше описанное происходит при запуске yarn install команды и только один раз. Чтобы пересобрать крейт, нужно просто пометить кэш как не актуальный и запустить yarn install снова. У меня это делает команда yarn repack rebuild имя_крейта*. Если не указать крейт, то будут пересобраны все.

*repack - это название плагина

Команда (install)

yarn repack install загрузит бинарники wasm-bindgen и binaryen в проект. Это требуется только при инициализации проекта или обновлении плагина, т.к. эти бинарники тоже можно коммитить.

Результат

Всё что мне теперь осталось сделать, это запустить yarn install и импортировать "крейт". Вау, а ведь это просто зависимость в package.json. И мне не приходиться беспокоиться о мусоре в моём проекте, а если мне понадобиться собрать крейт заново, я просто запущу yarn repack rebuild super-crate. К тому же результат сборки кэшируется, а значит я могу использовать его без проблем в CI без пересборки.

import init, { hello_world } from '@crate/super-crate'

await init()

console.log(hello_world())

Для тех кто дочитал

Примеры использования и сам плагин на github (он на стадии proof of concept, так что я расчитываю на фидбэк): https://github.com/LIMPIX31/plugin-repack

Комментарии (0)