В плагинах esbuild хук onLoad отвечает за перехват
процесса чтения содержимого модулей после того, как путь файла уже был
разрешён. Он срабатывает на этапе загрузки ресурса и позволяет полностью
заменить, модифицировать или динамически сгенерировать содержимое модуля
до того, как esbuild применит трансформации (loader, JSX, TypeScript и
т.д.).
Процесс обработки модуля в esbuild можно условно разделить на два ключевых этапа:
onResolve)onLoad)onResolve определяет, откуда берётся модуль, а
onLoad — что именно в этом модуле будет обработано
дальше.
Хук onLoad работает уже с финальным идентификатором
файла (или виртуального ресурса), который представлен в виде
объекта:
path — путь к ресурсуnamespace — пространство имён (важно для виртуальных
модулей и нестандартных источников)export default {
name: 'example-plugin',
setup(build) {
build.onLoad({ filter: /\.txt$/ }, async (args) => {
return {
contents: `export default ${JSON.stringify(await fs.promises.readFile(args.path, 'utf8'))}`,
loader: 'js'
};
});
}
};
В этом примере любой .txt файл превращается в ES-модуль,
экспортирующий строку.
Ключевые элементы результата:
contents — итоговое содержимое модуляloader — способ интерпретации (js,
ts, json, text,
base64, file и др.)Первый аргумент onLoad — объект с filter
(регулярное выражение). Он определяет, какие пути будут
обрабатываться:
build.onLoad({ filter: /\.md$/ }, handler);
Фильтр применяется к path, поэтому он работает уже после
onResolve. Это означает, что нельзя влиять на выбор файла
на этом этапе — только на его содержимое.
В esbuild можно зарегистрировать несколько onLoad для
одного и того же фильтра. Они выполняются в порядке регистрации, но
фактическое выполнение зависит от namespace и точного совпадения
фильтра.
Важно учитывать:
onLoad, вернувший результат,
завершает цепочкуnull, выполнение
продолжаетсяbuild.onLoad({ filter: /.*/ }, () => null);
build.onLoad({ filter: /\.js$/ }, () => ({
contents: 'export const x = 1',
loader: 'js'
}));
onLoad особенно важен для работы с виртуальными файлами.
В связке с onResolve он позволяет создавать модули, которых
не существует на диске.
build.onResolve({ filter: /^virtual:config$/ }, () => ({
path: 'virtual:config',
namespace: 'virtual'
}));
build.onLoad({ filter: /.*/, namespace: 'virtual' }, () => {
return {
contents: `export const config = { debug: true }`,
loader: 'js'
};
});
Здесь:
onResolve создаёт фиктивный путьonLoad предоставляет содержимое для этого путиNamespace выступает как дополнительный уровень изоляции, предотвращающий конфликт с реальными файлами.
onLoad поддерживает асинхронные операции, что позволяет
подключать внешние источники:
build.onLoad({ filter: /\.api$/ }, async (args) => {
const res = await fetch(`https://example.com/data?file=${args.path}`);
const json = await res.json();
return {
contents: `export default ${JSON.stringify(json)}`,
loader: 'js'
};
});
Это превращает esbuild в инструмент для сборки модулей с динамическими зависимостями, однако такие операции могут существенно замедлить сборку.
Поле loader определяет, как esbuild интерпретирует
contents.
Основные режимы:
js — JavaScriptts — TypeScriptjson — JSONtext — строкаbase64 — бинарные данныеfile — файл как ресурсПример преобразования Markdown в строковый модуль:
build.onLoad({ filter: /\.md$/ }, async (args) => {
const source = await fs.promises.readFile(args.path, 'utf8');
return {
contents: JSON.stringify(source),
loader: 'text'
};
});
В этом случае esbuild не будет пытаться интерпретировать содержимое как код.
onLoad позволяет полностью игнорировать исходный
файл:
build.onLoad({ filter: /generate:uuid/ }, () => {
return {
contents: `
export const uuid = () => crypto.randomUUID();
`,
loader: 'js'
};
});
Такой подход используется для:
esbuild не кэширует результаты onLoad автоматически на
уровне плагина. Это означает, что ответственность за оптимизацию лежит
на разработчике.
Типичная стратегия:
const cache = new Map();
build.onLoad({ filter: /\.data$/ }, async (args) => {
if (cache.has(args.path)) {
return cache.get(args.path);
}
const data = await fs.promises.readFile(args.path, 'utf8');
const result = {
contents: `export default ${JSON.stringify(data)}`,
loader: 'js'
};
cache.set(args.path, result);
return result;
});
Без кэширования повторные сборки или инкрементальные запуски могут приводить к лишним I/O операциям.
onResolve:
onLoad:
Типичный пайплайн:
import x from './file.ext'onResolve меняет путь или namespaceonLoadonLoad возвращает содержимоеЕсли onLoad выбрасывает исключение, сборка завершается
ошибкой. Корректнее возвращать диагностическое сообщение через
errors:
build.onLoad({ filter: /\.cfg$/ }, () => {
return {
errors: [{
text: 'Неверный формат конфигурации',
location: null
}]
};
});
Можно также комбинировать warnings и errors
для частичной деградации поведения.
Namespace позволяет разделять разные типы виртуальных ресурсов:
build.onLoad({ filter: /.*/, namespace: 'http' }, async (args) => {
const res = await fetch(args.path);
const text = await res.text();
return {
contents: text,
loader: 'text'
};
});
Это создаёт модель, в которой разные источники данных (файловая система, HTTP, виртуальные модули) обрабатываются единым механизмом, но не конфликтуют между собой.
При наличии нескольких плагинов важно учитывать:
filter может не перебить общийЭто делает архитектуру плагинов чувствительной к порядку подключения.
onLoad может возвращать уже готовый JavaScript, но также
может делегировать работу встроенным трансформерам esbuild:
build.onLoad({ filter: /\.custom$/ }, async (args) => {
const source = await fs.promises.readFile(args.path, 'utf8');
const transformed = source.replaceAll('%%VERSION%%', '1.0.0');
return {
contents: transformed,
loader: 'ts'
};
});
В этом случае после onLoad esbuild применяет
TypeScript/JS трансформации, JSX-компиляцию и другие встроенные
этапы.
onLoad не может изменить граф зависимостей напрямую —
только содержимоеЭти ограничения формируют модель, в которой onLoad
выступает точкой контроля содержимого, но не управления архитектурой
модулей.