Поля main, module, browser в package.json

Поле main в package.json традиционно определяет точку входа пакета для CommonJS-среды. Исторически оно появилось задолго до ES-модулей и служило универсальным способом указать файл, который должен быть загружен при require('package') в Node.js или аналогичных системах.

{
  "name": "example-lib",
  "version": "1.0.0",
  "main": "dist/index.cjs.js"
}

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

Однако в современных JavaScript-проектах этого недостаточно, поскольку экосистема разделилась на несколько типов модулей: CommonJS, ES Modules и браузерные сборки.


Поле module используется для указания точки входа в формате ES Modules (ESM). Оно было введено как негласный стандарт для сборщиков (включая Rollup, Webpack, Vite), чтобы отделить ESM-версию пакета от CommonJS-версии.

{
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js"
}

Смысл разделения заключается в том, что ES-модули обладают преимуществами статического анализа: tree-shaking, более эффективное связывание зависимостей, предсказуемая структура импорта.

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

Ключевое отличие:

  • main — CommonJS-формат
  • module — ES Module-формат

Пример использования в сборке:

import something from 'example-lib';

Если сборщик поддерживает ESM, он возьмёт версию из module, а не из main.


Поле browser

Поле browser предназначено для указания альтернативной сборки пакета, оптимизированной под браузерную среду. Оно решает две задачи:

  1. Замена Node.js-специфичных модулей
  2. Подмена файлов, использующих API Node.js (fs, path, net и т.д.)
{
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "browser": "dist/index.browser.js"
}

В расширенной форме browser может быть объектом, позволяющим заменять отдельные модули:

{
  "browser": {
    "fs": false,
    "path": "./shims/path-browser.js",
    "./internal/fs-utils.js": "./shims/fs-utils-browser.js"
  }
}

Значение false означает полное исключение модуля из бандла. Это критически важно для библиотек, которые изначально разрабатывались под Node.js, но должны корректно работать в браузере.

Rollup учитывает поле browser при сборке, если активирован соответствующий резолвер (например, через @rollup/plugin-node-resolve). В этом случае происходит замена зависимостей на браузерные аналоги до этапа бандлинга.


Приоритет разрешения полей

Разные инструменты используют разные правила приоритета, но типичная логика выглядит следующим образом:

Для Node.js

  1. main
  2. Файлы по умолчанию (index.js и т.д.)

Для ES-модульных сборщиков (Rollup, Vite, современный Webpack)

  1. module
  2. browser (если target = browser)
  3. main

Эта схема объясняет, почему один и тот же пакет может вести себя по-разному в разных окружениях.


Поведение Rollup при резолве

Rollup не использует package.json напрямую без плагинов. Основную роль играет @rollup/plugin-node-resolve, который интерпретирует поля следующим образом:

  • Если сборка для браузера:

    • сначала проверяется browser
    • затем module
    • затем main
  • Если сборка для Node.js:

    • игнорируется browser
    • используется module или main

Также важно, что Rollup работает со статическим графом зависимостей, поэтому выбор конкретного entry-файла происходит один раз на этапе анализа модулей.


Влияние на tree-shaking

Наиболее значительное влияние оказывает именно связка module + sideEffects.

Если пакет экспортирует ES Modules через module, Rollup способен:

  • анализировать импорт/экспорт на уровне AST
  • удалять неиспользуемые функции и переменные
  • исключать целые модули при отсутствии побочных эффектов

Пример структуры пакета:

{
  "name": "example-lib",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "browser": "dist/index.browser.js",
  "sideEffects": false
}

Поле sideEffects: false усиливает эффект tree-shaking, позволяя Rollup безопасно удалять неиспользуемые импорты без анализа побочных эффектов файлов.


Типовые ошибки конфигурации

Конфликт main и module

Часто встречается ситуация, когда main и module указывают на один и тот же файл:

{
  "main": "dist/index.js",
  "module": "dist/index.js"
}

Это фактически отключает преимущества ES Modules, поскольку Rollup не получает ESM-версию пакета.


Отсутствие browser при наличии Node.js-зависимостей

Если библиотека использует Node.js API, но не предоставляет browser-замены, браузерная сборка может завершиться ошибкой:

  • попытка импорта fs
  • использование path
  • обращение к process без полифиллов

Неправильный порядок экспорта

Некоторые пакеты экспортируют ESM-версию в main, а CommonJS — в module, что нарушает ожидания сборщиков:

{
  "main": "dist/index.esm.js",
  "module": "dist/index.cjs.js"
}

Такое распределение приводит к деградации tree-shaking и увеличению размера бандла.


Современная практика использования

В современных библиотеках чаще всего применяется тройственная схема:

  • main — CommonJS (для обратной совместимости)
  • module — ESM (для сборщиков)
  • browser — ESM или UMD, адаптированный под браузер

Пример:

{
  "name": "example-lib",
  "version": "2.0.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "browser": "dist/browser/index.js",
  "sideEffects": false
}

Такой подход обеспечивает совместимость с:

  • Node.js (CommonJS)
  • современными сборщиками (ESM)
  • браузерной средой без Node API

Взаимодействие с Rollup-конфигурацией

Хотя package.json играет важную роль, финальное поведение определяется конфигурацией Rollup:

export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/cjs/index.js',
      format: 'cjs'
    },
    {
      file: 'dist/esm/index.js',
      format: 'esm'
    },
    {
      file: 'dist/browser/index.js',
      format: 'esm'
    }
  ]
};

В этом случае package.json становится контрактом между библиотекой и внешними сборщиками, но не управляет самой генерацией файлов.


Итоговая логика взаимодействия полей

  • main обеспечивает базовую совместимость с Node.js
  • module оптимизирует работу сборщиков через ESM
  • browser адаптирует пакет под клиентскую среду
  • Rollup выбирает приоритет на основе среды сборки и плагинов
  • tree-shaking напрямую зависит от наличия корректного ESM-вывода

Эти поля формируют фундаментальную модель того, как один и тот же пакет может иметь разные представления в зависимости от среды исполнения и инструментов сборки.