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

Механизм плагинов в esbuild построен вокруг двух ключевых этапов: разрешение модулей и загрузка содержимого. В отличие от более тяжёлых сборщиков, esbuild сознательно ограничивает API, оставляя только минимально необходимый набор хуков: onResolve и onLoad. Именно через их комбинацию реализуется поддержка любых нестандартных форматов файлов — от YAML и Markdown до бинарных ресурсов и пользовательских DSL.

Плагин в esbuild представляет собой объект с именем и массивом правил, каждое из которых привязывается к определённым путям или расширениям. Логика обработки строится вокруг виртуальных пространств имён (namespace), позволяющих разделять стандартные и преобразованные ресурсы.


Базовая структура плагина

Любой плагин начинается с регистрации обработчиков:

  • onResolve — перехватывает импорт и определяет, как модуль должен быть интерпретирован
  • onLoad — отвечает за чтение и преобразование содержимого

Минимальная форма плагина для нестандартных файлов:

const customFormatPlugin = {
  name: 'custom-format',
  setup(build) {
    build.onResolve({ filter: /\.datax$/ }, args => {
      return {
        path: args.path,
        namespace: 'datax'
      };
    });

    build.onLoad({ filter: /.*/, namespace: 'datax' }, async (args) => {
      const fs = await import('fs/promises');
      const raw = await fs.readFile(args.path, 'utf8');

      return {
        contents: `export default ${JSON.stringify(raw)};`,
        loader: 'js'
      };
    });
  }
};

Здесь .datax рассматривается как отдельный формат, который преобразуется в обычный JavaScript-модуль.


Механизм onResolve: контроль импорта

onResolve выполняет роль маршрутизатора модулей. Он решает:

  • следует ли перехватывать импорт
  • в каком пространстве имён он будет обрабатываться дальше
  • как будет изменён путь

Пример обработки Markdown-файлов:

build.onResolve({ filter: /\.md$/ }, (args) => {
  return {
    path: args.path,
    namespace: 'markdown'
  };
});

Важно, что onResolve не обязан читать файл. Его задача — только классификация и маршрутизация.

Дополнительно можно модифицировать путь:

build.onResolve({ filter: /^virtual:/ }, (args) => {
  return {
    path: args.path.replace('virtual:', ''),
    namespace: 'virtual'
  };
});

Механизм onLoad: трансформация содержимого

onLoad получает управление после того, как esbuild определил источник и namespace. Здесь выполняется основная работа:

  • чтение файлов
  • парсинг нестандартного формата
  • генерация JavaScript-кода

Пример обработки CSV:

import fs from 'fs/promises';

build.onLoad({ filter: /\.csv$/, namespace: 'file' }, async (args) => {
  const text = await fs.readFile(args.path, 'utf8');

  const rows = text.trim().split('\n').map(line => line.split(','));

  const headers = rows.shift();

  const data = rows.map(row => {
    const obj = {};
    headers.forEach((h, i) => obj[h] = row[i]);
    return obj;
  });

  return {
    contents: `export default ${JSON.stringify(data)};`,
    loader: 'js'
  };
});

Таким образом CSV превращается в массив объектов, доступный напрямую в коде.


Работа с YAML и JSON-подобными форматами

Нестандартные форматы конфигурации часто требуют парсинга:

import fs from 'fs/promises';
import yaml from 'js-yaml';

build.onLoad({ filter: /\.ya?ml$/, namespace: 'file' }, async (args) => {
  const text = await fs.readFile(args.path, 'utf8');
  const parsed = yaml.load(text);

  return {
    contents: `export default ${JSON.stringify(parsed)};`,
    loader: 'js'
  };
});

Здесь важно, что esbuild не ограничивает выбор парсеров. Любая внешняя библиотека может быть использована в onLoad.


Обработка Markdown как модуля

Markdown часто используется как источник контента, который необходимо преобразовать в HTML:

import fs from 'fs/promises';
import { marked } from 'marked';

build.onLoad({ filter: /\.md$/, namespace: 'markdown' }, async (args) => {
  const source = await fs.readFile(args.path, 'utf8');
  const html = marked.parse(source);

  return {
    contents: `
      const html = ${JSON.stringify(html)};
      export default html;
    `,
    loader: 'js'
  };
});

