CommonJS остаётся одной из ключевых систем модулей в экосистеме JavaScript, особенно в среде Node.js и в большом количестве существующих npm-пакетов. При сборке с использованием Rollup работа с CommonJS требует понимания того, как устроена система модулей, какие трансформации выполняет сборщик и почему поведение таких модулей отличается от ES Modules.
CommonJS использует синхронную модель загрузки модулей через функцию
require. Экспорт осуществляется через объект
module.exports или его сокращённую форму
exports.
const utils = require('./utils');
module.exports = {
sum: (a, b) => a + b
};
Главное отличие от ES Modules заключается в том, что CommonJS:
requireВ ES Modules структура импортов и экспортов анализируется статически, что позволяет Rollup строить оптимизированный граф зависимостей и выполнять tree-shaking. В CommonJS такая оптимизация невозможна без дополнительных преобразований.
Rollup ориентирован на статический анализ зависимостей. Он ожидает, что импорт будет выражен в форме:
import { sum } from './utils.js';
В CommonJS аналогичный импорт выглядит так:
const utils = require('./utils');
Проблема заключается в том, что require может:
const moduleName = condition ? './a' : './b';
const mod = require(moduleName);
В таких случаях невозможно заранее определить структуру графа зависимостей, что блокирует оптимизации Rollup.
Для работы с CommonJS используется плагин
@rollup/plugin-commonjs. Он преобразует CommonJS-модули в
формат ES Modules, чтобы Rollup мог их обрабатывать.
Основная задача плагина:
require в importmodule.exports в
export default или именованные экспортыПример трансформации:
Исходный код:
module.exports = {
add(a, b) {
return a + b;
}
};
После преобразования:
const add = (a, b) => a + b;
export default {
add
};
Однако такая трансформация не всегда однозначна и зависит от структуры исходного кода.
Одним из ключевых преимуществ Rollup является удаление неиспользуемого кода. Однако CommonJS снижает эффективность tree-shaking.
Причины:
module.exports[process.env.TYPE] = function () {};
Такой код невозможно анализировать статически.
module.exports = {
a,
b,
c
};
При импорте:
const mod = require('./mod');
невозможно определить, какие поля используются, поэтому Rollup часто вынужден включать весь модуль.
CommonJS-модули могут выполнять код при загрузке:
console.log('module loaded');
module.exports = {};
Rollup не может безопасно удалить такой модуль даже при отсутствии использования экспортов.
CommonJS не имеет строгого понятия именованных экспортов, однако Rollup пытается их эмулировать.
exports.sum = (a, b) => a + b;
exports.mul = (a, b) => a * b;
Преобразуется в:
export const sum = (a, b) => a + b;
export const mul = (a, b) => a * b;
Однако проблемы возникают при смешанном использовании:
module.exports = function () {};
module.exports.helper = () => {};
Здесь одновременно присутствует default-экспорт и дополнительные свойства, что создаёт неоднозначную модель представления.
Rollup должен обеспечить корректное взаимодействие между системами модулей.
import pkg from 'commonjs-package';
Поскольку CommonJS не имеет default export в строгом смысле, Rollup создаёт синтетический default.
import { something } from 'commonjs-package';
Это возможно только при статическом анализе экспортов плагином. В противном случае Rollup выдаёт fallback через объектный доступ.
Часто CommonJS импортируется с деструктуризацией:
const { sum } = require('./utils');
Если экспорт не статичен, это может привести к ошибкам:
module.exports = getUtils();
В этом случае структура экспортируемого объекта известна только в runtime.
Rollup анализирует наличие побочных эффектов для оптимизации удаления кода. CommonJS усложняет этот процесс.
Факторы, влияющие на определение side effects:
module.exports в runtimerequire в условных блокахПлагин CommonJS пытается пометить модули как side-effect free, но это часто невозможно без риска.
function load(name) {
return require('./' + name);
}
Такая конструкция:
Rollup в подобных случаях либо включает все возможные модули, либо отказывается от оптимизации.
CommonJS использует кеширование через require.cache. Это
означает:
require возвращают кешированный
результатПри трансформации в ES Modules эта модель частично эмулируется, но поведение может отличаться в сложных случаях, особенно при циклических зависимостях.
CommonJS поддерживает циклические зависимости, но их поведение отличается от ES Modules.
// a.js
const b = require('./b');
module.exports.a = true;
// b.js
const a = require('./a');
module.exports.b = true;
В момент выполнения один из модулей может быть частично
инициализирован, что приводит к undefined значений.
Rollup при преобразовании таких модулей должен сохранять порядок инициализации, иначе поведение изменяется.
Обработка CommonJS увеличивает время сборки по нескольким причинам:
Особенно заметно это в больших проектах с зависимостями из npm, где значительная часть пакетов до сих пор использует CommonJS.
В реальных сборках Rollup CommonJS рассматривается как промежуточный слой:
Плагин CommonJS служит адаптером между вторым и первым уровнем, обеспечивая единый граф модулей.
Использование CommonJS в цепочке зависимостей приводит к архитектурным ограничениям:
По этой причине в современных сборках предпочтение отдаётся ES Modules, а CommonJS рассматривается как совместимый, но не оптимальный формат для обработки Rollup.