Перечень встроенных namespaces

Система namespaces в Esbuild представляет собой механизм логического разделения путей модулей. Каждый импортируемый ресурс в процессе сборки принадлежит определённому пространству имён (namespace), которое определяет способ его обработки, разрешения и загрузки.

Namespaces особенно важны при разработке собственных плагинов, поскольку позволяют создавать виртуальные модули, подключать данные из нестандартных источников и контролировать процесс разрешения импортов на разных этапах сборки.

Каждый модуль в графе зависимостей имеет два основных атрибута:

  • путь (path);
  • пространство имён (namespace).

Один и тот же путь может интерпретироваться по-разному в зависимости от используемого namespace.


Назначение namespaces

Рассмотрим обычный импорт:

import { sum } from './math.js'

После обработки Esbuild путь ./math.js попадает в стандартный namespace файловой системы.

Концептуально информация выглядит следующим образом:

{
  path: '/project/src/math.js',
  namespace: 'file'
}

Если же модуль создаётся плагином, его данные могут храниться не в файловой системе, а, например:

  • в памяти;
  • в базе данных;
  • в сети;
  • в сгенерированном коде;
  • в конфигурационном объекте.

В таких случаях используются другие namespaces.


Стандартный namespace file

Назначение

file является основным встроенным namespace.

Практически все модули проекта после разрешения путей попадают именно сюда.

Пример:

import './styles.css'
import './app.js'
import logo from './logo.png'

Все перечисленные файлы будут обрабатываться в namespace:

file

Внутри Esbuild именно этот namespace используется для:

  • чтения файлов с диска;
  • определения расширений;
  • применения загрузчиков (loader);
  • отслеживания изменений в режиме watch.

Использование в плагинах

Во многих случаях плагин намеренно возвращает namespace file.

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

build.onResolve({ filter: /^@src\// }, args => {
  return {
    path: args.path.replace('@src/', '/project/src/'),
    namespace: 'file'
  }
})

После завершения обработки модуль продолжит работу как обычный файл.


Namespace internal

Общая характеристика

internal используется самим Esbuild для внутренних операций.

Этот namespace не предназначен для прямого использования пользовательским кодом.

Он применяется движком сборщика для:

  • служебных импортов;
  • внутренних зависимостей;
  • промежуточных структур графа модулей.

Особенности

Попытки создавать собственные модули в namespace internal считаются плохой практикой:

return {
  path: 'virtual-module',
  namespace: 'internal'
}

Такой код может привести к конфликтам с внутренними механизмами Esbuild.

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


Namespace empty

Назначение

empty представляет специальное пространство имён для модулей без содержимого.

Подобный модуль фактически существует, но не содержит кода.

Пример результата:

export {}

Практическое применение

Иногда требуется исключить импорт из итоговой сборки.

Например, библиотека пытается загрузить модуль, который не нужен в браузере:

import 'fs'

Плагин может заменить его пустым модулем:

build.onResolve({ filter: /^fs$/ }, () => {
  return {
    path: 'fs',
    namespace: 'empty'
  }
})

В результате импорт будет удовлетворён, но код не попадёт в bundle.


Преимущества

Использование empty позволяет:

  • отключать ненужные зависимости;
  • устранять несовместимые модули;
  • уменьшать размер сборки;
  • избегать ошибок выполнения.

Пользовательские namespaces

Хотя встроенных пространств имён немного, архитектура 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'
    }
  }
)

Здесь создаётся полностью виртуальный модуль.


Взаимодействие namespace и onResolve

Метод onResolve отвечает за определение дальнейшей судьбы импорта.

Именно на этом этапе чаще всего назначается namespace.

Исходный импорт:

import config from 'config'

Обработчик:

build.onResolve(
  { filter: /^config$/ },
  () => ({
    path: 'config',
    namespace: 'config-ns'
  })
)

После выполнения Esbuild сохраняет:

{
  path: 'config',
  namespace: 'config-ns'
}

Далее модуль будет передан соответствующему обработчику onLoad.


Взаимодействие namespace и onLoad

После завершения этапа разрешения путей Esbuild ищет обработчик загрузки.

Поиск выполняется по двум критериям:

  1. соответствие пути фильтру;
  2. соответствие namespace.

Пример:

build.onLoad(
  {
    filter: /.*/,
    namespace: 'config-ns'
  },
  () => ({
    contents: `
      export default {
        debug: true
      }
    `,
    loader: 'js'
  })
)

Если namespace не совпадает, обработчик не будет вызван.


Изоляция обработчиков через namespaces

Одной из главных задач namespaces является предотвращение конфликтов между плагинами.

Предположим, существует два плагина:

namespace: 'images'

и

namespace: 'markdown'

Даже если пути совпадают:

content

Esbuild рассматривает их как разные сущности:

{
  path: 'content',
  namespace: 'images'
}

и

{
  path: 'content',
  namespace: 'markdown'
}

Конфликтов не возникает.


Namespace как источник данных

Фактически namespace определяет происхождение модуля.

Часто используются следующие соглашения:

Namespace Источник
file файловая система
http сеть
markdown markdown-документы
virtual виртуальные модули
env переменные окружения
graphql GraphQL-схемы
yaml YAML-конфигурации

Большинство таких пространств имён создаётся плагинами.


Пример namespace для HTTP-импортов

Иногда требуется загружать модули непосредственно из сети.

Обработчик разрешения:

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'
    }
  }
)

Все сетевые ресурсы теперь будут обрабатываться независимо от файловой системы.


Передача импортов между namespaces

Namespace может изменяться несколько раз в процессе разрешения зависимостей.

Например:

import data from 'remote-config'

Первый обработчик:

return {
  path: 'https://server/config.js',
  namespace: 'http'
}

После загрузки этот модуль может содержать новые импорты:

import helper from './helper.js'

Для них может использоваться уже другой namespace.

Таким образом формируется сложный граф зависимостей, включающий различные источники данных.


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-файл.


Проверка namespace внутри плагина

Объект аргументов обработчика содержит информацию о текущем пространстве имён.

Пример:

build.onResolve(
  { filter: /.*/ },
  args => {
    console.log(args.namespace)

    return null
  }
)

Возможный вывод:

file

или

virtual

или

http

Это позволяет строить сложную логику обработки в зависимости от происхождения модуля.


Лучшие практики использования namespaces

Использование уникальных имён

Предпочтительно выбирать специфические названия:

namespace: 'my-plugin-assets'

вместо:

namespace: 'assets'

Это уменьшает вероятность конфликтов между сторонними плагинами.


Разделение разных типов данных

Нежелательно помещать все виртуальные сущности в одно пространство имён:

namespace: 'virtual'

Лучше разделять:

namespace: 'virtual-css'
namespace: 'virtual-env'
namespace: 'virtual-icons'

Так код становится более поддерживаемым.


Возврат в file при необходимости

Если после обработки требуется стандартное поведение Esbuild, следует возвращать namespace:

file

Пример:

return {
  path: resolvedPath,
  namespace: 'file'
}

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


Сводная таблица встроенных namespaces

Namespace Назначение
file Работа с файлами файловой системы
internal Внутренние механизмы Esbuild
empty Пустые модули без содержимого

Несмотря на небольшой набор встроенных пространств имён, система namespaces является одним из фундаментальных механизмов архитектуры Esbuild. Она обеспечивает изоляцию источников данных, связывает этапы onResolve и onLoad, позволяет создавать виртуальные модули и служит основой для построения сложных плагинов и нестандартных схем загрузки ресурсов.