Декораторы и экспериментальные фичи

Модульный бандлер ориентирован на работу с современным JavaScript и ESM-экосистемой, но при этом сам по себе не является транспилятором языка. Это ключевое ограничение определяет подход к использованию экспериментальных возможностей ECMAScript, включая декораторы, приватные поля, pipeline-операторы и другие stage-фичи.

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


Архитектурное ограничение: парсинг без трансформации

Rollup не предназначен для понимания нестандартного синтаксиса без расширений. Его задача ограничивается:

  • построением графа зависимостей
  • анализом ESM-экспорта и импорта
  • tree-shaking
  • генерацией оптимизированного бандла

Любая синтаксическая конструкция проходит этап парсинга. Если она не поддерживается, процесс прерывается. Это особенно важно для декораторов, которые исторически проходили несколько стадий стандартизации.


Декораторы: стадии стандарта и влияние на сборку

Декораторы в JavaScript прошли несколько несовместимых версий:

  • legacy decorators (TypeScript, Babel 7 старого режима)
  • TC39 stage 2/3 decorators (современный стандарт)
  • различные промежуточные реализации

Эта несовместимость делает невозможным «нативную» поддержку без выбора конкретного трансформера.

Проблема интерпретации декораторов

Пример:

@sealed
class Service {
  @readonly
  config() {}
}

Без трансформации парсер не сможет обработать @sealed и @readonly, если они не включены в поддерживаемый синтаксис.


Использование Babel как слоя трансформации

Наиболее распространённый способ работы с декораторами — подключение Babel через плагин:

  • @rollup/plugin-babel

Конвейер выглядит следующим образом:

  1. исходный код с декораторами
  2. Babel трансформирует AST
  3. Rollup получает уже «чистый» ES5/ESM код
  4. выполняется бандлинг

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

import babel from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: {
    format: 'esm',
    file: 'dist/bundle.js'
  },
  plugins: [
    babel({
      babelHelpers: 'bundled',
      extensions: ['.js', '.ts']
    })
  ]
};

Ключевой момент заключается в том, что декораторы должны быть явно включены в Babel-конфигурации:

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-11" }]
  ]
}

TypeScript и декораторы в Rollup-пайплайне

TypeScript часто используется как первичный транспилятор до этапа бандлинга. В этом случае декораторы обрабатываются раньше, чем код попадает в Rollup.

Варианты стратегии

1. TypeScript-first

TS → JS → Rollup
  • декораторы трансформируются tsc
  • Rollup работает с уже готовым JS

2. Babel-first

TS → Babel → Rollup
  • больше гибкости
  • поддержка новых stage-фич быстрее

Экспериментальные фичи и проблема парсера

Экспериментальные возможности ECMAScript делятся на две категории:

1. Синтаксические расширения

Требуют изменения AST:

  • decorators
  • pipeline operator
  • do expressions
  • record & tuple (исторически)

2. Семантические фичи

Не требуют парсинга, но могут влиять на runtime:

  • new built-in methods
  • import attributes
  • top-level await (уже поддерживается)

Rollup критичен к первой категории, так как она влияет на этап анализа кода.


Acorn и расширение синтаксиса

Rollup использует Acorn как базовый парсер. Для экспериментальных фич возможны расширения через:

  • acorn plugins
  • кастомные parserOptions
  • предварительную трансформацию кода

Однако на практике это редко применяется для декораторов, так как:

  • сложность поддержки высока
  • стандарт декораторов нестабилен
  • Babel/TypeScript решают задачу универсально

Проблемы tree-shaking при использовании декораторов

Декораторы влияют на статический анализ:

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

Пример проблемы:

function log(target) {
  console.log('class defined');
}

@log
class A {}

Для Rollup это означает потенциальный side effect при импорте модуля.

В результате:

  • модуль может быть помечен как «не чистый»
  • tree-shaking становится менее агрессивным
  • часть кода сохраняется в бандле

Оптимизация работы с экспериментальными фичами

При использовании Rollup и декораторов важно учитывать несколько принципов архитектуры сборки:

Разделение ответственности

  • Rollup: бандлинг, tree-shaking, граф модулей
  • Babel/TypeScript: синтаксическая трансформация
  • плагины: интеграция и адаптация

Контроль порядка плагинов

Порядок критичен:

plugins: [
  typescript(),
  babel(),
  nodeResolve()
]

Неправильный порядок приводит к:

  • нераспознанным декораторам
  • двойной трансформации AST
  • ошибкам генерации кода

Поддержка legacy decorators

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

  • TypeScript < 5
  • старых Babel-конфигурациях

Особенности:

  • отличается порядок вызова
  • отличается сигнатура decorator factory
  • несовместим с TC39 proposal

При использовании в Rollup-конвейере важно не смешивать режимы:

  • либо legacy decorators
  • либо stage 3 decorators

Иначе итоговый бандл может содержать некорректное поведение классов.


Влияние экспериментальных фич на производительность сборки

Добавление транспиляции влияет на:

  • скорость сборки
  • размер AST
  • потребление памяти

Rollup сам по себе оптимизирован для быстрых графов модулей, но Babel/TypeScript становятся узким местом при:

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

Частые архитектурные ошибки

1. Попытка использовать декораторы без трансформера

Приводит к:

  • syntax error на этапе парсинга
  • остановке сборки

2. Дублирование трансформации

Пример:

  • TypeScript уже преобразовал декораторы
  • Babel повторно применяет transform

Результат:

  • некорректные обёртки классов
  • дублирование metadata
  • увеличение бандла

3. Игнорирование side effects

Декораторы часто:

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

Это напрямую влияет на tree-shaking и может приводить к неожиданному сохранению кода в финальном бандле.


Практика работы с будущими stage-фичами

Экспериментальные фичи ECMAScript требуют стратегии «плавающей совместимости»:

  • использование Babel preset-env с target environments
  • контроль proposal plugins
  • регулярная синхронизация с обновлениями стандарта

Rollup остаётся стабильным слоем, если вся нестабильность изолирована на уровне транспиляции.


Роль декораторов в архитектуре модульных приложений

Несмотря на экспериментальный статус, декораторы часто используются для:

  • dependency injection
  • логирования
  • декларативной регистрации сервисов
  • ORM-маппинга

Их интеграция в сборочный процесс требует чёткого разграничения:

  • синтаксическая стадия (Babel/TS)
  • модульная стадия (Rollup)
  • runtime-инициализация (браузер/Node.js)

При корректной настройке Rollup становится прозрачным слоем между этими уровнями, не вмешиваясь в семантику декораторов напрямую.