esbuild и PostCSS через плагины

PostCSS представляет собой инструмент для преобразования CSS-кода при помощи набора подключаемых модулей. В отличие от препроцессоров, таких как Sass или Less, PostCSS не навязывает собственный синтаксис и работает непосредственно с обычным CSS, выполняя анализ дерева стилей и его последующую модификацию.

В современных проектах PostCSS чаще всего используется для следующих задач:

  • автоматическое добавление вендорных префиксов;
  • поддержка будущих возможностей CSS;
  • оптимизация и минификация стилей;
  • преобразование CSS-переменных;
  • управление вложенностью правил;
  • удаление неиспользуемых стилей;
  • генерация адаптивных единиц измерения;
  • работа с Tailwind CSS.

Сама библиотека Esbuild не содержит встроенной поддержки PostCSS, однако благодаря системе плагинов оба инструмента легко интегрируются в единый процесс сборки.


Архитектура взаимодействия Esbuild и PostCSS

При обработке CSS через PostCSS обычно выполняется следующая последовательность:

  1. Esbuild обнаруживает CSS-файл.
  2. Плагин перехватывает загрузку файла.
  3. Содержимое передается в PostCSS.
  4. PostCSS запускает набор подключенных модулей.
  5. Возвращается преобразованный CSS.
  6. Esbuild продолжает сборку уже с обработанным результатом.

Упрощённая схема выглядит следующим образом:

styles.css
     │
     ▼
  Esbuild
     │
     ▼
 PostCSS
     │
     ├── Autoprefixer
     ├── cssnano
     ├── postcss-nested
     └── другие плагины
     │
     ▼
Обработанный CSS
     │
     ▼
 Финальный bundle

Установка зависимостей

Для базовой интеграции потребуется установить Esbuild и PostCSS:

npm install -D esbuild postcss

Дополнительно устанавливаются необходимые плагины PostCSS.

Например:

npm install -D autoprefixer

или

npm install -D cssnano

или

npm install -D postcss-nested

Создание собственного PostCSS-плагина для Esbuild

Наиболее гибкий подход заключается в написании собственного плагина Esbuild.

Структура проекта:

project/
├── src/
│   ├── index.js
│   └── styles.css
├── build.js
└── package.json

Файл сборки:

const esbuild = require('esbuild');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');

const postcssPlugin = {
    name: 'postcss',
    setup(build) {

        build.onLoad({ filter: /\.css$/ }, async (args) => {

            const fs = require('fs/promises');

            const source = await fs.readFile(
                args.path,
                'utf8'
            );

            const result = await postcss([
                autoprefixer()
            ]).process(source, {
                from: args.path
            });

            return {
                contents: result.css,
                loader: 'css'
            };
        });
    }
};

esbuild.build({
    entryPoints: ['src/index.js'],
    bundle: true,
    outfile: 'dist/app.js',
    plugins: [postcssPlugin]
});

В данном примере:

  • onLoad() перехватывает все CSS-файлы;
  • содержимое файла читается вручную;
  • PostCSS обрабатывает код;
  • Esbuild получает уже преобразованный CSS.

Использование конфигурационного файла PostCSS

В крупных проектах список плагинов может быть довольно большим. Хранить его непосредственно внутри сборочного скрипта неудобно.

Обычно создаётся отдельный файл конфигурации.

postcss.config.js

module.exports = {
    plugins: [
        require('autoprefixer'),
        require('postcss-nested')
    ]
};

После этого конфигурация может загружаться автоматически.

Пример:

const postcss = require('postcss');
const postcssConfig = require('./postcss.config');

const result = await postcss(
    postcssConfig.plugins
).process(css, {
    from: args.path
});

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


Автоматическое добавление вендорных префиксов

Одним из наиболее распространённых PostCSS-плагинов является Autoprefixer.

Исходный код:

.card {
    display: flex;
    user-select: none;
}

После обработки:

.card {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-user-select: none;
    user-select: none;
}

Настройка выполняется через файл Browserslist.

package.json

