@import и разрешение путей в CSS

CSS-директива @import в контексте esbuild рассматривается не как строковая вставка, а как часть графа зависимостей, который строится аналогично JavaScript-модулям. При включённом бандлинге esbuild анализирует каждый CSS-файл, извлекает импортируемые ресурсы и формирует единый связанный пакет стилей.

Механика разрешения @import

При встрече конструкции вида:

@import "./reset.css";
@import "normalize.css";

esbuild выполняет несколько последовательных шагов:

  1. Определение типа пути

    • относительный (./, ../, ./styles/...)
    • абсолютный (редко в CSS контексте)
    • пакетный (из node_modules)
  2. Разрешение относительно файла-импортёра

    • базовая точка отсчёта — директория текущего CSS-файла
    • путь нормализуется до абсолютного внутреннего представления
  3. Поиск модуля

    • для относительных путей: прямое сопоставление с файловой системой
    • для пакетных: поиск через алгоритм Node.js resolution (включая node_modules)
  4. Определение расширения

    • если расширение не указано, пробуются варианты (.css, а также другие подключённые загрузчики при расширениях через плагины)

Встраивание импортов в бандл

При включённой опции bundle: true CSS-импорты не остаются в итоговом файле. Вместо этого происходит их рекурсивная инлайн-обработка.

Пример:

/* main.css */
@import "./base.css";

body {
  margin: 0;
}
/* base.css */
html {
  font-size: 16px;
}

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

html {
  font-size: 16px;
}

body {
  margin: 0;
}

Порядок вставки сохраняется согласно графу зависимостей: импортируемые стили всегда оказываются выше по цепочке, если они объявлены раньше в дереве зависимостей.


Разрешение путей и относительные зависимости

esbuild строго привязывает разрешение @import к местоположению файла-источника.

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

styles/
  main.css
  shared/
    reset.css
/* main.css */
@import "./shared/reset.css";

Разрешение происходит следующим образом:

  • берётся абсолютный путь к main.css
  • вычисляется директория styles/
  • добавляется ./shared/reset.css
  • итог: styles/shared/reset.css

Особенность: если файл перемещается, вся цепочка импортов автоматически переопределяется без дополнительных настроек.


Импорт из node_modules

Если путь не начинается с ./ или ../, esbuild трактует его как пакетный импорт:

@import "modern-normalize";

Алгоритм разрешения:

  1. поиск пакета в node_modules ближайшего уровня
  2. чтение package.json
  3. выбор entry-point (обычно поле style, main или файловая карта экспорта)
  4. переход к CSS-файлу или совместимому ресурсу

Если пакет экспортирует несколько CSS-уровней, esbuild выбирает наиболее прямой путь без промежуточных JS-обёрток.


Влияние external и исключений из бандла

Если модуль помечен как внешний, например:

external: ["normalize.css"]

то соответствующий @import не будет инлайнен и останется в итоговом CSS:

@import "normalize.css";

Это используется в сценариях:

  • CDN-стили
  • разделение поставки библиотек
  • интеграция с runtime-окружениями, где CSS подгружается отдельно

Обработка @import с media-условиями

CSS допускает расширенную форму:

@import "./print.css" print;
@import "./dark.css" (prefers-color-scheme: dark);

esbuild сохраняет семантику медиа-условий, перенося их на уровень объединённого файла:

  • содержимое импортируемого файла вставляется
  • медиа-условие остаётся обёрткой

Результат после сборки:

@media print {
  /* содержимое print.css */
}

@media (prefers-color-scheme: dark) {
  /* содержимое dark.css */
}

Разрешение url() внутри импортируемых CSS

Хотя @import и url() различаются по семантике, в esbuild они проходят через единый механизм резолвинга путей.

.icon {
  background-image: url("./icons/edit.svg");
}

Правила:

  • относительный путь вычисляется от текущего CSS-файла

  • файл попадает в граф ассетов

  • при бандлинге может быть:

    • встроен как data URL
    • вынесен в отдельный файл в outdir
    • переименован с хешированием (при соответствующих настройках плагинов)

Порядок выполнения и граф зависимостей

esbuild строит ориентированный ациклический граф:

  • узлы — CSS-файлы
  • рёбра — @import зависимости

Обход графа выполняется в глубину:

  1. сначала обрабатываются конечные зависимости
  2. затем родительские узлы
  3. финальная сборка идёт в обратном порядке обхода

Это гарантирует корректный порядок каскада CSS.


Дедупликация импортов

Если один и тот же CSS-файл импортируется несколько раз по разным путям:

@import "./reset.css";
@import "../shared/reset.css";

esbuild:

  • сравнивает нормализованные абсолютные пути
  • обнаруживает совпадение
  • включает файл только один раз в граф сборки

Это предотвращает:

  • дублирование правил
  • повторное применение стилей
  • неконтролируемое увеличение размера бандла

Поведение при циклических импортных зависимостях

CSS теоретически допускает циклы:

a.css → b.css → a.css

esbuild:

  • отслеживает уже посещённые узлы графа
  • при повторном входе в файл прекращает рекурсивную обработку
  • предотвращает бесконечную петлю

В итоговом CSS цикл разрывается на уровне повторного включения содержимого.


Взаимодействие с другими загрузчиками

Если CSS импортирует препроцессорные файлы через плагины:

@import "./theme.scss";

то обработка зависит от loader-конфигурации:

  • sass-loader (через plugin) сначала компилирует SCSS → CSS
  • затем результат проходит через стандартный CSS pipeline esbuild
  • далее применяется резолвинг @import уже в финальном CSS

Таким образом @import становится последним этапом трансформации, а не первым.


Влияние splitting и код-сплиттинга CSS

При включении code splitting:

  • каждый entry-point может формировать отдельный CSS chunk

  • @import между чанками превращается в:

    • либо инлайн в общий chunk
    • либо в отдельный файл-чанк

esbuild старается минимизировать дублирование:

  • общие зависимости выносятся в shared chunk
  • уникальные стили остаются локальными

Особенности резолвинга в watch-режиме

В режиме наблюдения за файлами:

  • каждый @import регистрируется как зависимость
  • изменение импортируемого файла триггерит пересборку всех зависимых узлов
  • пересчёт происходит локально, без полного пересоздания графа при небольших изменениях

Это критично для проектов с большим количеством CSS-модулей, где зависимостей многоуровневая структура.


Итоговая модель обработки @import

Логически поведение esbuild можно представить как последовательность:

  • парсинг CSS
  • построение графа зависимостей через @import
  • резолвинг путей (filesystem + node_modules)
  • рекурсивная инлайн-обработка
  • дедупликация и защита от циклов
  • финальная сериализация в правильном порядке каскада