Одним из ключевых преимуществ Rollup является работа с модулями формата ES Modules (ESM). Однако значительная часть экосистемы JavaScript долгое время развивалась вокруг стандарта CommonJS, который активно использовался в Node.js и тысячах npm-пакетов.
Плагин @rollup/plugin-commonjs
предназначен для преобразования модулей CommonJS в формат, понятный
Rollup. Благодаря этому появляется возможность подключать библиотеки,
написанные через require(), module.exports и
exports, как если бы они были обычными ESM-модулями.
Без данного плагина Rollup способен корректно анализировать только ESM-код. Попытка импортировать CommonJS-пакет обычно приводит к ошибкам разрешения зависимостей или невозможности выполнить статический анализ.
Типичная ситуация:
const lodash = require('lodash');
module.exports = {
format(value) {
return lodash.camelCase(value);
}
};
Rollup не сможет корректно обработать такой файл без дополнительного преобразования.
Плагин устанавливается отдельно:
npm install @rollup/plugin-commonjs --save-dev
или:
yarn add @rollup/plugin-commonjs --dev
После установки его необходимо подключить в конфигурацию Rollup.
Наиболее распространённая конфигурация выглядит следующим образом:
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [
commonjs()
]
};
После подключения Rollup начинает преобразовывать CommonJS-модули в ESM во время сборки.
На практике @rollup/plugin-commonjs почти всегда
используется вместе с плагином разрешения модулей.
Установка:
npm install @rollup/plugin-node-resolve --save-dev
Конфигурация:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [
resolve(),
commonjs()
]
};
Порядок подключения имеет значение.
Сначала:
resolve()
Затем:
commonjs()
Сначала Rollup должен найти файл пакета внутри
node_modules, а уже затем выполнить преобразование
CommonJS-кода.
Рассмотрим модуль:
const fs = require('fs');
const path = require('path');
module.exports = function() {
return path.basename(__filename);
};
После обработки Rollup и plugin-commonjs формируют внутреннее представление, близкое к следующему:
import fs from 'fs';
import path from 'path';
function getName() {
return path.basename(__filename);
}
export default getName;
Разумеется, реальное преобразование сложнее, однако принцип остаётся тем же: CommonJS превращается в ESM.
CommonJS обычно экспортирует данные через объект
module.exports.
Исходный код:
module.exports = {
version: '1.0.0',
name: 'library'
};
После преобразования появляется default-экспорт:
import library from './library.js';
console.log(library.version);
Для Rollup такой модуль становится обычным ESM-источником.
Ещё один распространённый вариант:
exports.sum = (a, b) => a + b;
exports.sub = (a, b) => a - b;
exports.mul = (a, b) => a * b;
После преобразования можно использовать именованные импорты:
import { sum, sub, mul } from './math.js';
console.log(sum(2, 3));
Плагин автоматически анализирует структуру экспортов и формирует соответствующие ESM-экспорты.
Иногда встречаются модули следующего вида:
module.exports = mainFunction;
module.exports.version = '2.0';
module.exports.author = 'Developer';
Такой код является корректным для CommonJS, но создаёт дополнительные сложности при конвертации.
Плагин выполняет анализ подобных конструкций и пытается сохранить семантику исходного модуля.
Использование:
import mainFunction from './module.js';
или:
import moduleData from './module.js';
console.log(moduleData.version);
Конкретный результат зависит от структуры исходного кода и настроек преобразования.
Наиболее важная задача plugin-commonjs — работа со сторонними библиотеками.
Например:
import chalk from 'chalk';
import minimist from 'minimist';
import moment from 'moment';
Многие старые версии популярных пакетов распространялись исключительно в формате CommonJS.
Без plugin-commonjs сборка могла завершиться ошибкой:
Error: Unexpected token
или:
'default' is not exported by package
Подключение плагина решает эту проблему автоматически.
По умолчанию плагин анализирует большое количество файлов.
Для ограничения области обработки используется параметр
include.
commonjs({
include: /node_modules/
})
Обрабатываться будут только файлы внутри каталога:
node_modules
Можно указать массив шаблонов:
commonjs({
include: [
'node_modules/**',
'legacy/**'
]
})
Это полезно для ускорения сборки крупных проектов.
Иногда необходимо исключить отдельные каталоги.
commonjs({
exclude: [
'node_modules/some-library/**'
]
})
Либо:
commonjs({
exclude: /test/
})
Исключённые файлы не будут подвергаться преобразованию.
Позволяет игнорировать отдельные зависимости.
Пример:
commonjs({
ignore: [
'electron'
]
})
Любые вызовы:
require('electron')
останутся нетронутыми.
Это удобно при работе с платформозависимыми пакетами.
Одна из сложностей CommonJS — динамические импорты.
Например:
const moduleName = getModuleName();
const module = require(moduleName);
Во время сборки Rollup не может определить содержимое переменной:
moduleName
Для подобных случаев существует настройка:
commonjs({
ignoreDynamicRequires: true
})
Тогда вызов будет оставлен в итоговом коде без преобразования.
Статический анализ является основой работы Rollup.
Следующий код анализируется успешно:
require('./utils.js');
Но такой вариант уже вызывает сложности:
require('./' + name + '.js');
или:
require(config.module);
Поскольку путь неизвестен во время сборки, Rollup не способен включить нужный модуль в бандл заранее.
В подобных случаях приходится либо рефакторить код, либо использовать специальные настройки плагина.
Позволяет явно указать файлы, которые могут загружаться динамически.
commonjs({
dynamicRequireTargets: [
'src/plugins/*.js'
]
})
Например:
const plugin = require(`./plugins/${name}.js`);
Rollup заранее включит возможные варианты в сборку.
Это значительно повышает совместимость со старым Node.js-кодом.
Некоторые проекты содержат одновременно ESM и CommonJS.
Пример:
import config from './config.js';
const helper = require('./helper.js');
export default function() {
helper();
}
Подобные файлы называются смешанными.
По умолчанию такие конструкции могут обрабатываться не полностью.
Для принудительного преобразования используется:
commonjs({
transformMixedEsModules: true
})
После включения опции плагин анализирует файлы, содержащие
одновременно import и require.
В CommonJS часто встречается подобный код:
if (process.env.NODE_ENV === 'development') {
require('./dev-tools');
}
или:
if (isWindows) {
require('./windows');
}
Плагин старается определить подобные зависимости и включить их в граф модулей.
Однако чрезмерно сложная логика может привести к невозможности корректного анализа.
При взаимодействии CommonJS и ESM возникает вопрос о том, что должен возвращать импорт.
Например:
import value from 'library';
или:
import * as value from 'library';
Опция:
commonjs({
requireReturnsDefault: true
})
влияет на способ интерпретации экспортов.
Она особенно полезна при интеграции старых пакетов с современным ESM-кодом.
Пример настройки:
commonjs({
requireReturnsDefault: 'preferred'
})
Возможны различные режимы поведения, позволяющие добиться совместимости с конкретной библиотекой.
Во время преобразования плагин часто добавляет специальные вспомогательные функции.
Например:
function getDefaultExportFromCjs(x) {
return x && x.__esModule
? x.default
: x;
}
или:
function commonjsRequire() {
throw new Error(
'Dynamic requires are not currently supported'
);
}
Подобный код необходим для эмуляции поведения CommonJS внутри ESM-сборки.
Одно из главных достоинств Rollup — агрессивное удаление неиспользуемого кода.
С CommonJS ситуация сложнее.
Рассмотрим модуль:
exports.a = function() {};
exports.b = function() {};
exports.c = function() {};
Из-за динамической природы CommonJS Rollup не всегда способен точно определить, какие экспорты реально используются.
Поэтому эффективность tree shaking для CommonJS обычно ниже, чем для чистого ESM.
Лучшие результаты достигаются при использовании библиотек, уже опубликованных в формате ES Modules.
Обработка CommonJS требует дополнительного анализа:
require;В крупных проектах это может заметно увеличить время сборки.
Для оптимизации рекомендуется:
include;exclude;require;Конфигурация:
plugins: [
commonjs()
]
может оказаться недостаточной.
Если пакет расположен в node_modules, обычно
требуется:
plugins: [
resolve(),
commonjs()
]
Плохо:
plugins: [
commonjs(),
resolve()
]
Правильно:
plugins: [
resolve(),
commonjs()
]
Проблемный код:
require(variable);
или:
require(getPath());
Такие конструкции не поддаются полноценному статическому анализу.
Файл:
import helper from './helper.js';
module.exports = {
helper
};
может работать непредсказуемо без дополнительных настроек.
В подобных ситуациях часто помогает:
commonjs({
transformMixedEsModules: true
})
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
sourcemap: true
},
plugins: [
resolve(),
commonjs({
include: /node_modules/,
transformMixedEsModules: true
})
]
};
Подобная конфигурация обеспечивает совместимость с подавляющим большинством npm-пакетов, написанных на CommonJS, позволяет использовать старые библиотеки внутри современных проектов Rollup и значительно упрощает миграцию существующей кодовой базы на экосистему ES Modules.