Namespace: виртуальные модули

В системе плагинов esbuild пространство имён (namespace) используется как механизм изоляции и маршрутизации модулей на этапе разрешения и загрузки. Оно позволяет разделять источники модулей по логическим каналам, не привязываясь к файловой системе. Виртуальные модули строятся именно на этой основе: они существуют только в памяти сборщика и никогда не появляются как реальные файлы на диске.

Namespace задаётся на уровне результатов хуков onResolve и onLoad. Каждый модуль, проходящий через систему плагинов, получает метку пространства имён, которая влияет на дальнейшие шаги резолва, загрузки и трансформации.


Базовая модель namespace в esbuild

Внутри esbuild модуль описывается не только путём, но и контекстом:

  • path — строка идентификатора модуля
  • namespace — область интерпретации этого идентификатора

Обычные файлы используют namespace по умолчанию — file. Однако плагины могут вводить любые собственные пространства имён:

  • virtual
  • http
  • mem
  • css-module
  • embedded

Каждое пространство имён становится независимой “вселенной” для модулей.

Ключевая идея: два модуля с одинаковым path, но разным namespace, считаются разными сущностями.


Механика создания виртуального модуля

Виртуальный модуль создаётся через связку onResolveonLoad.

1. Перехват импорта

onResolve({ filter: /^virtual:/ }, args => {
  return {
    path: args.path,
    namespace: "virtual"
  }
})

Здесь любой импорт вида:

import data from "virtual:config"

перенаправляется в пространство имён virtual.


2. Загрузка содержимого

onLoad({ filter: /.*/, namespace: "virtual" }, args => {
  return {
    contents: `export default { mode: "memory" }`,
    loader: "js"
  }
})

На этом этапе esbuild не обращается к файловой системе. Модуль полностью формируется из строки, функции или вычисленного результата.


Поток обработки виртуального модуля

Внутренний конвейер esbuild при работе с namespace выглядит следующим образом:

  1. onResolve определяет namespace и новый путь
  2. модуль помещается в граф зависимостей
  3. при необходимости вызывается onLoad для соответствующего namespace
  4. содержимое преобразуется loader-ом
  5. модуль кэшируется внутри сборочного контекста

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


Разделение логики через namespace

Namespace позволяет разделять источники данных без конфликтов.

Пример: HTTP-источник

onResolve({ filter: /^https:/ }, args => {
  return {
    path: args.path,
    namespace: "http"
  }
})

onLoad({ filter: /.*/, namespace: "http" }, async (args) => {
  const res = await fetch(args.path)
  const text = await res.text()

  return {
    contents: text,
    loader: "js"
  }
})

Здесь каждый URL становится модулем, но не пересекается с файловыми путями.


Виртуальные конфигурации и генерация кода

Одно из типичных применений namespace — генерация конфигурационных модулей.

onResolve({ filter: /^app:config$/ }, () => ({
  path: "app:config",
  namespace: "virtual-config"
}))
onLoad({ filter: /.*/, namespace: "virtual-config" }, () => {
  const config = {
    debug: true,
    version: "1.0.0",
    features: ["auth", "cache"]
  }

  return {
    contents: `export default ${JSON.stringify(config)}`,
    loader: "js"
  }
})

Такой модуль ведёт себя как обычный ES-модуль:

import config from "app:config"

но физически не существует.


Изоляция и предотвращение конфликтов

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

Без namespace:

  • разные плагины могут перехватывать одинаковые пути
  • возможны конфликты onLoad
  • теряется контроль над источником модуля

С namespace:

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

Кэширование и стабильность идентичности

esbuild кэширует модули по паре:

(path, namespace)

Это означает:

  • одинаковый path в разных namespace не пересекается
  • повторный onLoad может не вызываться при идентичном запросе
  • виртуальные модули могут вести себя как стабильные единицы графа

Namespace как механизм расширения типов модулей

Виртуальные модули часто используются для создания новых “типов” импорта:

JSON через виртуальный слой

onResolve({ filter: /\.config$/ }, args => ({
  path: args.path,
  namespace: "config"
}))

onLoad({ filter: /.*/, namespace: "config" }, args => {
  const data = { parsed: true }

  return {
    contents: `export default ${JSON.stringify(data)}`,
    loader: "js"
  }
})

SVG как JS-модуль

onResolve({ filter: /\.svg$/ }, args => ({
  path: args.path,
  namespace: "svg"
}))

onLoad({ filter: /.*/, namespace: "svg" }, () => {
  return {
    contents: `export default "<svg></svg>"`,
    loader: "text"
  }
})

Здесь namespace превращает статический ресурс в программный интерфейс.


Динамическая генерация модулей

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

onResolve({ filter: /^gen:/ }, args => ({
  path: args.path,
  namespace: "generated"
}))

onLoad({ filter: /.*/, namespace: "generated" }, (args) => {
  const value = args.path.replace("gen:", "").toUpperCase()

  return {
    contents: `export const value = "${value}"`,
    loader: "js"
  }
})

Импорт:

import { value } from "gen:hello"

Результат:

export const value = "HELLO"

Сетевые и удалённые модули через namespace

Namespace часто используется для реализации удалённых источников:

  • CDN-модули
  • API-ответы как код
  • удалённые конфигурации

Принцип одинаков: onResolve маркирует источник, onLoad извлекает содержимое.


Взаимодействие с loader и transform pipeline

Namespace не заменяет loader, а определяет контекст загрузки.

Порядок обработки:

  1. namespace определяет обработчик onLoad

  2. contents передаётся в loader

  3. loader определяет тип обработки:

    • js
    • ts
    • text
    • json
  4. результат попадает в трансформационный пайплайн esbuild

Таким образом, namespace — это верхний уровень маршрутизации, а loader — нижний уровень интерпретации.


Переиспользование namespace для оптимизации

При проектировании плагинов namespace используется для:

  • разделения кешей разных источников
  • уменьшения количества проверок в filter
  • ускорения резолва за счёт точечных совпадений
  • изоляции тяжёлых трансформаций

Плагин с корректно выбранными namespace обычно требует меньше условий в onLoad, поскольку сам namespace уже выполняет первичную классификацию.


Композиция нескольких namespace в одном плагине

Один плагин может обслуживать несколько пространств имён:

onLoad({ namespace: "http" }, handlerA)
onLoad({ namespace: "virtual" }, handlerB)
onLoad({ namespace: "generated" }, handlerC)

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


Влияние namespace на граф зависимостей

Граф зависимостей esbuild строится с учётом namespace как части идентификатора узла. Это влияет на:

  • дедупликацию модулей
  • порядок обхода графа
  • инвалидацию кеша
  • повторное использование модулей в разных контекстах

Модуль с одинаковым путём, но другим namespace, всегда формирует отдельную ветку графа.


Ограничения и особенности использования

Namespace не является механизмом безопасности или sandbox:

  • он не изолирует выполнение кода
  • не ограничивает доступ к API
  • не влияет на runtime поведение JS

Его роль строго сборочная:

  • маршрутизация
  • классификация
  • управление загрузкой
  • разделение контекстов

Также namespace не предназначен для хранения больших данных; он лишь указывает, откуда и как их получать.


Архитектурная роль виртуальных модулей

Виртуальные модули на базе namespace формируют слой абстракции между:

  • источниками данных (файлы, сеть, память)
  • графом зависимостей esbuild
  • финальным бандлом

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