Хук onResolve: перехват разрешения путей

В системе плагинов esbuild этап разрешения импортируемых путей отделён от этапа загрузки содержимого модулей. За этот этап отвечает хук onResolve, который перехватывает каждый импорт, require, динамический import() и позволяет изменить то, как путь будет интерпретирован сборщиком.

Основная роль onResolve — преобразование входного пути модуля в формально разрешённый путь с указанием namespace, который далее будет использован в onLoad. Этот хук фактически управляет графом модулей.


Сигнатура и базовая структура

Плагин в esbuild описывается через функцию setup, внутри которой регистрируются обработчики:

const plugin = {
  name: 'example-plugin',
  setup(build) {
    build.onResolve({ filter: /.*/ }, (args) => {
      return {
        path: args.path,
      };
    });
  }
};

Ключевые элементы:

  • filter — регулярное выражение, определяющее, какие импорты перехватываются
  • callback (args) => result — логика резолва
  • возвращаемое значение — объект с инструкциями для esbuild

Объект args: контекст разрешения

Каждый вызов onResolve получает контекст, описывающий конкретный импорт:

Основные поля args

  • path — исходный импортируемый путь ("./utils", "react", "fs" и т.д.)

  • importer — модуль, из которого выполняется импорт

  • namespace — namespace импортера (по умолчанию file)

  • resolveDir — директория, относительно которой выполняется разрешение

  • kind — тип импорта:

    • import-statement
    • require-call
    • dynamic-import
    • entry-point
  • pluginData — данные, переданные предыдущими плагинами

Пример анализа контекста

build.onResolve({ filter: /\.css$/ }, (args) => {
  console.log(args.importer);
  console.log(args.resolveDir);
  return null;
});

Результат работы onResolve

Возвращаемый объект управляет дальнейшей обработкой модуля.

Основные поля результата

path

Финальный путь или идентификатор ресурса:

return {
  path: '/absolute/path/to/file.css'
};

namespace

Позволяет перенаправить модуль в другой обработчик onLoad:

return {
  path: args.path,
  namespace: 'custom'
};

Namespace используется как логический канал маршрутизации модулей.


external

Поле, исключающее модуль из бандла:

return {
  path: args.path,
  external: true
};

Используется для:

  • системных модулей (fs, path)
  • CDN-импортов
  • peer dependencies

sideEffects

Позволяет управлять tree-shaking на уровне конкретного импорта:

return {
  path: args.path,
  sideEffects: false
};

pluginData

Передача данных следующему этапу (onLoad):

return {
  path: args.path,
  pluginData: {
    transformed: true
  }
};

Принцип работы цепочки резолва

Каждый импорт проходит через последовательность:

  1. onResolve (фильтрация и преобразование пути)
  2. выбор namespace
  3. onLoad (загрузка содержимого)
  4. трансформация кода

Если несколько плагинов перехватывают один и тот же путь, порядок определяется регистрацией: первый вернувший результат «побеждает», если не используется явное продолжение через onResolveResult.


Управление приоритетами

Фильтрация через filter

Регулярное выражение определяет область применения:

build.onResolve({ filter: /^https?:\/\// }, (args) => {
  return {
    path: args.path,
    namespace: 'http'
  };
});

Приоритет задаётся порядком регистрации:

  • более ранние плагины имеют приоритет
  • более специфичные фильтры обычно располагаются выше

Продвинутое управление путями

Перенаправление модулей

Типичный кейс — подмена импортов:

build.onResolve({ filter: /^lodash$/ }, () => {
  return {
    path: 'lodash-es'
  };
});

Это позволяет:

  • заменять библиотеки
  • внедрять совместимые реализации
  • оптимизировать бандл

Алиасинг путей

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

Условное разрешение

Можно учитывать контекст:

build.onResolve({ filter: /.*/ }, (args) => {
  if (args.importer.includes('node_modules')) {
    return {
      path: args.path,
      external: true
    };
  }
});

Namespace как механизм маршрутизации

Namespace играет ключевую роль в архитектуре плагинов.

Стандартные namespace

  • file — обычные файлы
  • external — исключённые зависимости

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

build.onResolve({ filter: /\.md$/ }, (args) => {
  return {
    path: args.path,
    namespace: 'markdown'
  };
});

Далее:

build.onLoad({ filter: /.*/, namespace: 'markdown' }, () => {
  return {
    contents: 'export default "parsed markdown";',
    loader: 'js'
  };
});

Передача состояния между плагинами

pluginData используется как механизм межэтапной коммуникации:

Пример маркировки

build.onResolve({ filter: /\.svg$/ }, (args) => {
  return {
    path: args.path,
    pluginData: {
      optimize: true
    }
  };
});

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

build.onLoad({ filter: /.*/, namespace: 'file' }, (args) => {
  if (args.pluginData?.optimize) {
    // оптимизация SVG
  }

  return {
    contents: '...',
    loader: 'text'
  };
});

Ошибки и контроль резолва

onResolve может сигнализировать об ошибке:

build.onResolve({ filter: /\.secret$/ }, (args) => {
  return {
    errors: [{
      text: 'Доступ к секретным файлам запрещён',
      location: null
    }]
  };
});

Также возможно возвращать warnings.


Пропуск обработки

Возврат null означает:

  • передать управление следующему плагину
  • или использовать встроенный резолвер esbuild
build.onResolve({ filter: /.*/ }, () => {
  return null;
});

Практика построения системы резолва

1. HTTP импорты

build.onResolve({ filter: /^https:\/\// }, (args) => {
  return {
    path: args.path,
    namespace: 'http'
  };
});

2. Локальные алиасы

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

3. Внешние зависимости

build.onResolve({ filter: /^[a-z].*/ }, (args) => {
  if (args.path.startsWith('node:')) {
    return { path: args.path, external: true };
  }
});

Типовые архитектуры использования

Прокси-резолвер

  • перехватывает все пути
  • нормализует алиасы
  • маршрутизирует внешние зависимости

Изолированный namespace-пайплайн

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

Многоуровневая трансформация

  • первый onResolve — нормализация
  • второй — оптимизация маршрута
  • третий — безопасность/валидация

Поведение при конфликте плагинов

Если несколько onResolve подходят под один импорт:

  • выполняются в порядке регистрации
  • первый вернувший результат завершает цепочку
  • последующие не вызываются, если нет явной передачи управления

Это делает порядок регистрации критическим фактором архитектуры.


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

onResolve определяет:

  • путь ресурса
  • namespace
  • дополнительные метаданные

onLoad затем использует эти данные для:

  • чтения файлов
  • генерации кода
  • трансформации содержимого

Связка этих двух хуков формирует полный цикл обработки модуля в esbuild.