Хук resolveDynamicImport

В системе плагинов Rollup хук resolveDynamicImport отвечает за разрешение динамических импортов, создаваемых через конструкцию import(). Он используется в тех случаях, когда необходимо явно контролировать процесс определения модуля, загружаемого динамически, и вмешиваться в стандартный алгоритм разрешения зависимостей.

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


Общая сигнатура и поведение

Хук resolveDynamicImport вызывается для каждого динамического выражения import(...), которое Rollup обнаруживает во время анализа исходного кода.

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

resolveDynamicImport(specifier, importer) {
  return null;
}

Параметры:

  • specifier — выражение, переданное в import(). Может быть строкой, шаблонной строкой или сложным выражением.
  • importer — путь модуля, в котором встречается динамический импорт.

Возвращаемое значение может принимать несколько форм:

  • null — использовать стандартное поведение Rollup
  • string — заменить путь модуля
  • { id, external } — явное указание идентификатора модуля и его внешнего статуса
  • false — полностью отменить обработку импорта
  • Promise с одним из указанных значений — асинхронное разрешение

Место в жизненном цикле сборки

Хук resolveDynamicImport вызывается после анализа синтаксического дерева модуля, но до формирования окончательного графа зависимостей. Он находится между стадиями:

  1. Парсинг модуля
  2. Обнаружение import() выражений
  3. Разрешение динамического импорта через resolveDynamicImport
  4. Добавление зависимости в граф

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


Отличие от resolveId

Ключевое различие между resolveId и resolveDynamicImport заключается в типе импортов:

  • resolveId обрабатывает:

    • import x from 'module'
    • export * from 'module'
  • resolveDynamicImport обрабатывает:

    • import('module')
    • import(variable)
    • import(./${name}.js)

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


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

Перенаправление динамического импорта

export default function plugin() {
  return {
    name: 'dynamic-import-rewrite',

    resolveDynamicImport(specifier) {
      if (specifier === 'legacy-module') {
        return 'modern-module';
      }
      return null;
    }
  };
}

В данном случае любой динамический импорт legacy-module будет заменён на modern-module.


Маркировка внешних зависимостей

resolveDynamicImport(specifier) {
  if (specifier.startsWith('http')) {
    return { id: specifier, external: true };
  }
  return null;
}

Такой подход используется для CDN-загрузки модулей или микрофронтенд-архитектур, где часть зависимостей не включается в бандл.


Асинхронное разрешение

resolveDynamicImport(specifier) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`resolved/${specifier}`);
    }, 10);
  });
}

Асинхронный вариант полезен при обращении к файловой системе, метаданным или внешним сервисам.


Обработка шаблонных строк и выражений

Динамический импорт часто используется с шаблонными строками:

import(`./modules/${name}.js`);

В таких случаях specifier может быть не строкой, а выражением AST. Rollup передаёт структуру, которую плагин может анализировать или частично интерпретировать.

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

  • onDynamicImport (внутренний анализ AST)
  • совместное использование с transform

Ограничения и особенности

Невозможность полной статической оптимизации

Если динамический импорт содержит переменную:

import(moduleName);

Rollup не может определить зависимость заранее. В этом случае resolveDynamicImport становится единственной точкой контроля.


Потенциальная потеря tree-shaking

Неправильная обработка динамических импортов может привести к:

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

Влияние на code splitting

Динамические импорты напрямую влияют на разбиение кода на чанки. Каждый разрешённый import() потенциально создаёт отдельный chunk:

  • статически известный путь → предсказуемый chunk
  • изменённый через плагин путь → новый chunk
  • external: true → исключение из сборки

Типовые сценарии применения

Алиасинг динамических модулей

Используется для перенаправления импортов в зависимости от окружения:

resolveDynamicImport(specifier) {
  if (specifier.includes('env=prod')) {
    return specifier.replace('env=prod', 'env=production');
  }
}

Интеграция с виртуальными модулями

При работе с виртуальными модулями (virtual: namespace):

resolveDynamicImport(specifier) {
  if (specifier.startsWith('virtual:')) {
    return specifier;
  }
}

Поддержка CDN и удалённых модулей

resolveDynamicImport(specifier) {
  if (specifier.startsWith('cdn:')) {
    return {
      id: `https://cdn.example.com/${specifier.slice(4)}.js`,
      external: true
    };
  }
}

Взаимодействие с другими хуками

resolveDynamicImport часто используется совместно с:

  • resolveId — для статических импортов
  • load — для подмены содержимого модулей
  • transform — для анализа AST перед сборкой
  • shouldTransformCachedModule — для оптимизации повторных сборок

Типичный поток обработки:

  1. resolveId обрабатывает статические зависимости
  2. resolveDynamicImport обрабатывает runtime зависимости
  3. load загружает содержимое
  4. transform модифицирует код
  5. формируется финальный граф

Практические нюансы реализации

Проверка типа specifier

В реальных плагинах важно учитывать, что specifier может быть:

  • строкой
  • объектом AST
  • null (в некоторых edge-case сценариях)

Поэтому безопасная обработка включает проверки:

if (typeof specifier !== 'string') {
  return null;
}

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

Если несколько плагинов реализуют resolveDynamicImport, их порядок имеет значение. Rollup применяет их последовательно до первого не-null результата, что позволяет:

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

Совместимость с ESM и CommonJS

Динамические импорты поддерживаются только в ESM-контексте. При взаимодействии с CommonJS:

  • Rollup может оборачивать модуль
  • динамический импорт преобразуется в runtime-обертку
  • resolveDynamicImport всё равно вызывается на этапе анализа

Роль в архитектуре Rollup

resolveDynamicImport является ключевым механизмом контроля runtime-зависимостей. В отличие от статических импортов, он позволяет управлять поведением приложения в момент исполнения, а не только компиляции.

Его использование критично в системах:

  • микрофронтендов
  • модульных CDN
  • ленивой загрузки компонентов
  • плагинных архитектур

Он расширяет модель Rollup от статического анализа к частично динамическому управлению графом модулей, сохраняя при этом предсказуемость сборки.