Поле output.freeze и output.esModule

Параметр output.freeze управляет генерацией вызовов Object.freeze() для экспортируемых объектов и пространств имён в итоговом бандле. Настройка влияет на поведение модулей ES при работе с экспортами, а также на совместимость со средами выполнения и производительность.

По умолчанию Rollup стремится приблизить поведение сгенерированного кода к стандартной семантике ES Modules. Одним из механизмов такого приближения становится заморозка namespace-объектов.

Назначение Object.freeze

В JavaScript функция Object.freeze() запрещает:

  • добавление новых свойств;
  • удаление существующих свойств;
  • изменение значений свойств;
  • изменение дескрипторов свойств.

Пример:

const user = {
    name: 'Alex'
};

Object.freeze(user);

user.name = 'John';

console.log(user.name); // Alex

После заморозки объект становится неизменяемым.


Как Rollup использует output.freeze

При сборке модулей Rollup может генерировать namespace-объекты:

import * as utils from './utils.js';

Подобные объекты представляют пространство имён модуля. Чтобы сделать их поведение ближе к стандарту ES Modules, Rollup по умолчанию замораживает такие структуры.

Пример генерации:

var utils = /*#__PURE__*/Object.freeze({
    __proto__: null,
    sum: sum,
    multiply: multiply
});

Здесь Rollup:

  1. создаёт namespace-объект;
  2. добавляет экспортируемые элементы;
  3. вызывает Object.freeze().

Значение по умолчанию

output: {
    freeze: true
}

Поведение включено автоматически.


Отключение freeze

export default {
    input: 'src/main.js',
    output: {
        file: 'dist/bundle.js',
        format: 'esm',
        freeze: false
    }
};

В этом случае Rollup перестанет добавлять Object.freeze().

Сгенерированный код станет проще:

var utils = {
    __proto__: null,
    sum: sum,
    multiply: multiply
};

Для чего отключают output.freeze

Уменьшение размера бандла

Каждый вызов Object.freeze() увеличивает итоговый объём кода.

В небольших проектах разница почти незаметна, однако в крупных библиотеках с большим количеством namespace-объектов объём может увеличиваться ощутимо.


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

Object.freeze() требует дополнительных операций во время инициализации.

В большинстве приложений влияние минимально, но:

  • в микробиблиотеках;
  • в high-performance runtime;
  • в системах с большим количеством динамических модулей

иногда отключают freeze ради ускорения старта.


Совместимость со старыми окружениями

Некоторые старые JavaScript-движки или нестандартные embedded-среды работают с Object.freeze() медленно либо некорректно.

В подобных случаях параметр отключают полностью.


Побочные эффекты отключения

Нарушение семантики ES Modules

ES Modules предполагают неизменяемость namespace-объектов.

При отключённом freeze появляется возможность модифицировать экспортированный namespace:

import * as api from './api.js';

api.test = 123;

При freeze: true подобный код вызовет ошибку либо будет проигнорирован.

При freeze: false изменение станет возможным.


Поведение библиотек может измениться

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

Особенно это касается:

  • систем анализа модулей;
  • SSR-инструментов;
  • тестовых раннеров;
  • devtools;
  • runtime-валидаторов.

Когда рекомендуется freeze: true

Библиотеки

Для npm-пакетов заморозка почти всегда является правильным решением.

Причины:

  • предсказуемость API;
  • соответствие стандарту ES Modules;
  • защита экспортов от мутаций;
  • стабильность поведения.

Пример:

export default {
    input: 'src/index.js',
    output: {
        dir: 'dist',
        format: 'esm',
        freeze: true
    }
};

SDK и shared-модули

Если модуль используется множеством внешних потребителей, неизменяемость экспортов уменьшает вероятность ошибок.


Публичные runtime API

Например:

export const config = {
    api: '/v1'
};

Freeze помогает защититься от случайного изменения структуры.


Когда используется freeze: false

Внутренние приложения

Если бандл не публикуется как библиотека и используется только внутри проекта, строгая неизменяемость часто не нужна.


Высоконагруженные runtime

Некоторые проекты минимизируют любую дополнительную работу при инициализации.


Микробиблиотеки

Иногда разработчики жертвуют частью корректности ради минимального размера.


Связь с tree shaking

output.freeze не влияет напрямую на tree shaking.

Однако отключение freeze:

  • уменьшает объём обёрток;
  • упрощает итоговый код;
  • иногда улучшает минификацию.

Но влияние обычно незначительно.


Использование с разными форматами

ESM

Наиболее актуальный сценарий.

output: {
    format: 'esm',
    freeze: true
}

CommonJS

Rollup может замораживать namespace-объекты и при генерации CJS.


UMD и IIFE

В UMD/IIFE freeze тоже применяется к объектам экспортов.


