В системе сборки на основе Esbuild плагины позволяют расширять стандартный процесс обработки модулей и внедрять произвольную логику получения и трансформации ресурсов. Одним из распространённых сценариев становится загрузка данных из внешних источников — HTTP API, удалённых файловых хранилищ, динамических конфигураций или сервисов контента.
Механизм плагинов строится вокруг двух ключевых хуков: onResolve и onLoad, которые формируют цепочку разрешения и загрузки модулей. При реализации загрузчика внешних данных важно учитывать кэширование, обработку ошибок, форматирование ответа и контроль повторных запросов.
Плагин Esbuild представляет собой объект с методом
setup, который получает контекст сборщика. Через него
регистрируются обработчики:
export const externalDataPlugin = () => ({
name: "external-data-plugin",
setup(build) {
// onResolve и onLoad регистрируются здесь
}
});
Ключевой принцип заключается в перехвате импортов определённого формата, например:
import data fr om "https-data:https://api.example.com/users";
Такая схема позволяет интерпретировать импорт как запрос к внешнему ресурсу.
Первым этапом является определение, какие модули должны
обрабатываться плагином. Для этого используется
onResolve.
build.onResolve({ filter: /^https-data:/ }, (args) => {
return {
path: args.path,
namespace: "https-data"
};
});
Namespace играет критическую роль, так как он изолирует логику плагина от стандартных файловых модулей.
После разрешения пути управление передаётся в onLoad,
где происходит фактическое получение данных.
build.onLoad({ filter: /.*/, namespace: "https-data" }, async (args) => {
const url = args.path.replace("https-data:", "");
const response = await fetch(url);
const json = await response.json();
return {
contents: `export default ${JSON.stringify(json)}`,
loader: "js"
};
});
Такой подход превращает внешний API в часть графа зависимостей.
Esbuild ожидает, что результат onLoad будет представлен
как валидный модуль. Наиболее универсальный способ — генерация строки с
экспортом:
export default {...}
В случае более сложных структур можно формировать именованные экспорты:
return {
contents: `
export const users = ${JSON.stringify(json.users)};
export const meta = ${JSON.stringify(json.meta)};
`,
loader: "js"
};
Это позволяет интегрировать внешние данные в систему tree-shaking и статического анализа.
Внешние источники не ограничиваются JSON. Часто требуется поддержка CSV, XML или бинарных данных.
function parseCSV(text) {
const [header, ...rows] = text.trim().split("\n");
const keys = header.split(",");
return rows.map(row => {
const values = row.split(",");
return Object.fromEntries(keys.map((k, i) => [k, values[i]]));
});
}
Использование в плагине:
const text = await response.text();
const data = parseCSV(text);
return {
contents: `export default ${JSON.stringify(data)}`,
loader: "js"
};
Без кэширования сборка становится нестабильной и медленной. Esbuild позволяет реализовать собственный кэш на уровне плагина.
const cache = new Map();
build.onLoad({ filter: /.*/, namespace: "https-data" }, async (args) => {
if (cache.has(args.path)) {
return cache.get(args.path);
}
const url = args.path.replace("https-data:", "");
const res = await fetch(url);
const data = await res.json();
const result = {
contents: `export default ${JSON.stringify(data)}`,
loader: "js"
};
cache.set(args.path, result);
return result;
});
Работа с внешними источниками требует устойчивой обработки ошибок:
build.onLoad({ filter: /.*/, namespace: "https-data" }, async (args) => {
try {
const url = args.path.replace("https-data:", "");
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return {
contents: `export default ${JSON.stringify(data)}`,
loader: "js"
};
} catch (err) {
return {
contents: `throw new Error(${JSON.stringify(err.message)})`,
loader: "js"
};
}
});
Такой подход позволяет:
Часто внешний источник требует динамических параметров. В этом случае путь модуля может содержать query-параметры:
import users fr om "https-data:https://api.example.com/users?limit=10";
Разбор параметров:
const url = new URL(args.path.replace("https-data:", ""));
const lim it = url.searchParams.get("limit");
Использование параметров в запросе:
const response = await fetch(`${url.origin}${url.pathname}?limit=${lim it}`);
Это позволяет строить гибкие модули, зависящие от конфигурации импорта.
Esbuild позволяет создавать модули, которые не существуют в файловой системе. В контексте внешних данных это особенно важно.
build.onResolve({ filter: /^virtual-api:/ }, (args) => {
return {
path: args.path,
namespace: "virtual-api"
};
});
build.onLoad({ filter: /.*/, namespace: "virtual-api" }, async (args) => {
const endpoint = args.path.replace("virtual-api:", "");
const res = await fetch(`https://api.example.com/${endpoint}`);
const data = await res.json();
return {
contents: `export default ${JSON.stringify(data)}`,
loader: "js"
};
});
Такая схема позволяет скрыть реальные URL и создать слой абстракции над API.
При больших объёмах данных полезно использовать потоковое чтение:
const response = await fetch(url);
const reader = response.body.getReader();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += new TextDecoder().decode(value);
}
После сборки потока данные могут быть преобразованы и сериализованы в модуль.
При работе с внешними источниками критично учитывать влияние на скорость сборки.
Основные оптимизации:
Пример ограничения параллелизма:
let active = 0;
const queue = [];
async function limitedFetch(url) {
if (active >= 5) {
await new Promise(resolve => queue.push(resolve));
}
active++;
try {
return await fetch(url);
} finally {
active--;
queue.shift()?.();
}
}
Работа с удалёнными данными требует строгого контроля:
Фильтрация доменов:
const allowedHosts = new Set(["api.example.com"]);
if (!allowedHosts.has(url.hostname)) {
throw new Error("Domain not allowed");
}
Внешние данные становятся частью графа зависимостей, поэтому важно учитывать:
Esbuild рассматривает результат onLoad как полноценный
модуль, поэтому любые изменения содержимого влияют на граф сборки и
могут инициировать пересборку зависимостей.
Внешние данные часто требуют дополнительной обработки перед экспортом:
const transformed = data.map(item => ({
id: item.id,
name: item.name.toUpperCase()
}));
return {
contents: `export default ${JSON.stringify(transformed)}`,
loader: "js"
};
Такой подход позволяет внедрять бизнес-логику прямо на этапе сборки, снижая нагрузку на runtime.
Плагин может агрегировать данные из разных API:
const [usersRes, postsRes] = await Promise.all([
fetch("https://api.example.com/users"),
fetch("https://api.example.com/posts")
]);
const users = await usersRes.json();
const posts = await postsRes.json();
return {
contents: `
export const users = ${JSON.stringify(users)};
export const posts = ${JSON.stringify(posts)};
`,
loader: "js"
};
Так формируется единый модуль, объединяющий несколько источников данных.
При росте функциональности плагин разделяется на слои:
Такая архитектура снижает связность и упрощает поддержку при увеличении количества внешних источников.