Конфигурация package.json: main, module, exports, types

Файл package.json определяет способ подключения пакета, доступные точки входа, типы модулей, совместимость с CommonJS и ESM, а также экспорт TypeScript-типов. Для библиотек правильная настройка main, module, exports и types влияет на:

  • совместимость с Node.js;
  • поддержку bundler’ов;
  • tree shaking;
  • корректную работу TypeScript;
  • импорт в CommonJS и ES Modules;
  • безопасность внутренних файлов пакета;
  • поддержку IDE и автодополнения.

Ошибки в конфигурации приводят к дублированию бандлов, невозможности импорта, конфликтам ESM/CJS и проблемам типов.


Поле main

Назначение

Поле main определяет основной entry point пакета для CommonJS.

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

При использовании:

const lib = require('my-library');

Node.js ищет:

  1. exports
  2. main

Если exports отсутствует, используется main.


Типичный сценарий

Структура:

dist/
 ├─ index.js
 └─ utils.js

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

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

Импорт:

const lib = require('my-library');

Связь с CommonJS

Обычно main указывает на CJS-сборку:

{
  "main": "./dist/index.cjs"
}

или:

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

если проект использует CommonJS по умолчанию.


Поведение в старых bundler’ах

Старые версии:

  • Webpack
  • Rollup
  • Parcel
  • Browserify

используют main как основной источник пакета.


Ограничения main

Поле не умеет:

  • разделять ESM и CJS;
  • описывать subpath exports;
  • скрывать внутренние файлы;
  • задавать условные импорты;
  • описывать browser/node версии.

Для современных библиотек одного main недостаточно.


Поле module

Назначение

Поле module используется bundler’ами для указания ESM-версии библиотеки.

{
  "module": "./dist/index.esm.js"
}

Основная идея

CommonJS:

const lib = require('lib');

ESM:

import lib from 'lib';

Bundler предпочитает module, потому что ESM:

  • поддерживает tree shaking;
  • позволяет анализировать зависимости статически;
  • уменьшает размер бандла.

Типичная конфигурация

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

Как Webpack использует module

Webpack анализирует поля:

{
  "main": "...",
  "module": "..."
}

Приоритет обычно такой:

  1. browser
  2. module
  3. main

Если существует ESM-версия, Webpack использует её.


Пример tree shaking

ESM:

export function add() {}
export function sub() {}

Импорт:

import { add } from 'lib';

Webpack может удалить sub.


Проблемы поля module

Поле module не является официальным стандартом Node.js.

Его поддерживают:

  • Webpack
  • Rollup
  • Vite
  • esbuild

Но Node.js игнорирует module.


Конфликт main и module

Проблемная конфигурация:

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

Если файл содержит CommonJS:

module.exports = {};

bundler ожидает ESM и возникают ошибки анализа.


Поле types

Назначение

Поле types указывает TypeScript declaration file.

{
  "types": "./dist/index.d.ts"
}

Роль declaration files

TypeScript использует .d.ts для:

  • проверки типов;
  • автодополнения;
  • документации API;
  • анализа импортов.

Пример

Файл:

// index.d.ts

export function sum(a: number, b: number): number;

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

{
  "types": "./dist/index.d.ts"
}

Теперь IDE знает типы библиотеки.


Альтернативное поле typings

Исторически использовалось:

{
  "typings": "./dist/index.d.ts"
}

Сегодня предпочтительно:

{
  "types": "./dist/index.d.ts"
}

Генерация типов

TypeScript:

{
  "compilerOptions": {
    "declaration": true
  }
}

После сборки:

dist/
 ├─ index.js
 └─ index.d.ts

Совместимость с JavaScript-библиотеками

Даже если библиотека написана на JavaScript, можно публиковать типы:

{
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

Поле exports

Причины появления

main и module не решали:

  • условные экспорты;
  • поддержку ESM/CJS одновременно;
  • скрытие внутренних файлов;
  • subpath imports;
  • browser/node разделение.

Для этого появился exports.


Базовый синтаксис exports

Простейший вариант

{
  "exports": "./dist/index.js"
}

Эквивалент:

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

но с более строгим контролем.


Объект exports

{
  "exports": {
    ".": "./dist/index.js"
  }
}

Точка "." означает корневой импорт:

import lib from 'lib';

Subpath exports

Экспорт дополнительных модулей

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}

Теперь доступны:

import lib from 'lib';
import utils from 'lib/utils';

Защита внутренних файлов

Без exports пользователи могут делать:

import x from 'lib/internal/private.js';

С exports это запрещено:

{
  "exports": {
    ".": "./dist/index.js"
  }
}

Теперь доступны только объявленные entry points.


Conditional exports

Разделение ESM и CommonJS

Современная конфигурация:

{
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs"
    }
  }
}

Как это работает

ESM:

import lib from 'lib';

