Система модулей CommonJS, несмотря на историческую значимость в экосистеме Node.js, вступает в фундаментальное противоречие с моделью статического анализа, на которой основан Rollup. Основная идея Rollup заключается в построении статического графа зависимостей и последующем удалении неиспользуемого кода (tree-shaking). CommonJS же изначально проектировался как динамическая система загрузки модулей, где структура зависимостей может быть определена только во время выполнения.
Ключевая проблема заключается в отсутствии гарантированно статического контекста. В CommonJS используются конструкции вида:
const dep = require(someVariable);
или даже:
if (condition) {
require('./module-a');
} else {
require('./module-b');
}
Такие выражения делают невозможным построение полного графа зависимостей на этапе сборки. Rollup в этом случае вынужден либо игнорировать потенциальные зависимости, либо эмулировать поведение выполнения, что резко снижает точность анализа.
Tree-shaking в Rollup основан на предположении, что каждый импорт можно точно сопоставить с экспортами. В ES-модулях это возможно благодаря строгой спецификации: все импорты и экспорты определяются на уровне синтаксиса.
CommonJS нарушает эту модель по нескольким причинам:
module.exports может быть перезаписан
целиком;Пример:
module.exports = Math.random() > 0.5
? require('./a')
: require('./b');
В такой ситуации невозможно определить, какие именно символы будут доступны в результате выполнения модуля. Rollup вынужден считать такой модуль неоптимизируемым с точки зрения tree-shaking, что приводит к включению большего объёма кода в бандл.
Для поддержки CommonJS Rollup использует плагинную систему, чаще
всего через @rollup/plugin-commonjs. Этот плагин не
превращает CommonJS в полноценный ES-модуль, а лишь пытается
эвристически реконструировать статическую
структуру.
Основные подходы:
require('module')module.exports = ...exportsОднако любая динамика резко снижает точность анализа. Например:
const name = './module-' + process.env.TYPE;
const mod = require(name);
Такой код полностью выходит за рамки статического анализа. Rollup в этом случае не может определить, какие файлы потенциально участвуют в сборке.
Помимо CommonJS, аналогичную проблему создают динамические импорты
через import() в ES-модулях. Несмотря на то, что
import() является частью стандарта, его поведение
принципиально отличается от статического import.
Динамический импорт:
import('./module.js').then(m => {
m.run();
});
создаёт точку разрыва статического графа. Rollup вынужден трактовать такие зависимости как асинхронные границы чанков, что влияет на архитектуру итогового бандла.
Основные последствия:
Особенно проблематичны случаи, когда путь в import()
вычисляется динамически:
import(`./locale/${lang}.js`);
Такие конструкции превращают граф зависимостей в частично неопределённый, где Rollup может лишь предполагать возможные варианты, но не гарантировать полноту анализа.
Наиболее сложные случаи возникают при комбинации CommonJS и
динамических импортов. Например, когда CommonJS модуль возвращает
функцию, которая внутри вызывает import():
module.exports = function load() {
return import('./heavy-module.js');
};
Здесь возникает двойная неопределённость:
Rollup не может построить ни статический граф модулей, ни предсказать runtime-разветвления.
Преобразование CommonJS в ES-модули в Rollup не является изоморфным. Это всегда приближённая модель, зависящая от структуры кода.
Основные ограничения:
module.exports = functionexports.foo = ... при динамическом
добавлении свойствdefault export и named
exportsПример проблемного случая:
Object.defineProperty(exports, 'value', {
get() {
return computeExpensiveValue();
}
});
В ES-модулях подобная семантика не имеет прямого эквивалента, что делает трансформацию либо неточной, либо полностью отказной.
Дополнительная сложность возникает из-за побочных эффектов. CommonJS модули часто предполагают выполнение кода при загрузке:
console.log('module loaded');
global.state = true;
Rollup в ES-режиме старается минимизировать такие эффекты, но при CommonJS он вынужден считать модуль потенциально опасным для удаления.
В результате:
Особенно критично это при использовании пакетов из npm, где CommonJS остаётся доминирующим стандартом.
Rollup стремится определить, какие именно символы экспортируются модулем. В CommonJS это часто невозможно:
for (const key of Object.keys(someObject)) {
exports[key] = someObject[key];
}
Такой код полностью разрушает статическую модель экспортов. Плагин CommonJS вынужден либо:
Оба варианта ухудшают эффективность tree-shaking.
Наиболее проблемный паттерн:
function load(name) {
return require('./' + name);
}
Он делает невозможным:
Rollup в таких случаях перестаёт быть инструментом оптимизации и превращается в простой объединитель файлов с минимальной трансформацией.
Конфликт CommonJS и Rollup является не технической недоработкой, а следствием различий в философии модульности:
В результате любые попытки совместного использования неизбежно приводят к компромиссам между совместимостью и качеством оптимизации, где точность анализа всегда уменьшается пропорционально росту динамики кода.