Декораторы: версия 2022-03 и legacy

Декораторы в SWC реализуются как трансформационный слой, который переписывает синтаксис функций-декораторов в эквивалентный JavaScript-код. SWC поддерживает две несовместимые модели: legacy-декораторы (историческая реализация TypeScript и ранних транспайлеров) и стандартную спецификацию ECMAScript Decorators 2022-03, которая существенно меняет семантику, порядок выполнения и форму API. ## Архитектура обработки декораторов в SWC Внутри SWC декораторы обрабатываются на этапе AST-трансформации. Парсер формирует дерево, в котором декораторы представлены как отдельные узлы, привязанные к классам, методам, полям и параметрам. Далее трансформер выбирает стратегию преобразования в зависимости от конфигурации: * `legacy: true` — используется модель TypeScript legacy decorators * `legacy: false` — используется спецификация TC39 (2022-03 и новее) * `decoratorMetadata: true` — добавление метаданных типов (совместимость с reflect-metadata) Конфигурация SWC обычно задаётся через `.swcrc`: ```json { "jsc": { "transform": { "legacyDecorator": true, "decoratorMetadata": true } } } ``` или в более новых версиях: ```json { "jsc": { "transform": { "legacyDecorator": false } } } ``` Разница между режимами не ограничивается синтаксисом — меняется сама модель исполнения. --- ## Legacy-декораторы Legacy-модель основана на поведении TypeScript до стандартизации ECMAScript. Декораторы представляют собой функции, которые вызываются с заранее определёнными аргументами в момент определения класса. ### Форма декоратора Для метода: ```ts function log(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`Call: ${key}`); return original.apply(this, args); }; } ``` Использование: ```ts class UserService { @log getUser(id: number) { return { id }; } } ``` ### Семантика выполнения Legacy-декораторы: * применяются сверху вниз * получают `target`, `key`, `descriptor` * могут мутировать descriptor напрямую * работают через `Object.defineProperty` * поддерживают `emitDecoratorMetadata` Ключевая особенность — изменение существующего дескриптора свойства. Это делает legacy-декораторы тесно связанными с `Object.defineProperty` и прототипной моделью JavaScript. ### Метаданные При включении `emitDecoratorMetadata` SWC добавляет вызовы `Reflect.metadata`: ```ts import "reflect-metadata"; class Example { constructor(private service: Service) {} } ``` После трансформации появляется: ```js __metadata("design:paramtypes", [Service]) ``` Это используется DI-контейнерами и фреймворками, но не является частью стандарта ECMAScript. --- ## Декораторы 2022-03 (ECMAScript Proposal) Новая модель декораторов, зафиксированная в спецификации 2022-03, полностью пересматривает механизм. Основное изменение — переход от мутации descriptor к декларативному описанию поведения через context-объект. ### Форма декоратора ```ts function log(value, context) { if (context.kind === "method") { return function (...args) { console.log(`Call: ${context.name}`); return value.apply(this, args); }; } } ``` Использование: ```ts class UserService { @log getUser(id) { return { id }; } } ``` ### Контекст декоратора Вместо `target/key/descriptor` используется объект `context`, который содержит: * `kind` — тип элемента (`class`, `method`, `field`, `getter`, `setter`, `accessor`) * `name` — имя свойства * `access` — доступ к оригинальному значению (в некоторых режимах) * `private` — флаг приватности * `addInitializer` — регистрация инициализаторов * `metadata` — пространство для метаданных --- ## Ключевые отличия моделей ### 1. Момент и способ модификации Legacy: * модификация через `PropertyDescriptor` * прямое изменение поведения метода * патчинг объекта 2022-03: * возврат новой функции или descriptor-like структуры * явное управление через return * возможность не мутировать исходный объект --- ### 2. Порядок применения Legacy: * снизу вверх при объявлении * сверху вниз при вызове обёрток 2022-03: * строгий порядок применения: сначала поля, затем методы, затем классы * разделение инициализации и определения --- ### 3. Поля класса Legacy: * нестабильная поддержка * часто реализуется через `Object.defineProperty` в конструкторе 2022-03: * полноценная поддержка class fields * возможность использовать `addInitializer` Пример: ```ts function initLogger(value, context) { if (context.kind === "field") { return function (initialValue) { console.log(`Init field ${context.name}`); return initialValue; }; } } ``` --- ### 4. Инициализация (initializers) Одно из ключевых различий SWC при переключении режимов. 2022-03 вводит механизм: ```ts context.addInitializer(() => { // выполняется при создании экземпляра }); ``` Это позволяет: * регистрировать side effects без изменения конструктора * избегать ручного патчинга prototype * работать в композиции декораторов Legacy не имеет эквивалента — любые побочные эффекты реализуются вручную через обёртки конструктора. --- ## Поведение SWC при трансформации legacy-декораторов SWC в legacy-режиме генерирует вспомогательные функции: * `__decorate` * `__metadata` * `__param` Пример трансформации: ```ts class A { @log method() {} } ``` становится: ```js class A { method() {} } __decorate([ log ], A.prototype, "method", null); ``` Это отражает внешний, «пост-компиляционный» характер применения декораторов. --- ## Поведение SWC при трансформации 2022-03 В новом режиме SWC избегает глобальных helper-функций в стиле TypeScript legacy. Вместо этого генерируется код, который: * создаёт функции-декораторы с контекстом * применяет их во время определения класса * использует локальные вспомогательные обвязки Принципиально меняется модель: декораторы становятся частью декларативной инициализации класса, а не пост-обработкой. --- ## Совместимость и конфликт режимов Legacy и 2022-03 несовместимы на уровне API. ### Конфликтные зоны: * сигнатура декоратора * наличие `context` * return-значения * работа с полями класса * порядок выполнения Попытка смешивания приводит к ошибкам трансформации или некорректному runtime-поведению. --- ## SWC и TypeScript interoperability SWC часто используется как drop-in замена TypeScript компилятора, поэтому поддержка двух моделей декораторов критична. ### Типичный сценарий TS legacy: ```json { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } } ``` ### Эквивалент SWC: ```json { "jsc": { "parser": { "syntax": "typescript", "decorators": true }, "transform": { "legacyDecorator": true, "decoratorMetadata": true } } } ``` --- ## Практическое различие поведения в runtime ### Legacy: * декоратор выполняется сразу при определении класса * влияет на prototype * легко перехватывает методы ### 2022-03: * разделение definition-time и initialization-time * возможность откладывать эффекты * более строгая изоляция контекста --- ## Причины перехода на 2022-03 модель Основные проблемы legacy: * отсутствие стандартизации * сложная композиция декораторов * неочевидный порядок выполнения * сильная зависимость от `Object.defineProperty` Новая модель решает это через: * унифицированный context API * детерминированный порядок * поддержку полей и классов на уровне спецификации * возможность безопасной композиции --- ## Особенности реализации SWC SWC оптимизирует обе модели: * минимизирует runtime helper-ов * инлайнит простые декораторы * избегает лишних обёрток при отсутствии side effects * различает pure и impure декораторы при трансформации Особенно важно, что в 2022-03 режиме SWC может сохранять больше исходной структуры AST, снижая объем генерируемого кода. --- ## Типичные ошибки при миграции * использование `target, key, descriptor` в новом режиме * ожидание автоматического `reflect-metadata` поведения * попытка изменять descriptor в 2022-03 * смешивание `legacyDecorator: true` и нового API Такие ошибки часто проявляются не на этапе компиляции, а только в runtime, что усложняет диагностику.