Виртуальные модули

Виртуальные модули в Rollup представляют собой механизм генерации модулей «на лету» без наличия физического файла на диске. Они создаются и обслуживаются плагином через хуки resolveId и load, позволяя внедрять синтетические зависимости прямо в граф сборки.

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

Базовый принцип работы виртуальных модулей

Виртуальный модуль существует только в памяти сборщика. Его жизненный цикл полностью контролируется плагином:

  • идентификатор модуля создаётся искусственно;
  • resolveId сообщает Rollup, что модуль существует;
  • load возвращает исходный код;
  • дальше модуль участвует в обычной цепочке трансформаций.

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

export default function virtualPlugin() {
  const VIRTUAL_ID = 'virtual:example-module';

  return {
    name: 'virtual-module',

    resolveId(source) {
      if (source === VIRTUAL_ID) {
        return VIRTUAL_ID;
      }
      return null;
    },

    load(id) {
      if (id === VIRTUAL_ID) {
        return `
          export const message = 'Hello from virtual module';
        `;
      }
      return null;
    }
  };
}

После подключения такого плагина импорт virtual:example-module начинает работать как обычный ES-модуль.

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

В Rollup нет строгого стандарта именования виртуальных модулей, но на практике используются несколько подходов:

Префикс virtual:

Наиболее распространённый вариант — использование пространственного префикса:

  • virtual:config
  • virtual:env
  • virtual:runtime

Такой подход снижает вероятность конфликта с реальными файлами.

Служебный префикс \0

Rollup использует нулевой символ (\0) для внутренних модулей. Плагины часто применяют его для скрытия виртуальных зависимостей:

const VIRTUAL_ID = '\0my-virtual-module';

Преимущество подхода — гарантия отсутствия пересечений с пользовательскими путями, так как файловая система не допускает такой символ в именах файлов.

Параметризованные идентификаторы

Виртуальные модули могут включать параметры:

  • virtual:data?type=json
  • virtual:component?name=Button

В этом случае resolveId выполняет парсинг строки и нормализацию идентификатора.

Генерация кода в load-хуке

Основная логика виртуального модуля сосредоточена в load. Он возвращает строку исходного кода, который затем проходит через стандартный пайплайн Rollup.

Пример динамической генерации:

export default function envPlugin(options = {}) {
  const id = 'virtual:env';

  return {
    name: 'env-plugin',

    resolveId(source) {
      if (source === id) return id;
      return null;
    },

    load(sourceId) {
      if (sourceId === id) {
        const env = options.env || {};

        return `
          export const env = ${JSON.stringify(env)};
        `;
      }
      return null;
    }
  };
}

Такой модуль позволяет инкапсулировать конфигурацию окружения без отдельного JSON-файла.

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

Хук resolveId отвечает за идентификацию модуля, а load — за его содержимое. Их взаимодействие можно рассматривать как двухфазный процесс:

  1. Разрешение импорта
  2. Загрузка содержимого

Если resolveId возвращает null, Rollup продолжает поиск в файловой системе и других плагинах. Если возвращён идентификатор — цепочка фиксируется за конкретным модулем.

Важно учитывать, что resolveId может возвращать не только строку, но и объект с метаданными:

resolveId(source) {
  if (source === 'virtual:demo') {
    return {
      id: '\0virtual:demo',
      moduleSideEffects: false
    };
  }
  return null;
}

Это позволяет оптимизировать tree-shaking.

Виртуальные модули как слой абстракции

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

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

  • генерация маршрутов (routing);
  • внедрение переменных окружения;
  • создание индексных модулей;
  • агрегация данных из нескольких источников;
  • адаптация внешних API под ES-модули.

Пример генерации маршрутов:

export default function routesPlugin(routes) {
  const id = 'virtual:routes';

  return {
    name: 'routes-plugin',

    resolveId(source) {
      if (source === id) return id;
    },

    load(sourceId) {
      if (sourceId !== id) return null;

      const imports = routes.map((r, i) =>
        `import * as m${i} from '${r.path}';`
      ).join('\n');

      const exports = `
        export const routes = [
          ${routes.map((r, i) => `{ path: '${r.path}', module: m${i} }`).join(',\n')}
        ];
      `;

      return `${imports}\n\n${exports}`;
    }
  };
}

Работа с зависимостями внутри виртуальных модулей

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

load(id) {
  if (id === 'virtual:main') {
    return `
      import data from './data.json';

      export default {
        data
      };
    `;
  }
}

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

Кеширование и стабильность идентификаторов

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

  • нарушению кеширования;
  • лишним пересборкам;
  • нестабильному HMR (в режиме watch).

Поэтому идентификаторы должны быть детерминированными:

const id = `virtual:component:${name}`;

и не зависеть от случайных значений.

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

Поскольку виртуальные модули генерируют код вручную, важно учитывать sourcemaps.

Rollup ожидает, что load может вернуть объект:

load(id) {
  if (id === 'virtual:example') {
    return {
      code: 'export const x = 1;',
      map: { mappings: '' }
    };
  }
}

При генерации сложного кода sourcemap становится критическим для отладки, особенно если виртуальный модуль участвует в трансформациях TypeScript или Babel.

Ошибки и диагностика

Плагины виртуальных модулей часто используют this.error и this.warn для диагностики:

load(id) {
  if (id === 'virtual:config') {
    if (!this.meta.config) {
      this.warn('Config is not provided, using defaults');
    }

    return `export const config = {};`;
  }
}

Ошибки, возникающие внутри виртуальных модулей, отображаются как ошибки обычных модулей, но их трассировка зависит от корректности sourcemap.

Взаимодействие с watch-режимом

В режиме наблюдения виртуальные модули могут пересоздаваться при изменении внешних данных. Для этого используется кеширование состояния внутри плагина:

let cache;

export default function plugin() {
  return {
    name: 'virtual-watch',

    buildStart() {
      cache = null;
    },

    load(id) {
      if (id === 'virtual:data') {
        if (!cache) {
          cache = computeHeavyData();
        }

        return `export default ${JSON.stringify(cache)}`;
      }
    }
  };
}

Такой подход снижает нагрузку при частых пересборках.

Виртуальные модули и ESM-границы

Rollup строго работает с ES-модулями, поэтому виртуальные модули должны возвращать корректный ESM-код. Недопустимы:

  • CommonJS-синтаксис без трансформации;
  • некорректные import/export;
  • динамически сломанная структура модулей.

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

Композиция нескольких виртуальных модулей

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

const modules = {
  'virtual:alpha': 'export const a = 1;',
  'virtual:beta': 'export const b = 2;'
};

export default function multiVirtual() {
  return {
    name: 'multi-virtual',

    resolveId(source) {
      if (modules[source]) return source;
      return null;
    },

    load(id) {
      if (modules[id]) return modules[id];
      return null;
    }
  };
}

Такой подход используется в библиотеках, которые предоставляют набор runtime-утилит через Rollup.

Типичные проблемы при использовании виртуальных модулей

  • конфликт идентификаторов при масштабировании плагинов;
  • потеря tree-shaking из-за неправильных sideEffects;
  • отсутствие sourcemap и сложная отладка;
  • дублирование виртуальных модулей при параллельных плагинах;
  • нестабильность кеша в watch-режиме.

Каждая из этих проблем возникает не из-за самого механизма, а из-за отсутствия строгой дисциплины при проектировании плагина.

Роль виртуальных модулей в архитектуре сборки

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

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

Их использование особенно эффективно в плагинах, которые работают как мини-компиляторы внутри Rollup, расширяя границы стандартного ESM-графа.