Экосистема JavaScript долгое время опиралась на формат модулей
CommonJS, где экспорт осуществляется через module.exports,
а импорт через require(). Несмотря на переход индустрии к
ESM (ECMAScript Modules), значительное количество npm-пакетов продолжает
распространяться именно в формате CommonJS. Это создаёт необходимость
трансформации таких модулей при сборке через Rollup.
Rollup изначально ориентирован на ESM и не интерпретирует CommonJS
без дополнительных преобразований. Для корректной работы используется
специализированный плагин @rollup/plugin-commonjs, который
конвертирует CommonJS-модули в ESM-совместимый формат на этапе
бандлинга.
CommonJS-модуль характеризуется синхронной моделью загрузки и динамической природой экспорта:
// commonjs-module.js
const value = 42;
module.exports = {
value
};
Эквивалент в ESM:
// esm-эквивалент
export const value = 42;
Проблема заключается в том, что CommonJS допускает динамическое изменение экспорта:
module.exports.value = compute();
Rollup не может статически анализировать такие конструкции без дополнительной трансформации.
Основной инструмент поддержки CommonJS в Rollup —
@rollup/plugin-commonjs. Он преобразует CommonJS-модули в
ESM-совместимый код, позволяя Rollup включать их в граф
зависимостей.
Пример конфигурации:
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/index.js',
output: {
format: 'esm',
file: 'dist/bundle.js'
},
plugins: [
resolve(),
commonjs()
]
};
Ключевой момент заключается в порядке подключения плагинов: сначала
выполняется разрешение модулей (node-resolve), затем
трансформация CommonJS.
Плагин выполняет статический анализ и преобразует CommonJS-экспорты в именованные и дефолтные экспорты ESM:
Исходный код:
module.exports = function () {
return 10;
};
После трансформации:
const commonjsModule = function () {
return 10;
};
export default commonjsModule;
При наличии нескольких свойств:
exports.a = 1;
exports.b = 2;
Преобразуется в:
const a = 1;
const b = 2;
export { a, b };
require() и статического анализаCommonJS использует require() как механизм импорта:
const fs = require('fs');
const lib = require('./lib');
Rollup с плагином преобразует такие вызовы в ESM-импорты:
import fs from 'fs';
import lib from './lib.js';
Однако поддерживается только статическая форма
require(). Динамические конструкции ограничены:
const mod = require(path); // сложный случай
Подобные выражения либо игнорируются, либо оборачиваются в fallback-обработчики, что снижает эффективность tree-shaking.
Одним из ключевых ограничений является невозможность полноценного tree-shaking для CommonJS-модулей.
Причины:
module.exportsrequire() внутри условийПример:
if (process.env.NODE_ENV === 'production') {
module.exports = require('./prod');
} else {
module.exports = require('./dev');
}
Такие конструкции препятствуют анализу зависимости на этапе сборки.
Плагин предоставляет параметры, влияющие на интерпретацию CommonJS-экспорта.
requireReturnsDefaultОпределяет, как интерпретируется require() при
экспорте:
true — всегда использовать defaultfalse — использовать объект модулей"auto" — автоматическое определениеПример:
commonjs({
requireReturnsDefault: 'auto'
});
transformMixedEsModulesОбеспечивает обработку модулей, содержащих одновременно ESM и CommonJS:
export const a = 1;
module.exports = { b: 2 };
Без трансформации такие модули приводят к конфликту форматов.
strictRequiresУлучшает предсказуемость обработки require():
commonjs({
strictRequires: true
});
При включении снижается вероятность некорректной интерпретации динамических импортов.
CommonJS не имеет формального механизма именованных экспортов, но многие библиотеки имитируют его:
exports.parse = function () {};
exports.stringify = function () {};
Плагин формирует именованные экспорты:
export const parse;
export const stringify;
Однако при нестандартных паттернах (например, замена
module.exports целиком) требуется явная настройка:
commonjs({
namedExports: {
'some-commonjs-lib': ['parse', 'stringify']
}
});
module.exports = functionОсобый случай представляет экспорт функции:
module.exports = function add(a, b) {
return a + b;
};
После трансформации:
function add(a, b) {
return a + b;
}
export default add;
Если дополнительно добавляются свойства:
module.exports.version = '1.0.0';
формируется смешанный экспорт:
function add() {}
add.version = '1.0.0';
export default add;
export const version = add.version;
CommonJS допускает циклические зависимости через частично инициализированные объекты:
// a.js
const b = require('./b');
module.exports.a = true;
// b.js
const a = require('./a');
module.exports.b = true;
При трансформации Rollup сохраняет поведение, но структура становится менее предсказуемой из-за статической природы ESM, где циклы обрабатываются иначе (live bindings).
CommonJS-модули часто выполняют код при импорте:
console.log('init');
module.exports = {};
Rollup учитывает флаг sideEffects в
package.json:
{
"sideEffects": false
}
Однако для CommonJS это не всегда корректно, так как анализ побочных эффектов ограничен.
Встроенные модули Node.js (fs, path,
crypto) при использовании через require()
преобразуются в ESM-импорты:
import fs from 'fs';
import path from 'path';
Плагин @rollup/plugin-node-resolve совместно с
commonjs обеспечивает корректное разрешение таких
зависимостей.
При сборке крупных проектов с множеством CommonJS-пакетов возникают следующие узкие места:
require()
паттерновДля уменьшения нагрузки используется ограничение области применения плагина:
commonjs({
include: /node_modules/,
exclude: ['src/**']
});
Многие современные npm-пакеты используют гибридную структуру:
export const esm = true;
module.exports = {
cjs: true
};
Такие случаи требуют комбинированной обработки. Rollup интерпретирует ESM-часть напрямую, а CommonJS-часть через трансформацию.
Одной из ключевых проблем является различие между:
export defaultmodule.exportsПлагин создаёт интероп-обёртку:
import pkg from 'cjs-package';
Фактически:
const pkg = interopRequireDefault(require('cjs-package'));
где interopRequireDefault обеспечивает корректное
извлечение default-значения.
Порядок подключения влияет на результат трансформации:
node-resolve — определяет реальные пути модулейcommonjs — конвертирует найденные CommonJS-модулиНарушение порядка приводит к некорректной интерпретации зависимостей и потере экспортов.
Некоторые паттерны CommonJS остаются проблемными:
module.exports в рантаймеeval внутри модулейrequiremodule.exports = condition ? require('./a') : require('./b');
Подобные конструкции могут приводить к частичной потере tree-shaking и необходимости сохранения лишнего кода в итоговом бандле.