использует:

./dist/index.esm.js

CommonJS:

const lib = require('lib');

использует:

./dist/index.cjs

Поле type

Влияние на Node.js

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

{
  "type": "module"
}

означает:

  • .js трактуется как ESM;
  • .cjs — CommonJS.

Без type: module

По умолчанию:

  • .js = CommonJS;
  • .mjs = ESM.

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

Часто используют:

{
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

Browser exports

Разделение браузера и Node.js

{
  "exports": {
    ".": {
      "browser": "./dist/browser.js",
      "node": "./dist/node.js",
      "default": "./dist/browser.js"
    }
  }
}

Практическое применение

Node.js версия:

import fs from 'fs';

Browser версия:

fetch('/api');

Bundler выбирает нужный файл автоматически.


Поле default

Зачем нужно

default используется как fallback.

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.mjs"
    }
  }
}

Экспорт типов через exports

Поддержка TypeScript

Современный вариант:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

Преимущества

TypeScript получает типы напрямую из exports.

Это особенно важно при:

  • subpath exports;
  • monorepo;
  • dual package;
  • ESM-only библиотеках.

Subpath types

Типы для внутренних модулей

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js"
    }
  }
}

Импорт

import { helper } from 'lib/utils';

TypeScript автоматически найдёт:

dist/utils.d.ts

Dual package

Поддержка ESM и CJS одновременно

Типичная структура:

dist/
 ├─ index.cjs
 ├─ index.js
 ├─ index.d.ts
 └─ utils.js

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

{
  "type": "module",

  "main": "./dist/index.cjs",

  "module": "./dist/index.js",

  "types": "./dist/index.d.ts",

  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

Почему main всё ещё используют

Несмотря на exports, main часто сохраняют для:

  • старых bundler’ов;
  • старых Node.js;
  • обратной совместимости.

ESM-only пакеты

Современный подход

Многие библиотеки публикуются только как ESM.

Пример:

{
  "type": "module",

  "exports": {
    ".": "./dist/index.js"
  }
}

Ограничения

CommonJS:

require('lib');

перестаёт работать.


Типичная ошибка

Error [ERR_REQUIRE_ESM]

Экспорт package.json

Почему это важно

После включения exports файл package.json становится недоступным.

Ошибка:

import pkg from 'lib/package.json';

Решение

{
  "exports": {
    ".": "./dist/index.js",
    "./package.json": "./package.json"
  }
}

Wildcard exports

Шаблонные пути

{
  "exports": {
    "./features/*": "./dist/features/*.js"
  }
}

Импорт

import x from 'lib/features/math';

Ошибки при настройке exports

Ошибка несовместимости расширений

Проблема:

{
  "type": "module"
}

Файл:

module.exports = {};

Node.js интерпретирует файл как ESM.


Ошибка отсутствующего require

{
  "exports": {
    ".": {
      "import": "./dist/index.js"
    }
  }
}

CommonJS:

require('lib');

ломается.


Ошибка скрытых файлов

После добавления exports:

import x from 'lib/utils';

может перестать работать.

Нужно явно экспортировать subpath:

{
  "exports": {
    "./utils": "./dist/utils.js"
  }
}

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

Приоритет полей

Webpack 5 обычно анализирует:

  1. exports
  2. browser
  3. module
  4. main

Tree shaking

Лучший вариант для tree shaking:

{
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js"
    }
  }
}

Совместимость с Rollup и Vite

Rollup и Vite предпочитают:

  • ESM;
  • exports;
  • module.

CommonJS используется как fallback.


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

moduleResolution

Современные режимы:

{
  "compilerOptions": {
    "moduleResolution": "NodeNext"
  }
}

или:

{
  "compilerOptions": {
    "moduleResolution": "Bundler"
  }
}

TypeScript начинает учитывать:

  • exports;
  • conditional exports;
  • ESM/CJS;
  • subpath imports.

Полноценная современная конфигурация

Универсальный вариант библиотеки

{
  "name": "my-library",

  "type": "module",

  "main": "./dist/index.cjs",

  "module": "./dist/index.js",

  "types": "./dist/index.d.ts",

  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },

    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    },

    "./package.json": "./package.json"
  },

  "sideEffects": false
}

Назначение sideEffects

{
  "sideEffects": false
}

Сообщает bundler’у, что модули безопасно удалять при tree shaking.


Сравнение main, module и exports

Поле Назначение Поддержка Node.js Поддержка bundler
main CommonJS entry Да Да
module ESM для bundler Нет Да
exports Современная система экспортов Да Да
types TypeScript types TypeScript TypeScript

Рекомендуемые схемы

Только CommonJS

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

CommonJS + ESM

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

Современный dual package

{
  "type": "module",

  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

ESM-only

{
  "type": "module",

  "exports": {
    ".": "./dist/index.js"
  }
}