@rollup/plugin-commonjs

Одним из ключевых преимуществ 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 во время сборки.


Совместное использование с plugin-node-resolve

На практике @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-кода.


Преобразование require()

Рассмотрим модуль:

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.


Работа с module.exports

CommonJS обычно экспортирует данные через объект module.exports.

Исходный код:

module.exports = {
    version: '1.0.0',
    name: 'library'
};

После преобразования появляется default-экспорт:

import library from './library.js';

console.log(library.version);

Для Rollup такой модуль становится обычным ESM-источником.


Работа с exports

Ещё один распространённый вариант:

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);

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


Обработка пакетов из node_modules

Наиболее важная задача 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

По умолчанию плагин анализирует большое количество файлов.

Для ограничения области обработки используется параметр include.

commonjs({
    include: /node_modules/
})

Обрабатываться будут только файлы внутри каталога:

node_modules

Можно указать массив шаблонов:

commonjs({
    include: [
        'node_modules/**',
        'legacy/**'
    ]
})

Это полезно для ускорения сборки крупных проектов.


Настройка exclude

Иногда необходимо исключить отдельные каталоги.

commonjs({
    exclude: [
        'node_modules/some-library/**'
    ]
})

Либо:

commonjs({
    exclude: /test/
})

Исключённые файлы не будут подвергаться преобразованию.


Параметр ignore

Позволяет игнорировать отдельные зависимости.

Пример:

commonjs({
    ignore: [
        'electron'
    ]
})

Любые вызовы:

require('electron')

останутся нетронутыми.

Это удобно при работе с платформозависимыми пакетами.


Параметр ignoreDynamicRequires

Одна из сложностей CommonJS — динамические импорты.

Например:

const moduleName = getModuleName();

const module = require(moduleName);

Во время сборки Rollup не может определить содержимое переменной:

moduleName

Для подобных случаев существует настройка:

commonjs({
    ignoreDynamicRequires: true
})

Тогда вызов будет оставлен в итоговом коде без преобразования.


Проблема динамических require()

Статический анализ является основой работы Rollup.

Следующий код анализируется успешно:

require('./utils.js');

Но такой вариант уже вызывает сложности:

require('./' + name + '.js');

или:

require(config.module);

Поскольку путь неизвестен во время сборки, Rollup не способен включить нужный модуль в бандл заранее.

В подобных случаях приходится либо рефакторить код, либо использовать специальные настройки плагина.


Параметр dynamicRequireTargets

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

commonjs({
    dynamicRequireTargets: [
        'src/plugins/*.js'
    ]
})

Например:

const plugin = require(`./plugins/${name}.js`);

Rollup заранее включит возможные варианты в сборку.

Это значительно повышает совместимость со старым Node.js-кодом.


Параметр transformMixedEsModules

Некоторые проекты содержат одновременно ESM и CommonJS.

Пример:

import config from './config.js';

const helper = require('./helper.js');

export default function() {
    helper();
}

Подобные файлы называются смешанными.

По умолчанию такие конструкции могут обрабатываться не полностью.

Для принудительного преобразования используется:

commonjs({
    transformMixedEsModules: true
})

После включения опции плагин анализирует файлы, содержащие одновременно import и require.


Работа с условными require()

В CommonJS часто встречается подобный код:

if (process.env.NODE_ENV === 'development') {
    require('./dev-tools');
}

или:

if (isWindows) {
    require('./windows');
}

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

Однако чрезмерно сложная логика может привести к невозможности корректного анализа.


Параметр requireReturnsDefault

При взаимодействии 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-сборки.


Совместимость с tree shaking

Одно из главных достоинств Rollup — агрессивное удаление неиспользуемого кода.

С CommonJS ситуация сложнее.

Рассмотрим модуль:

exports.a = function() {};
exports.b = function() {};
exports.c = function() {};

Из-за динамической природы CommonJS Rollup не всегда способен точно определить, какие экспорты реально используются.

Поэтому эффективность tree shaking для CommonJS обычно ниже, чем для чистого ESM.

Лучшие результаты достигаются при использовании библиотек, уже опубликованных в формате ES Modules.


Производительность

Обработка CommonJS требует дополнительного анализа:

  • поиска вызовов require;
  • отслеживания экспортов;
  • преобразования структуры модуля;
  • генерации промежуточного кода.

В крупных проектах это может заметно увеличить время сборки.

Для оптимизации рекомендуется:

  • ограничивать область обработки через include;
  • исключать ненужные каталоги через exclude;
  • минимизировать использование динамических require;
  • по возможности использовать ESM-версии библиотек.

Типичные ошибки

Отсутствует plugin-node-resolve

Конфигурация:

plugins: [
    commonjs()
]

может оказаться недостаточной.

Если пакет расположен в node_modules, обычно требуется:

plugins: [
    resolve(),
    commonjs()
]

Неверный порядок плагинов

Плохо:

plugins: [
    commonjs(),
    resolve()
]

Правильно:

plugins: [
    resolve(),
    commonjs()
]

Необрабатываемый динамический require

Проблемный код:

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.