Решил я тут на днях попробовать соорудить что нибудь на wasm, поскольку ранее начитался про него и выбрал Rust. Это рассказ про то как я затащил wasm на фронтенд без боли.
В чём заключается упомянутая боль?
Я люблю когда в моём проекте чисто, каждая директория за что-то обязательно несёт ответственность. Я не хочу тянуть всякие lerna
в проект и прочие штуки из которых мне может понадобится всего одна фича. Возможно то что я описал для некоторых читателей и не проблема вовсе.
Излишний конвейер
Чтобы прийти к возможности просто "импортировать" rust нужно преодолеть много разных препятствий. К счастью это уже решено за нас и нужно лишь использовать.
Соберите ваш крейт под wasm32 используя
cargo build --target wasm32-unknown-unknown
Сгенерируйте JS и объявления типов TS при помощи wasm-bindgen
Оптимизируйте 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 в корне проекта.
Фетчер
Получит путь к крейту из резольвера и соберёт его в кэш. Тут поподробней опишу действия
Создаётся временная директория для артефактов сборки (yarn предгает xfs, с возможностью создать временную директорию нативно из yarn).
Запускается
cargo build
с флагом--out-dir
чтобы направить артефакты во временную директорию.Выполняется
bindgen
, чтобы создать типы .d.ts и привязки js.Опционально запускается оптимизатор
wasm-opt
из binaryen.Результат упаковывается в tgz и отправляется в кэш.
Временная директория удаляется.
Команда (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