Как сделать один плагин сразу для всех сборщиков фронтенда?

Здравствуйте, товарищи! Сегодня мы снова поговорим про тулинг для фронтенда. В этот раз обсудим разработку плагинов для сборщиков, таких как: Webpack, Vite, esbuild и подобных. За основу мы возьмем Unplugin.

Короткий ответ на вопрос из заголовка: пока никак. Чуть более длинный — изображен на обложке. А в качестве полноценного ответа, предлагаю вам эту статью. Попробуем хотя бы приблизиться к заявленному результату.

Сразу стоит уточнить: это не туториал и не пересказ документации, а скорее case-study.

С чего все началось

Я делаю open source проект — mlut. Это инструмент для верстки с подходом Atomic CSS. Что-то похожее на Tailwind, но по некоторым параметрам mlut превосходит все существующие аналоги. Если хотите узнать об инструменте подробнее, то рекомендую мою предыдущую статью. Вкратце напомню основную схему работы инструмента:

  1. Пишем в HTML/JSX/etc разметке атомарные CSS-классы

  2. JIT-движок смотрит наш код и генерирует CSS, на основе этих классов

Реализовав JIT-движок, я сразу же сделал CLI — это база, без которой инструмент нельзя было бы просто поставить и запустить. Но кроме CLI, второй частый кейс использования (а возможно и первый по частоте) — подключение в процесс сборки проекта. Это значит, что нужно как-то внедряться в сборку и запускать наш инструмент.

Для подобных целей, в большинстве сборщиках можно создавать плагины. В случае работы со сборщиком, мне нужна была часть, которая заменяла бы то, что делал CLI. А именно:

  • Принимать опции работы JIT: input/output файлы, минификация и т.д. В mlut почти все эти опции можно передавать прямо в Sass-конфиге. Но считывает их оттуда тоже CLI)

  • Находить файлы с контентом: в CLI мы их указываем явно. В случае со сборкой задачу чуть проще, поскольку нам сразу будут доступны файлы, из которых собирается бандл

  • Следить за изменениями: тут тоже просто — сборщики это умеют из коробки

  • Выполнять основную логику: закидывать файлы в JIT-движок и писать скомпилированный CSS в файл

  • Процессить итоговый CSS: минифицировать и добавлять вендорные префиксы по необходимости. Это также добавляется отдельно теми, кто настраивает сборку

Поиск решения

Со сборщиками мой опыт был скромный, поскольку с SPA я в принципе работал мало. Тем более, я плохо представлял, как вообще делать для них плагины. Поэтому первым делом я пошел смотреть: как подобное реализовали «конкурент»)

Хорошие художники копируют, великие — воруют © Бэтмен

  • Tailwind — тут все хитро: он интегрируется через PostCSS, поскольку сам Tailwind v3 — просто большой PostCSS плагин)

  • UnoCSS — с Vite он работает by design, а для остальных случаев есть отдельные пакеты. Для Webpack, например

  • Atomizer — здесь я обнаружил кое-что интересное. Часть интеграций были написаны через Unplugin! Погуглив, я вспомнил, как однажды на него натыкался, поскольку был немного знаком с экосистемой unjs

Дополнение

Если ранее не слышали про unjs, то рекомендую ознакомиться. Ребята делают достойные framework-agnostic инструменты. Среди участников Anthony Fu и кое-кто из команды Nuxt

Мне хотелось упростить себе задачу и реиспользовать один и тот же код для нескольких интеграций. Тем более, у меня не было опыта с плагинами и хотелось начать с чего-то попроще. Так Unplugin стал идеальным решением.

Unplugin

По заявлению авторов — это унифицированная система плагинов для разных сборщиков. Пишешь нужную логику и просто экспортируешь плагины для желаемых сборщиков:

import { createUnplugin } from 'unplugin'

export const unplugin = createUnplugin((options) => ({
  name: 'unplugin-starter',

  transformInclude(id) {
    return id.endsWith('main.ts')
  },

  transform(code) {
    return code.replace(/