В более сложных сценариях Markdown может разделяться на:

  • метаданные (frontmatter)
  • тело документа
  • таблицы и блоки кода

Каждая часть может быть преобразована отдельно и экспортирована как структура.


Работа с бинарными форматами

esbuild поддерживает загрузку бинарных данных через loader: 'binary', но плагины позволяют расширить поведение.

Пример обработки изображения:

import fs from 'fs/promises';

build.onLoad({ filter: /\.(png|jpg|jpeg)$/, namespace: 'file' }, async (args) => {
  const buffer = await fs.readFile(args.path);

  return {
    contents: buffer,
    loader: 'binary'
  };
});

Возможен также инлайнинг в base64:

const base64 = buffer.toString('base64');

return {
  contents: `export default "data:image/png;base64,${base64}"`,
  loader: 'js'
};

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

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

Пример виртуальных модулей:

build.onResolve({ filter: /^config:/ }, (args) => {
  return {
    path: args.path,
    namespace: 'virtual-config'
  };
});

build.onLoad({ filter: /.*/, namespace: 'virtual-config' }, () => {
  return {
    contents: `export default { mode: "production" };`,
    loader: 'js'
  };
});

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


Каскадная обработка и цепочки трансформаций

Плагины могут работать совместно, образуя цепочку:

  1. Первый плагин перехватывает импорт
  2. Второй преобразует формат
  3. Третий оптимизирует результат

Пример:

  • .md → HTML (плагин Markdown)
  • HTML → минификация (плагин оптимизации)
  • результат → JS модуль

Каждый этап работает независимо через namespace.


Обработка динамических импортов

esbuild поддерживает частичную обработку динамических путей:

build.onResolve({ filter: /^dynamic:/ }, (args) => {
  return {
    path: args.path.replace('dynamic:', ''),
    namespace: 'dynamic'
  };
});

Это позволяет реализовать ленивую загрузку нестандартных ресурсов, например:

  • конфигурации по окружению
  • шаблоны
  • языковые файлы

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

Плагины часто опираются на внешние инструменты:

  • Babel для AST-преобразований
  • PostCSS для CSS-подобных форматов
  • Markdown парсеры
  • JSON schema валидаторы

Пример интеграции AST-трансформации:

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

build.onLoad({ filter: /\.specialjs$/, namespace: 'file' }, async (args) => {
  const source = await fs.readFile(args.path, 'utf8');

  const result = babel.transformSync(source, {
    presets: ['@babel/preset-env']
  });

  return {
    contents: result.code,
    loader: 'js'
  };
});

Оптимизация производительности плагинов

Механизм esbuild ориентирован на скорость, поэтому плагины должны учитывать:

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

Пример простого кэша:

const cache = new Map();

build.onLoad({ filter: /\.md$/, namespace: 'markdown' }, async (args) => {
  if (cache.has(args.path)) {
    return cache.get(args.path);
  }

  const fs = await import('fs/promises');
  const source = await fs.readFile(args.path, 'utf8');

  const result = {
    contents: `export default ${JSON.stringify(source)}`,
    loader: 'js'
  };

  cache.set(args.path, result);

  return result;
});

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

Один плагин может обслуживать множество типов файлов:

build.onLoad({ filter: /\.(csv|yaml|md)$/ }, async (args) => {
  if (args.path.endsWith('.csv')) {
    // обработка CSV
  }

  if (args.path.endsWith('.yaml')) {
    // обработка YAML
  }

  if (args.path.endsWith('.md')) {
    // обработка Markdown
  }
});

Такой подход удобен для унифицированных систем контента, где разные форматы преобразуются в единый JSON-слой.


Расширение модели загрузчиков esbuild

Хотя esbuild предлагает ограниченный набор loader-ов (js, ts, css, json, binary, text, base64), плагины позволяют эмулировать любые типы через промежуточную компиляцию в JavaScript.

Типичный паттерн:

  • входной файл нестандартного формата
  • парсинг
  • генерация JS-кода
  • возврат через loader: 'js'

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