Поддержка современного синтаксиса CSS: nesting, layers

CSS nesting в сборке через esbuild

CSS nesting (вложенные правила) представляет собой синтаксическое расширение, позволяющее описывать иерархию селекторов прямо внутри родительского блока. Это приближает CSS к структурам препроцессоров (Sass, Less), но при этом сохраняет нативный формат спецификации, который постепенно стандартизируется.

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

Синтаксис CSS nesting

Типичный пример вложенности:

.card {
  padding: 16px;

  .title {
    font-size: 18px;
    font-weight: 600;
  }

  &:hover {
    background: #f5f5f5;
  }
}

После трансформации результат эквивалентен классическому CSS:

.card {
  padding: 16px;
}

.card .title {
  font-size: 18px;
  font-weight: 600;
}

.card:hover {
  background: #f5f5f5;
}

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

Поведение esbuild при обработке вложенности

В пайплайне esbuild CSS-файлы проходят стадию разбора и нормализации. В зависимости от версии:

  • вложенность может транслироваться встроенным трансформером
  • либо передаваться через цепочку плагинов (например, PostCSS)
  • либо игнорироваться, если включён режим минимальной трансформации

Типичная конфигурация с плагином:

import esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/index.css"],
  bundle: true,
  outfile: "dist/style.css",
  loader: {
    ".css": "css"
  }
});

При подключении PostCSS-плагины для nesting часто используются дополнительно:

import postcss from "postcss";
import postcssNested from "postcss-nested";

const nestedPlugin = {
  name: "css-nesting",
  setup(build) {
    build.onLoad({ filter: /\.css$/ }, async (args) => {
      const fs = await import("fs");
      const source = await fs.promises.readFile(args.path, "utf8");

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

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

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

Ограничения и особенности

CSS nesting в сборке имеет несколько характерных ограничений:

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

Особое внимание требуется при использовании комбинации вложенности и медиа-запросов:

.card {
  padding: 16px;

  @media (max-width: 768px) {
    padding: 12px;

    .title {
      font-size: 16px;
    }
  }
}

После обработки структура превращается в набор независимых правил:

.card {
  padding: 16px;
}

@media (max-width: 768px) {
  .card {
    padding: 12px;
  }

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

Такое преобразование критично для предсказуемости каскада.


Cascade Layers (@layer) и их обработка

CSS Cascade Layers (@layer) вводят механизм управления приоритетами стилей на уровне слоёв, а не только специфичности селекторов. Это позволяет выстраивать архитектуру CSS в виде логических уровней: reset, base, components, utilities.

В рамках esbuild поддержка @layer связана с задачами объединения и корректного упорядочивания правил при бандлинге нескольких файлов.

Базовый синтаксис layers

@layer reset, base, components;

@layer reset {
  * {
    margin: 0;
    padding: 0;
  }
}

@layer base {
  body {
    font-family: system-ui;
  }
}

@layer components {
  .button {
    padding: 10px 16px;
  }
}

Слои определяют порядок применения правил независимо от специфичности селекторов внутри них.

Поведение при сборке

При объединении CSS из нескольких модулей сборщик сталкивается с задачей:

  • сохранить декларации @layer
  • объединить одноимённые слои
  • корректно выстроить порядок приоритета

Пример входных файлов:

fileA.css

@layer base {
  h1 {
    font-size: 24px;
  }
}

fileB.css

@layer base {
  h2 {
    font-size: 20px;
  }
}

После бандлинга результат должен сохранять единый слой:

@layer base {
  h1 {
    font-size: 24px;
  }

  h2 {
    font-size: 20px;
  }
}

При этом порядок файлов влияет на итоговую композицию, если отсутствует явное объявление порядка слоёв.


Явное управление порядком слоёв

Системы сборки, включая esbuild, опираются на правило: порядок @layer определяется либо первой декларацией, либо явным списком.

@layer reset, base, components, utilities;

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


Взаимодействие @layer и вложенности

Комбинация @layer и CSS nesting формирует более сложную структуру преобразования.

@layer components {
  .card {
    padding: 16px;

    .title {
      font-size: 18px;
    }

    &:hover {
      box-shadow: 0 4px 10px rgba(0,0,0,0.1);
    }
  }
}

После трансформации логика слоёв сохраняется, но вложенность разворачивается:

@layer components {
  .card {
    padding: 16px;
  }

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

  .card:hover {
    box-shadow: 0 4px 10px rgba(0,0,0,0.1);
  }
}

Слой остаётся единым контейнером для всех производных правил.


Слияние слоёв при мульти-энтри сборке

При сборке нескольких CSS-файлов esbuild выполняет конкатенацию с учётом:

  • совпадения имени слоя
  • порядка подключения модулей
  • наличия глобальных объявлений @layer

Пример конфликтного сценария:

/* a.css */
@layer base {
  body { line-height: 1.5; }
}

/* b.css */
@layer base {
  body { line-height: 1.6; }
}

Результат зависит от порядка подключения файлов, если не применяется дополнительная нормализация через плагин. Это делает критичным контроль порядка entry points в сборке.


Практические аспекты интеграции

При использовании CSS nesting и @layer в одном проекте с esbuild часто применяется архитектурный подход разделения ответственности:

  • reset/normalize — низший слой
  • base — типографика и базовые элементы
  • components — UI-компоненты
  • utilities — утилитарные классы

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

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

@layer reset, base, components, utilities;

@layer reset {
  /* сброс */
}

@layer base {
  /* базовые стили */
}

@layer components {
  /* компоненты */
}

@layer utilities {
  /* утилиты */
}

Совместимость и ограничения при бандлинге

При сборке через esbuild следует учитывать несколько системных особенностей:

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

Особенно важно учитывать, что итоговый CSS после сборки становится линейным текстом, и любые логические конструкции существуют только до стадии бандлинга.


Связь с архитектурой модульного CSS

Использование nesting и @layer в рамках esbuild часто сопровождает модульный подход к стилям, где каждый компонент или пакет содержит собственный CSS-фрагмент.

@layer components {
  .modal {
    display: flex;

    .header {
      font-weight: 600;
    }

    .body {
      padding: 16px;
    }
  }
}

После сборки такие блоки объединяются в единый CSS-граф, где:

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

Особенности отладки после трансформации

После обработки CSS сборщиком исходная структура теряет вложенность, что влияет на отладку:

  • исходные вложенные блоки отсутствуют в итоговом файле
  • DevTools показывает только результирующие селекторы
  • сопоставление с исходником требует source maps
  • слои остаются видимыми как логические контейнеры @layer

Использование source maps в esbuild помогает восстановить соответствие между вложенными структурами и финальными селекторами, что критично при сложных каскадных конфигурациях.