{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

Autoprefixer анализирует целевые браузеры и автоматически добавляет только необходимые префиксы.


Поддержка вложенных правил через postcss-nested

Обычный CSS не поддерживает вложенность так, как это реализовано в Sass.

Плагин postcss-nested добавляет подобную возможность.

Исходный код:

.card {
    padding: 20px;

    .title {
        font-size: 24px;
    }

    &:hover {
        background: #eee;
    }
}

После преобразования:

.card {
    padding: 20px;
}

.card .title {
    font-size: 24px;
}

.card:hover {
    background: #eee;
}

Настройка:

module.exports = {
    plugins: [
        require('postcss-nested')
    ]
};

Использование cssnano для минификации

Во время production-сборки CSS обычно подвергается дополнительной оптимизации.

Для этого часто используется cssnano.

Исходный код:

.button {
    margin: 10px 10px 10px 10px;
    color: #ffffff;
}

После обработки:

.button{margin:10px;color:#fff}

Подключение:

const cssnano = require('cssnano');

postcss([
    cssnano()
]);

Разделение конфигурации для разработки и production

Практически всегда набор PostCSS-плагинов зависит от режима сборки.

Пример:

const isProduction =
    process.env.NODE_ENV === 'production';

Формирование списка плагинов:

const plugins = [
    require('postcss-nested'),
    require('autoprefixer')
];

if (isProduction) {
    plugins.push(
        require('cssnano')
    );
}

Далее:

await postcss(plugins).process(css, {
    from: file
});

Такой подход позволяет сохранить читаемость CSS во время разработки и получить максимально компактный результат в production.


Работа с CSS-переменными

Плагин postcss-custom-properties позволяет преобразовывать пользовательские свойства CSS.

Исходный код:

:root {
    --primary: #2196f3;
}

.button {
    color: var(--primary);
}

После обработки:

.button {
    color: #2196f3;
}

Подключение:

module.exports = {
    plugins: [
        require('postcss-custom-properties')
    ]
};

Подобное преобразование полезно для поддержки старых браузеров.


Использование современных возможностей CSS

Плагин postcss-preset-env предоставляет доступ к функциональности будущих версий CSS.

Подключение:

module.exports = {
    plugins: [
        require('postcss-preset-env')({
            stage: 1
        })
    ]
};

Пример использования:

:root {
    --main-color: #1976d2;
}

.card {
    color: color-mod(
        var(--main-color)
        alpha(90%)
    );
}

PostCSS выполнит преобразование в совместимый CSS-код.


Интеграция Tailwind CSS через Esbuild и PostCSS

Tailwind CSS использует PostCSS как один из основных механизмов обработки.

Установка:

npm install -D tailwindcss postcss

Конфигурация:

module.exports = {
    plugins: [
        require('tailwindcss'),
        require('autoprefixer')
    ]
};

Файл стилей:

@tailwind base;
@tailwind components;
@tailwind utilities;

Во время сборки PostCSS генерирует итоговый CSS на основе используемых классов.


Обработка Source Maps

При использовании PostCSS совместно с Esbuild желательно сохранять карты исходников.

Пример:

const result = await postcss(plugins)
    .process(css, {
        from: args.path,
        map: {
            inline: false
        }
    });

Далее карта может быть возвращена Esbuild:

return {
    contents: result.css,
    loader: 'css'
};

Source Maps позволяют связывать итоговый CSS с исходными файлами и значительно упрощают отладку.


Кэширование результатов PostCSS

При больших объёмах CSS повторная обработка файлов может занимать заметное время.

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

const cache = new Map();

Перед обработкой:

if (cache.has(args.path)) {
    return cache.get(args.path);
}

После обработки:

cache.set(args.path, result);

Такой механизм особенно полезен при использовании режима наблюдения (watch).


Поддержка режима Watch

Esbuild предоставляет встроенное наблюдение за изменениями файлов.

Пример:

const ctx = await esbuild.context({
    entryPoints: ['src/index.js'],
    bundle: true,
    outfile: 'dist/app.js',
    plugins: [postcssPlugin]
});

await ctx.watch();

После изменения CSS-файла:

  1. Esbuild обнаруживает изменение.
  2. Плагин повторно загружает файл.
  3. PostCSS запускает обработку.
  4. Выполняется новая сборка.

В результате процесс разработки становится практически мгновенным.


Обработка нескольких типов стилей

Иногда требуется запускать PostCSS не только для .css, но и для других расширений.

Например:

build.onLoad({
    filter: /\.(css|pcss)$/
}, async (args) => {

});

Либо:

build.onLoad({
    filter: /\.(css|scss)$/
}, async (args) => {

});

В подобных сценариях PostCSS может выступать дополнительным этапом после Sass-компиляции или других преобразований.


Использование готовых плагинов интеграции

Вместо написания собственного решения можно воспользоваться существующими пакетами.

Наиболее распространённая схема:

npm install -D esbuild-plugin-postcss2

Подключение:

const postcssPlugin =
    require('esbuild-plugin-postcss2');

Конфигурация:

esbuild.build({
    entryPoints: ['src/index.js'],
    bundle: true,
    plugins: [
        postcssPlugin()
    ]
});

Подобные плагины автоматически:

  • находят postcss.config.js;
  • подключают указанные модули;
  • обрабатывают CSS;
  • интегрируются с watch-режимом;
  • поддерживают source maps.

Особенности производительности

Одним из ключевых преимуществ Esbuild является чрезвычайно высокая скорость работы благодаря реализации на Go.

Однако скорость обработки CSS во многом начинает зависеть именно от PostCSS и набора используемых модулей.

Наиболее затратными обычно являются:

  • Tailwind CSS;
  • cssnano;
  • сложные пользовательские плагины;
  • анализ больших таблиц совместимости браузеров.

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

  • подключать только действительно необходимые плагины;
  • запускать минификацию исключительно в production;
  • использовать кэширование;
  • избегать дублирующих преобразований;
  • разделять тяжёлые этапы обработки между режимами разработки и публикации.

Порядок выполнения PostCSS-плагинов

Плагины выполняются последовательно в том порядке, в котором перечислены в конфигурации.

Пример:

module.exports = {
    plugins: [
        require('postcss-nested'),
        require('autoprefixer'),
        require('cssnano')
    ]
};

Последовательность обработки:

  1. Разворачивается вложенность.
  2. Добавляются вендорные префиксы.
  3. Выполняется минификация.

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


Типичный production-конвейер

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

module.exports = {
    plugins: [
        require('postcss-nested'),
        require('postcss-preset-env'),
        require('autoprefixer'),
        require('cssnano')
    ]
};

Этапы обработки:

  1. Преобразование вложенности.
  2. Поддержка современных возможностей CSS.
  3. Добавление префиксов.
  4. Финальная оптимизация и минификация.

В сочетании с высокой скоростью Esbuild такой конвейер обеспечивает быстрое создание production-сборок и позволяет использовать практически весь современный инструментарий обработки CSS через гибкую экосистему PostCSS-плагинов.