Написание простого Transformer

В основе Parcel лежит модульная система плагинов, где ключевую роль играют трансформеры (transformers). Именно они отвечают за преобразование входных файлов в формат, пригодный для дальнейшей сборки и бандлинга. Любой файл, проходящий через пайплайн Parcel, может быть изменён одним или несколькими трансформерами в зависимости от его типа, расширения и настроек проекта.

Трансформер в Parcel — это модуль, который принимает исходный код и возвращает структурированное представление в виде AST (Abstract Syntax Tree) или модифицированного кода. На практике чаще используется кодовая трансформация без явной работы с AST, однако поддержка AST-инструментов также встроена.

Parcel использует строгую контрактную модель: каждый трансформер должен соответствовать API плагинов и явно объявлять, какие типы файлов он обрабатывает.


Базовая структура трансформера

Трансформер в Parcel представляет собой объект с набором методов, где обязательным является метод transform. Минимальная реализация выглядит следующим образом:

export default {
  transform({ asset }) {
    const code = asset.getCode();

    const transformedCode = code.replace(/console\.log/g, 'logger.log');

    asset.setCode(transformedCode);

    return [asset];
  }
};

Ключевые элементы:

  • asset — объект, представляющий файл в системе Parcel
  • getCode() — получение исходного содержимого файла
  • setCode() — запись изменённого содержимого
  • возвращаемое значение — массив активов, которые продолжают путь в сборке

Такой трансформер может быть подключён к конфигурации Parcel через .parcelrc.


Регистрация трансформера в Parcel

Parcel использует декларативную систему конфигурации плагинов. Для подключения трансформера используется файл конфигурации:

{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.txt": ["./transformers/text-transformer.js"]
  }
}

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

Важно, что порядок трансформеров имеет значение: каждый следующий получает результат предыдущего.


Работа с Asset API

Объект asset является центральной абстракцией Parcel. Он содержит не только код, но и метаданные файла.

Основные методы:

  • asset.getCode() — исходный текст
  • asset.setCode(code) — обновление содержимого
  • asset.type — тип файла (js, css, txt и т.д.)
  • asset.filePath — путь к файлу
  • asset.meta — пользовательские метаданные

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

export default {
  transform({ asset }) {
    const code = asset.getCode();

    asset.meta.transformedAt = Date.now();

    asset.setCode(code.toUpperCase());

    return [asset];
  }
};

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


Асинхронные трансформации

Parcel поддерживает асинхронные трансформеры, что важно для работы с сетью, файловой системой или тяжёлыми вычислениями.

export default {
  async transform({ asset }) {
    const code = await asset.getCode();

    const response = await fetch('https://api.example.com/transform', {
      method: 'POST',
      body: code
    });

    const result = await response.text();

    asset.setCode(result);

    return [asset];
  }
};

Асинхронная модель исполнения позволяет не блокировать основной поток сборки и масштабировать пайплайн.


Работа с несколькими выходными активами

Трансформер может возвращать несколько файлов, создавая новые активы на основе одного входного.

export default {
  transform({ asset }) {
    const code = asset.getCode();

    const minified = code.replace(/\s+/g, '');

    const minAsset = asset.clone();
    minAsset.setCode(minified);
    minAsset.filePath = asset.filePath.replace('.js', '.min.js');

    return [asset, minAsset];
  }
};

Такой подход используется для генерации:

  • минифицированных версий
  • source map
  • альтернативных форматов (например, ESM + CJS)

Работа с AST (Abstract Syntax Tree)

Для сложных трансформаций используется AST. Parcel предоставляет интеграцию с Babel и другими парсерами.

Пример трансформации через AST:

import * as babel from '@babel/core';

export default {
  transform({ asset }) {
    const code = asset.getCode();

    const result = babel.transformSync(code, {
      plugins: [
        function customPlugin() {
          return {
            visitor: {
              Identifier(path) {
                if (path.node.name === 'debug') {
                  path.node.name = 'logger';
                }
              }
            }
          };
        }
      ]
    });

    asset.setCode(result.code);

    return [asset];
  }
};

AST-подход обеспечивает:

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

Условная обработка файлов

Parcel позволяет ограничивать применение трансформеров по типу или содержимому файла.

export default {
  transform({ asset }) {
    if (asset.type !== 'js') {
      return [asset];
    }

    const code = asset.getCode();

    if (!code.includes('console')) {
      return [asset];
    }

    asset.setCode(code.replace(/console/g, 'log'));

    return [asset];
  }
};

Такой подход снижает избыточную обработку и ускоряет сборку.


Интеграция с зависимостями

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

export default {
  transform({ asset }) {
    asset.addDependency({
      specifier: './utils.js',
      specifierType: 'esm'
    });

    const code = asset.getCode();

    asset.setCode(code);

    return [asset];
  }
};

Это позволяет строить динамические графы модулей прямо на этапе трансформации.


Обработка ошибок в трансформерах

Parcel ожидает, что трансформер либо вернёт корректный результат, либо выбросит исключение.

export default {
  transform({ asset }) {
    try {
      const code = asset.getCode();

      if (!code) {
        throw new Error('Empty file');
      }

      asset.setCode(code);

      return [asset];
    } catch (err) {
      throw new Error(`Transform error in ${asset.filePath}: ${err.message}`);
    }
  }
};

Ошибки в трансформерах прерывают сборку и отображаются в терминале с указанием файла.


Кэширование трансформаций

Parcel автоматически кэширует результаты трансформаций на основе:

  • содержимого файла
  • конфигурации плагина
  • зависимостей

Для корректной работы трансформера важно учитывать детерминированность:

export default {
  transform({ asset }) {
    const code = asset.getCode();

    // Нежелательно: недетерминированный результат
    const randomSuffix = Math.random();

    asset.setCode(code + randomSuffix);

    return [asset];
  }
};

Такой код нарушает кэширование и приводит к нестабильным сборкам.


Оптимизация трансформеров

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

  • минимизация операций над строками
  • избегание лишнего парсинга AST
  • ранний выход при отсутствии изменений
  • использование кешируемых вычислений

Пример раннего выхода:

export default {
  transform({ asset }) {
    const code = asset.getCode();

    if (!code.includes('TODO')) {
      return [asset];
    }

    asset.setCode(code.replace(/TODO/g, ''));

    return [asset];
  }
};

Комбинирование нескольких трансформеров

Parcel последовательно применяет трансформеры, формируя цепочку обработки:

Исходный файл → Transformer A → Transformer B → Transformer C → Bundler

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

Пример конфигурации:

{
  "transformers": {
    "*.js": [
      "./transformers/remove-debug.js",
      "./transformers/replace-api.js",
      "./transformers/minify.js"
    ]
  }
}

Расширенные сценарии использования

Трансформеры применяются не только для модификации кода, но и для:

  • внедрения feature flags
  • генерации telemetry-кода
  • адаптации под разные окружения
  • конвертации языков (TypeScript → JavaScript, Markdown → HTML)
  • внедрения polyfill-логики

Пример feature flag трансформации:

export default {
  transform({ asset }) {
    const code = asset.getCode();

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

    const result = code.replace(/__DEV__/g, String(!isProd));

    asset.setCode(result);

    return [asset];
  }
};