Поле exports в package.json является
механизмом управления публичным API пакета и определяет, какие модули
доступны потребителю библиотеки и в каком виде они могут быть
импортированы. В контексте Rollup это особенно важно, поскольку сборка
библиотеки часто предполагает несколько форматов (ESM, CJS, UMD), а
также разные точки входа для браузера, Node.js и вспомогательных
утилит.
До появления exports основным способом указания входной
точки был main:
{
"main": "dist/index.cjs.js"
}
Позднее добавился module для ES-модулей:
{
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js"
}
Однако оба поля имеют существенные ограничения:
Поле exports решает эти проблемы, вводя явное описание
публичного API пакета.
Минимальная форма:
{
"exports": "./dist/index.js"
}
Это эквивалентно единственной публичной точке входа. Любые попытки импортировать внутренние файлы, например:
import x from "my-lib/internal/utils.js";
будут заблокированы, если они не описаны в exports.
Ключевая возможность exports — выбор файла в зависимости
от условий окружения. Это критично при сборке библиотек через Rollup,
где обычно генерируются разные форматы.
Пример:
{
"exports": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
}
Здесь:
import используется для ESM-импортов;require используется для CommonJS.Node.js и современные сборщики (Vite, Webpack, Rollup) автоматически выбирают нужную ветку.
Можно расширять условия:
{
"exports": {
"node": {
"import": "./dist/node/index.esm.js",
"require": "./dist/node/index.cjs.js"
},
"browser": {
"import": "./dist/browser/index.esm.js",
"require": "./dist/browser/index.cjs.js"
}
}
}
Такая структура позволяет:
default используется как fallback:
{
"exports": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"default": "./dist/index.esm.js"
}
}
Если среда не распознаёт условия, будет использован
default.
Одна из ключевых возможностей — явное описание внутренних модулей:
{
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
},
"./utils": {
"import": "./dist/utils.esm.js",
"require": "./dist/utils.cjs.js"
}
}
}
Теперь библиотека контролирует доступ:
import { helper } from "my-lib/utils";
Но при этом невозможно обратиться к неописанным путям:
import x from "my-lib/src/internal.js"; // ошибка
Rollup не использует exports напрямую, но структура
пакета, определённая этим полем, влияет на архитектуру сборки.
Обычно при настройке Rollup:
exports;Пример конфигурации:
export default {
input: {
index: "src/index.js",
utils: "src/utils.js"
},
output: [
{
dir: "dist/esm",
format: "esm"
},
{
dir: "dist/cjs",
format: "cjs"
}
]
};
И затем package.json синхронизируется:
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./utils": {
"import": "./dist/esm/utils.js",
"require": "./dist/cjs/utils.js"
}
}
}
Одно из ключевых свойств exports — жёсткая
инкапсуляция.
Без exports:
my-lib/
src/
dist/
internal/
Потребитель мог импортировать:
import x from "my-lib/internal/x.js";
С exports это становится невозможным, если путь не
объявлен.
Это позволяет:
Node.js поддерживает шаблоны:
{
"exports": {
"./*": "./dist/*.js"
}
}
Это позволяет проксировать структуру каталогов.
Пример использования:
import something from "my-lib/features/some-feature";
соответствует:
./dist/features/some-feature.js
Однако использование wildcard требует осторожности, поскольку:
TypeScript учитывает exports при резолве модулей
(начиная с современных версий при moduleResolution: node16
или nodenext).
Типичная конфигурация:
{
"compilerOptions": {
"moduleResolution": "node16"
}
}
Структура exports позволяет TypeScript корректно:
При сборке библиотеки с Rollup обычно формируется следующая стратегия:
exports.Типичная схема:
src/index.js → dist/esm/index.jssrc/index.js → dist/cjs/index.jssrc/utils.js → dist/esm/utils.jssrc/utils.js → dist/cjs/utils.jsИ затем:
{
"type": "module",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./utils": {
"import": "./dist/esm/utils.js",
"require": "./dist/cjs/utils.js"
}
}
}
Несоответствие путей сборки и exports Если Rollup выводит файлы в
dist/esm, а exports указывает на
dist/es, импорт сломается.
Отсутствие require/import веток В средах Node.js без ESM поддержка может быть нарушена.
Смешивание внутренних и публичных модулей Если не все entry
points отражены в exports, структура становится
непредсказуемой.
Использование module вместе с exports
Поле module игнорируется при наличии exports в
Node.js-резолве.
exports фактически становится контрактом между
библиотекой и потребителем. Он фиксирует:
При использовании Rollup это превращается в централизованную модель, где:
exports определяет доступ;Такой подход делает библиотеку предсказуемой, устойчивой к рефакторингу и совместимой с современными инструментами экосистемы JavaScript.