Поле exports у package.json визначає, які файли пакета можна імпортувати і який файл віддати залежно від умов (ES-модулі чи CommonJS, браузер чи Node.js, типи TypeScript).
Старий підхід - поле main (одна точка входу) плюс можливість імпортувати будь-який внутрішній файл: import x from 'lib/dist/internal/helpers.js'. Користувачі пакета покладалися на внутрішню структуру, і будь-яке перейменування файлу ламало їхній код.
З exports:
{
"name": "@acme/ui",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./button": {
"types": "./dist/button.d.ts",
"import": "./dist/button.js"
},
"./styles.css": "./dist/styles.css",
"./package.json": "./package.json"
}
}
import '@acme/ui'→dist/index.js, аrequire('@acme/ui')→dist/index.cjs;import '@acme/ui/button'- дозволена «підточка»;import '@acme/ui/dist/internal.js'→ помилкаERR_PACKAGE_PATH_NOT_EXPORTED. Внутрішні файли інкапсульовані.
Умови перевіряються в порядку, в якому записані в об'єкті, - перша відповідна перемагає:
types- для TypeScript, завжди першою;import/require- залежно від способу імпорту;browser,node,deno,worker- середовище (збирачі передаютьbrowser);development/production- режим (підтримують збирачі);default- запасний варіант, завжди останнім.
Шаблони: "./icons/*": "./dist/icons/*.js" - для пакетів з сотнями файлів.
Поле imports - дзеркальне: внутрішні псевдоніми для коду самого пакета, що починаються з #:
"imports": { "#utils/*": "./src/utils/*.js" }
import { slugify } from '#utils/strings';
Працює без налаштувань збирача - на відміну від псевдонімів @/ у Vite чи TypeScript.
Підводні камені:
- додавання
exportsдо існуючого пакета - ламаюча зміна: усі глибокі імпорти, які раніше працювали, перестануть. Тому це роблять у мажорній версії; - подвійний пакет (dual package hazard): якщо застосунок завантажить і ESM-, і CJS-версію пакета, у пам'яті буде два екземпляри - два різні класи, два синглтони,
instanceofміж ними не працює; - TypeScript читає
exportsлише зmoduleResolution: "node16"/"nodenext"/"bundler". Зі старимnodeвін бачить лишеtypes/main, і помилки типів з'являються лише у споживачів пакета.
Перевірка перед публікацією: інструмент @arethetypeswrong/cli і publint знаходять невідповідності між exports, файлами й типами.