Система namespaces в Esbuild представляет собой механизм логического разделения путей модулей. Каждый импортируемый ресурс в процессе сборки принадлежит определённому пространству имён (namespace), которое определяет способ его обработки, разрешения и загрузки.
Namespaces особенно важны при разработке собственных плагинов, поскольку позволяют создавать виртуальные модули, подключать данные из нестандартных источников и контролировать процесс разрешения импортов на разных этапах сборки.
Каждый модуль в графе зависимостей имеет два основных атрибута:
path);namespace).Один и тот же путь может интерпретироваться по-разному в зависимости от используемого namespace.
Рассмотрим обычный импорт:
import { sum } from './math.js'
После обработки Esbuild путь ./math.js попадает в
стандартный namespace файловой системы.
Концептуально информация выглядит следующим образом:
{
path: '/project/src/math.js',
namespace: 'file'
}
Если же модуль создаётся плагином, его данные могут храниться не в файловой системе, а, например:
В таких случаях используются другие namespaces.
file является основным встроенным namespace.
Практически все модули проекта после разрешения путей попадают именно сюда.
Пример:
import './styles.css'
import './app.js'
import logo from './logo.png'
Все перечисленные файлы будут обрабатываться в namespace:
file
Внутри Esbuild именно этот namespace используется для:
loader);Во многих случаях плагин намеренно возвращает namespace
file.
Пример обработчика:
build.onResolve({ filter: /^@src\// }, args => {
return {
path: args.path.replace('@src/', '/project/src/'),
namespace: 'file'
}
})
После завершения обработки модуль продолжит работу как обычный файл.
internal используется самим Esbuild для внутренних
операций.
Этот namespace не предназначен для прямого использования пользовательским кодом.
Он применяется движком сборщика для:
Попытки создавать собственные модули в namespace
internal считаются плохой практикой:
return {
path: 'virtual-module',
namespace: 'internal'
}
Такой код может привести к конфликтам с внутренними механизмами Esbuild.
Для пользовательских решений рекомендуется создавать собственные пространства имён.
empty представляет специальное пространство имён для
модулей без содержимого.
Подобный модуль фактически существует, но не содержит кода.
Пример результата:
export {}
Иногда требуется исключить импорт из итоговой сборки.
Например, библиотека пытается загрузить модуль, который не нужен в браузере:
import 'fs'
Плагин может заменить его пустым модулем:
build.onResolve({ filter: /^fs$/ }, () => {
return {
path: 'fs',
namespace: 'empty'
}
})
В результате импорт будет удовлетворён, но код не попадёт в bundle.
Использование empty позволяет:
Хотя встроенных пространств имён немного, архитектура Esbuild предполагает активное использование собственных namespaces.
Пример:
build.onResolve({ filter: /^env$/ }, () => {
return {
path: 'env',
namespace: 'env-ns'
}
})
Затем происходит загрузка:
build.onLoad(
{ filter: /.*/, namespace: 'env-ns' },
() => {
return {
contents: `
export const API_URL = "https://api.site.com"
`,
loader: 'js'
}
}
)
Здесь создаётся полностью виртуальный модуль.
Метод onResolve отвечает за определение дальнейшей
судьбы импорта.
Именно на этом этапе чаще всего назначается namespace.
Исходный импорт:
import config from 'config'
Обработчик:
build.onResolve(
{ filter: /^config$/ },
() => ({
path: 'config',
namespace: 'config-ns'
})
)
После выполнения Esbuild сохраняет:
{
path: 'config',
namespace: 'config-ns'
}
Далее модуль будет передан соответствующему обработчику
onLoad.
После завершения этапа разрешения путей Esbuild ищет обработчик загрузки.
Поиск выполняется по двум критериям:
Пример:
build.onLoad(
{
filter: /.*/,
namespace: 'config-ns'
},
() => ({
contents: `
export default {
debug: true
}
`,
loader: 'js'
})
)
Если namespace не совпадает, обработчик не будет вызван.
Одной из главных задач namespaces является предотвращение конфликтов между плагинами.
Предположим, существует два плагина:
namespace: 'images'
и
namespace: 'markdown'
Даже если пути совпадают:
content
Esbuild рассматривает их как разные сущности:
{
path: 'content',
namespace: 'images'
}
и
{
path: 'content',
namespace: 'markdown'
}
Конфликтов не возникает.
Фактически namespace определяет происхождение модуля.
Часто используются следующие соглашения:
| Namespace | Источник |
|---|---|
| file | файловая система |
| http | сеть |
| markdown | markdown-документы |
| virtual | виртуальные модули |
| env | переменные окружения |
| graphql | GraphQL-схемы |
| yaml | YAML-конфигурации |
Большинство таких пространств имён создаётся плагинами.
Иногда требуется загружать модули непосредственно из сети.
Обработчик разрешения:
build.onResolve(
{ filter: /^https?:\/\// },
args => ({
path: args.path,
namespace: 'http'
})
)
Обработчик загрузки:
build.onLoad(
{
filter: /.*/,
namespace: 'http'
},
async args => {
const response = await fetch(args.path)
return {
contents: await response.text(),
loader: 'js'
}
}
)
Все сетевые ресурсы теперь будут обрабатываться независимо от файловой системы.
Namespace может изменяться несколько раз в процессе разрешения зависимостей.
Например:
import data from 'remote-config'
Первый обработчик:
return {
path: 'https://server/config.js',
namespace: 'http'
}
После загрузки этот модуль может содержать новые импорты:
import helper from './helper.js'
Для них может использоваться уже другой namespace.
Таким образом формируется сложный граф зависимостей, включающий различные источники данных.
Одно из наиболее распространённых применений namespaces связано с виртуальными модулями.
Пример:
import buildInfo from 'build-info'
Разрешение:
build.onResolve(
{ filter: /^build-info$/ },
() => ({
path: 'build-info',
namespace: 'virtual'
})
)
Загрузка:
build.onLoad(
{
filter: /.*/,
namespace: 'virtual'
},
() => ({
contents: `
export default {
version: "1.0.0",
timestamp: "${Date.now()}"
}
`,
loader: 'js'
})
)
Такой модуль никогда не существует на диске, но выглядит для приложения как обычный JavaScript-файл.
Объект аргументов обработчика содержит информацию о текущем пространстве имён.
Пример:
build.onResolve(
{ filter: /.*/ },
args => {
console.log(args.namespace)
return null
}
)
Возможный вывод:
file
или
virtual
или
http
Это позволяет строить сложную логику обработки в зависимости от происхождения модуля.
Предпочтительно выбирать специфические названия:
namespace: 'my-plugin-assets'
вместо:
namespace: 'assets'
Это уменьшает вероятность конфликтов между сторонними плагинами.
Нежелательно помещать все виртуальные сущности в одно пространство имён:
namespace: 'virtual'
Лучше разделять:
namespace: 'virtual-css'
namespace: 'virtual-env'
namespace: 'virtual-icons'
Так код становится более поддерживаемым.
Если после обработки требуется стандартное поведение Esbuild, следует возвращать namespace:
file
Пример:
return {
path: resolvedPath,
namespace: 'file'
}
Это позволяет использовать встроенную систему загрузчиков без дополнительных действий.
| Namespace | Назначение |
|---|---|
file |
Работа с файлами файловой системы |
internal |
Внутренние механизмы Esbuild |
empty |
Пустые модули без содержимого |
Несмотря на небольшой набор встроенных пространств имён, система
namespaces является одним из фундаментальных механизмов архитектуры
Esbuild. Она обеспечивает изоляцию источников данных, связывает этапы
onResolve и onLoad, позволяет создавать
виртуальные модули и служит основой для построения сложных плагинов и
нестандартных схем загрузки ресурсов.