Пример полного конфига

import terser from '@rollup/plugin-terser';

export default {
    input: 'src/index.js',

    output: {
        file: 'dist/bundle.js',
        format: 'umd',
        name: 'MyLibrary',
        freeze: true
    },

    plugins: [
        terser()
    ]
};

Поле output.esModule

Параметр output.esModule управляет добавлением специального маркера ES Module в CommonJS-бандлы.

Речь идёт о свойстве:

__esModule

Этот флаг широко используется экосистемой JavaScript для совместимости между:

  • CommonJS;
  • ES Modules;
  • Babel;
  • TypeScript;
  • Webpack;
  • Rollup;
  • Node.js.

Что такое __esModule

Многие транспайлеры и сборщики добавляют специальное свойство:

exports.__esModule = true;

или:

Object.defineProperty(exports, '__esModule', {
    value: true
});

Этот флаг сообщает:

модуль был создан как ES Module либо совместим с ES Module semantics.


Зачем нужен __esModule

Проблема возникает из-за различий между:

CommonJS

module.exports = value;

и:

exports.test = value;

ES Modules

export default value;

и:

export const test = value;

Системам совместимости необходимо понимать:

  • где default export;
  • где namespace;
  • как интерпретировать импорт.

Пример проблемы

CommonJS-модуль:

module.exports = 'hello';

Импорт:

import value from './module.js';

Без дополнительных механизмов совместимости возможны неоднозначности.


Как работает output.esModule

Rollup может автоматически добавлять:

Object.defineProperty(exports, '__esModule', {
    value: true
});

Значения output.esModule

true

Всегда добавлять __esModule.

output: {
    format: 'cjs',
    esModule: true
}

Результат:

Object.defineProperty(exports, '__esModule', {
    value: true
});

false

Никогда не добавлять.

output: {
    format: 'cjs',
    esModule: false
}

"if-default-prop"

Добавлять только при необходимости.

Это современное рекомендуемое поведение Rollup.

Пример:

output: {
    format: 'cjs',
    esModule: 'if-default-prop'
}

Значение по умолчанию

В современных версиях Rollup:

esModule: "if-default-prop"

Rollup старается избегать лишнего __esModule, но добавляет его при необходимости совместимости.


Когда нужен __esModule

Совместимость с Babel

Babel активно использует этот флаг.

Без него могут появляться конструкции:

module.default.default

или некорректные default imports.


Совместимость с TypeScript

TypeScript при esModuleInterop и allowSyntheticDefaultImports ориентируется на наличие __esModule.


Интеграция с Webpack

Webpack также учитывает этот маркер.


Публикация библиотек

Для npm-библиотек наличие __esModule часто улучшает совместимость с различными сборщиками.


Когда esModule: false полезен

Минимизация размера

Каждая дополнительная строка увеличивает размер.

В микро-бандлах иногда отключают:

output: {
    esModule: false
}

Полный контроль над экспортами

Некоторые проекты предпочитают самостоятельно управлять interop-логикой.


Старые CommonJS-системы

Иногда legacy-runtime ожидает «чистый» CommonJS без дополнительных свойств.


Поведение при format: 'esm'

Для настоящих ES Modules параметр практически не имеет значения.

__esModule нужен прежде всего для CommonJS interoperability.


Пример генерации CJS

Исходный модуль:

export default function sum(a, b) {
    return a + b;
}

Конфиг:

export default {
    input: 'src/index.js',

    output: {
        file: 'dist/index.cjs',
        format: 'cjs',
        esModule: true
    }
};

Rollup может сгенерировать:

'use strict';

Object.defineProperty(exports, '__esModule', {
    value: true
});

function sum(a, b) {
    return a + b;
}

exports.default = sum;

Различие exports.default и module.exports

ES Module style

exports.default = value;

Classic CommonJS

module.exports = value;

__esModule помогает инструментам понять, как правильно интерпретировать экспорт.


Связь с interop

Параметр тесно связан с:

output.interop

Interop управляет преобразованием импортов между CJS и ESM.

esModule определяет наличие служебного флага совместимости.


Рекомендуемые настройки

Для библиотек

output: {
    format: 'cjs',
    esModule: 'if-default-prop'
}

или:

output: {
    format: 'cjs',
    esModule: true
}

Для внутренних приложений

output: {
    esModule: false
}

если совместимость не требуется.


Совместное использование freeze и esModule

Пример:

export default {
    input: 'src/index.js',

    output: {
        file: 'dist/library.cjs',
        format: 'cjs',

        freeze: true,
        esModule: 'if-default-prop'
    }
};

Такой конфиг:

  • сохраняет корректную семантику экспортов;
  • улучшает interop;
  • повышает совместимость библиотеки;
  • делает поведение ближе к стандартным ES Modules.