Механизм плагинов в 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 выполняет роль маршрутизатора модулей. Он
решает:
Пример обработки 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 получает управление после того, как esbuild
определил источник и namespace. Здесь выполняется основная работа:
Пример обработки 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 превращается в массив объектов, доступный напрямую в коде.
Нестандартные форматы конфигурации часто требуют парсинга:
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 часто используется как источник контента, который необходимо преобразовать в 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 может разделяться на:
Каждая часть может быть преобразована отдельно и экспортирована как структура.
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 позволяет разделять обработку разных типов ресурсов с одинаковыми расширениями или логикой.
Пример виртуальных модулей:
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'
};
});
Такой подход позволяет создавать полностью синтетические модули без файловой системы.
Плагины могут работать совместно, образуя цепочку:
Пример:
.md → HTML (плагин Markdown)Каждый этап работает независимо через namespace.
esbuild поддерживает частичную обработку динамических путей:
build.onResolve({ filter: /^dynamic:/ }, (args) => {
return {
path: args.path.replace('dynamic:', ''),
namespace: 'dynamic'
};
});
Это позволяет реализовать ленивую загрузку нестандартных ресурсов, например:
Плагины часто опираются на внешние инструменты:
Пример интеграции 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 предлагает ограниченный набор loader-ов
(js, ts, css, json,
binary, text, base64), плагины
позволяют эмулировать любые типы через промежуточную компиляцию в
JavaScript.
Типичный паттерн:
loader: 'js'Это делает систему универсальной для любых пользовательских форматов, включая DSL и конфигурационные языки, специфичные для проекта.