Перейти к содержанию

Модули: пакеты

История
Версия Изменения
v14.13.0, v12.20.0 Добавьте поддержку шаблонов экспорта.
v14.6.0, v12.19.0 Добавьте поле «импорт» пакета.
v13.7.0, v12.17.0 Снимите флажок условного экспорта.
v13.7.0, v12.16.0 Удалите опцию --experimental-conditional-exports. В версии 12.16.0 условный экспорт по-прежнему отстает от --experimental-modules.
v13.6.0, v12.16.0 Снимите флажок со ссылки на пакет, используя его имя.
v12.7.0 Представьте поле «exports» «package.json» как более мощную альтернативу классическому полю «main».
v12.0.0 Добавьте поддержку модулей ES, используя расширение файла .js через поле "type" package.json`.

Введение

Пакет — это дерево каталогов, описанное файлом package.json. Пакет включает каталог с этим package.json и все подкаталоги до следующего каталога с другим package.json или до каталога с именем node_modules.

Здесь — рекомендации авторам пакетов по package.json и справочник по полям package.json, определённым в Node.js.

Определение системы модулей

Введение

Node.js будет считать следующее ES-модулями, если передать это в node как начальный ввод или сослаться через операторы import или выражения import():

  • файлы с расширением .mjs;

  • файлы с расширением .js, если ближайший родительский package.json содержит поле верхнего уровня "type" со значением "module";

  • строки, переданные в --eval или в node через STDIN с флагом --input-type=module;

  • код, который синтаксически разбирается только как ES-модули, например операторы import/export или import.meta, без явного указания интерпретации. Явные маркеры — расширения .mjs/.cjs, поле "type" в package.json со значениями "module" или "commonjs" или флаг --input-type. Динамические выражения import() допустимы и в CommonJS, и в ES-модулях и сами по себе не заставляют файл считаться ES-модулем. См. обнаружение синтаксиса.

Node.js будет считать следующее CommonJS, если передать это в node как начальный ввод или сослаться через import/import():

  • файлы с расширением .cjs;

  • файлы с расширением .js, если ближайший родительский package.json содержит поле "type" со значением "commonjs";

  • строки для --eval или --print, а также ввод через STDIN с флагом --input-type=commonjs;

  • файлы .js без родительского package.json или при отсутствии поля type в ближайшем package.json, если код успешно выполняется как CommonJS. Иными словами, Node.js сначала пытается выполнить такие «двусмысленные» файлы как CommonJS и повторно оценивает их как ES-модули, если разбор как CommonJS не удался из-за синтаксиса ES-модуля.

Использование синтаксиса ES-модулей в «двусмысленных» файлах даёт накладные расходы, поэтому рекомендуется везде, где возможно, быть явным. В частности, авторам пакетов следует всегда указывать поле "type" в package.json, даже если все исходники — CommonJS. Явный type защитит пакет, если когда-нибудь изменится тип по умолчанию в Node.js, и упростит работу сборщиков и загрузчиков при определении интерпретации файлов.

Обнаружение синтаксиса

История
Версия Изменения
v22.7.0, v20.19.0 Обнаружение синтаксиса включено по умолчанию.

Стабильность: 1.2 — кандидат на выпуск

Node.js просматривает исходный код двусмысленного ввода и, если обнаружен синтаксис ES-модуля, обрабатывает ввод как ES-модуль.

Двусмысленный ввод — это:

  • файлы с расширением .js или без расширения, при отсутствии управляющего package.json или при отсутствии в нём поля type;
  • строковый ввод (--eval или STDIN), если не указан --input-type.

Синтаксис ES-модуля — это синтаксис, который при выполнении как CommonJS привёл бы к ошибке. К нему относятся:

  • операторы import (но не выражения import() — они допустимы в CommonJS);
  • операторы export;
  • обращения к import.meta;
  • await на верхнем уровне модуля;
  • повторные лексические объявления переменных обёртки CommonJS (require, module, exports, __dirname, __filename).

Разрешение и загрузка модулей

У Node.js два вида разрешения и загрузки модулей — в зависимости от способа запроса.

Когда модуль запрашивается через require() (по умолчанию в CommonJS и может быть создан через createRequire() и в CommonJS, и в ES-модулях):

  • Разрешение:
    • разрешение через require() поддерживает папки как модули;
    • при отсутствии точного совпадения require() подставляет расширения (.js, .json, затем .node) и снова пытается разрешить папки как модули;
    • по умолчанию URL в качестве спецификаторов не поддерживаются.
  • Загрузка:
    • .json обрабатываются как JSON-текст;
    • .node — скомпилированные аддоны, загружаемые через process.dlopen();
    • .ts, .mts, .cts — как TypeScript;
    • прочие расширения или отсутствие расширения — как JavaScript-текст;
    • require() может загружать ES-модули из CommonJS только если ES-модуль и его зависимости синхронны (нет top-level await).

Когда модуль запрашивается статическим import (только в ES-модулях) или выражением import() (в CommonJS и ES-модулях):

  • Разрешение:
    • у import/import() нет «папок как модулей», индекс каталога (например './startup/index.js') задаётся полностью;
    • поиск расширений не выполняется: для относительного или абсолютного file URL нужно указать расширение;
    • по умолчанию поддерживаются спецификаторы file:// и data:.
  • Загрузка:
    • .json — JSON-текст; при импорте JSON-модулей нужен атрибут типа импорта (например import json from './data.json' with { type: 'json' });
    • .node — аддоны через process.dlopen(), если включён --experimental-addon-modules;
    • .ts, .mts, .cts — как TypeScript;
    • для JavaScript-текста допускаются только расширения .js, .mjs, .cjs;
    • .wasmWebAssembly-модули;
    • иные расширения дают ошибку ERR_UNKNOWN_FILE_EXTENSION; дополнительные расширения — через хуки настройки;
    • import/import() могут загружать JavaScript-модули CommonJS; для них используется merve, чтобы по возможности вывести именованные экспорты статическим анализом.

Независимо от способа запроса разрешение и загрузку можно настроить через хуки настройки.

package.json и расширения файлов

В пакете поле package.json "type" задаёт, как Node.js интерпретирует файлы .js. Без "type" файлы .js считаются CommonJS.

"type": "module" означает, что .js в этом пакете — ES module.

"type" действует не только на точку входа (node my-app.js), но и на файлы, подключаемые через import и import().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// my-app.js считается ES-модулем: рядом package.json с "type": "module".

import './startup/init.js';
// ES-модуль: в ./startup нет package.json, наследуется "type" с уровня выше.

import 'commonjs-package';
// CommonJS: у ./node_modules/commonjs-package/package.json нет "type" или "type": "commonjs".

import './node_modules/commonjs-package/index.js';
// То же: package.json пакета без "module" или с "commonjs".

Файлы .mjs всегда загружаются как ES modules, независимо от родительского package.json.

Файлы .cjs всегда загружаются как CommonJS, независимо от родительского package.json.

1
2
3
4
5
import './legacy-file.cjs';
// CommonJS: расширение .cjs всегда CommonJS.

import 'commonjs-package/src/index.mjs';
// ES-модуль: .mjs всегда ES-модуль.

Расширения .mjs и .cjs позволяют смешивать типы в одном пакете:

  • в пакете с "type": "module" отдельный файл можно пометить как CommonJS, давав ему .cjs.js, и .mjs в таком пакете — ES-модули);

  • в пакете с "type": "commonjs" отдельный файл можно пометить как ES module, давав ему .mjs.js, и .cjs в таком пакете — CommonJS).

Флаг --input-type

Строки для --eval/-e или из STDIN обрабатываются как ES modules, если задано --input-type=module.

1
2
3
node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"

echo "import { sep } from 'node:path'; console.log(sep);" | node --input-type=module

Также есть --input-type=commonjs для явного запуска строки как CommonJS. Если --input-type не указан, по умолчанию это поведение CommonJS.

Точки входа пакета

В package.json точки входа задают поля "main" и "exports". Оба подходят и для ES-модулей, и для CommonJS.

"main" поддерживается во всех версиях Node.js, но задаёт только главную точку входа.

"exports" — современная замена "main": несколько точек входа, условное разрешение в разных средах и запрет любых путей вне перечисленных в "exports". Так проще явно описать публичный API пакета.

Для новых пакетов под актуальные версии Node.js рекомендуется "exports". Для поддержки Node.js 10 и ниже нужен "main". Если заданы оба, "exports" имеет приоритет над "main" в поддерживаемых версиях.

Условные экспорты внутри "exports" задают разные точки входа по среде, включая require и import. Про совместимый CommonJS и ES в одном пакете см. раздел про dual-пакеты.

Если у существующего пакета появляется "exports", потребители не смогут использовать непроэкспортированные пути, в том числе package.json (например require('your-package/package.json')). Это обычно ломающее изменение.

Чтобы ввести "exports" без поломки, экспортируйте все ранее поддерживаемые пути; лучше явно перечислить точки входа. Например, если раньше были main, lib, feature и package.json, можно задать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "name": "my-package",
    "exports": {
        ".": "./lib/index.js",
        "./lib": "./lib/index.js",
        "./lib/index": "./lib/index.js",
        "./lib/index.js": "./lib/index.js",
        "./feature": "./feature/index.js",
        "./feature/index": "./feature/index.js",
        "./feature/index.js": "./feature/index.js",
        "./package.json": "./package.json"
    }
}

Либо экспортировать целые каталоги с шаблонами и с расширениями, и без:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "name": "my-package",
    "exports": {
        ".": "./lib/index.js",
        "./lib": "./lib/index.js",
        "./lib/*": "./lib/*.js",
        "./lib/*.js": "./lib/*.js",
        "./feature": "./feature/index.js",
        "./feature/*": "./feature/*.js",
        "./feature/*.js": "./feature/*.js",
        "./package.json": "./package.json"
    }
}

Так сохраняется обратная совместимость в минорных версиях; в следующем major можно сузить экспорты только до нужных путей:

1
2
3
4
5
6
7
8
{
    "name": "my-package",
    "exports": {
        ".": "./lib/index.js",
        "./feature/*.js": "./feature/*.js",
        "./feature/internal/*": null
    }
}

Экспорт главной точки входа

Для нового пакета рекомендуется поле "exports":

1
2
3
{
    "exports": "./index.js"
}

Если задано "exports", подпути пакета инкапсулированы и недоступны импортёрам. Например, require('pkg/subpath.js') даёт ERR_PACKAGE_PATH_NOT_EXPORTED.

Так проще гарантировать контракт API и semver; полной изоляции нет: прямой require('/path/to/node_modules/pkg/subpath.js') по-прежнему может загрузить файл.

Актуальные Node.js и сборщики поддерживают "exports". Для старых версий можно дублировать "main" тем же путём, что и "exports":

1
2
3
4
{
    "main": "./index.js",
    "exports": "./index.js"
}

Экспорт подпутей

При использовании поля "exports" можно задать пользовательские подпути наряду с главной точкой входа, считая главную точку входа подпутём ".":

1
2
3
4
5
6
{
    "exports": {
        ".": "./index.js",
        "./submodule.js": "./src/submodule.js"
    }
}

Тогда потребитель может импортировать только подпуть, явно заданный в "exports":

1
2
import submodule from 'es-module-package/submodule.js';
// Loads ./node_modules/es-module-package/src/submodule.js

Другие подпути приведут к ошибке:

1
2
import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED

Расширения в подпутях

Авторам пакетов следует указывать в экспорте либо подпути с расширением (import 'pkg/subpath.js'), либо без него (import 'pkg/subpath'). Так для каждого экспортируемого модуля остаётся единственный подпуть, все зависимости используют один и тот же спецификатор, контракт пакета ясен потребителям, а автодополнение подпутей упрощается.

Традиционно пакеты чаще использовали стиль без расширения: он удобнее читается и скрывает реальный путь к файлу внутри пакета.

Теперь, когда карты импорта задают стандарт разрешения пакетов в браузерах и других средах JavaScript, стиль без расширения может раздувать определения карт импорта. Явные расширения файлов помогают избежать этого, позволяя карте импорта использовать отображение папки пакетов и по возможности сопоставлять несколько подпутей вместо отдельной записи карты на каждый экспортируемый подпуть. Это же согласуется с требованием указывать полный путь спецификатора в относительных и абсолютных спецификаторах импорта.

Правила путей и проверка целей экспорта

Задавая пути как цели в поле "exports", Node.js применяет несколько правил ради безопасности, предсказуемости и инкапсуляции. Понимание этих правил важно для авторов, публикующих пакеты.

Цели должны быть относительными URL

Все целевые пути в карте "exports" (значения, сопоставленные ключам экспорта) должны быть строками относительных URL, начинающимися с ./.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// package.json
{
    "name": "my-package",
    "exports": {
        ".": "./dist/main.js", // Correct
        "./feature": "./lib/feature.js" // Correct
        // "./origin-relative": "/dist/main.js", // Incorrect: Must start with ./
        // "./absolute": "file:///dev/null", // Incorrect: Must start with ./
        // "./outside": "../common/util.js" // Incorrect: Must start with ./
    }
}

Причины такого поведения:

  • Безопасность: нельзя экспортировать произвольные файлы за пределами собственного каталога пакета.
  • Инкапсуляция: все экспортируемые пути разрешаются относительно корня пакета, пакет остаётся самодостаточным.
Без обхода каталогов и недопустимых сегментов

Цели экспорта не должны разрешаться в расположение вне корня пакета. Кроме того, сегменты пути вроде . (одна точка), .. (две точки) или node_modules (и их эквиваленты в URL-кодировании) как правило запрещены в строке target после начального ./ и в любой части subpath, подставляемой в шаблон цели.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// package.json
{
    "name": "my-package",
    "exports": {
        // ".": "./dist/../../elsewhere/file.js", // Invalid: path traversal
        // ".": "././dist/main.js",             // Invalid: contains "." segment
        // ".": "./dist/../dist/main.js",       // Invalid: contains ".." segment
        // "./utils/./helper.js": "./utils/helper.js" // Key has invalid segment
    }
}

Синтаксический сахар для exports

Если экспорт "." — единственный, поле "exports" допускает сокращённую запись: вместо объекта можно указать непосредственно значение "exports".

1
2
3
4
5
{
    "exports": {
        ".": "./index.js"
    }
}

эквивалентно:

1
2
3
{
    "exports": "./index.js"
}

Импорт подпутей

История
Версия Изменения
v25.4.0, v24.14.0 Разрешить импорт подпутей, начинающихся с #/.

Помимо поля "exports" в пакете есть поле "imports" — для приватных сопоставлений, которые действуют только для спецификаторов импорта изнутри самого пакета.

Записи в поле "imports" всегда должны начинаться с #, чтобы их можно было отличить от спецификаторов внешних пакетов.

Например, поле imports позволяет получить преимущества условных экспортов для внутренних модулей:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// package.json
{
    "imports": {
        "#dep": {
            "node": "dep-node-native",
            "default": "./dep-polyfill.js"
        }
    },
    "dependencies": {
        "dep-node-native": "^1.0.0"
    }
}

здесь import '#dep' не получает разрешение внешнего пакета dep-node-native (включая его собственные exports), а в других средах получает локальный файл ./dep-polyfill.js относительно пакета.

В отличие от поля "exports", поле "imports" допускает сопоставление с внешними пакетами.

Правила разрешения для поля imports в остальном аналогичны полю exports.

Шаблоны подпутей

История
Версия Изменения
v16.10.0, v14.19.0 Поддержка трейлеров шаблонов в поле «Импорт».
v16.9.0, v14.19.0 Поддержка шаблонов прицепов.

Для пакетов с небольшим числом экспортов или импортов рекомендуется явно перечислять каждый подпуть в exports. Если же подпутей очень много, это может раздувать package.json и усложнять сопровождение.

В таких случаях вместо этого можно использовать шаблоны подпутей экспорта:

1
2
3
4
5
6
7
8
9
// ./node_modules/es-module-package/package.json
{
    "exports": {
        "./features/*.js": "./src/features/*.js"
    },
    "imports": {
        "#internal/*.js": "./src/internal/*.js"
    }
}

Сопоставления с * раскрывают вложенные подпути как синтаксис простой подстановки строк.

Все вхождения * в правой части заменяются этим значением, в том числе если в нём есть разделители /.

1
2
3
4
5
6
7
8
import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y.js';
// Loads ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z.js';
// Loads ./src/internal/z.js

Это прямое статическое сопоставление и замена без особой обработки расширений файлов. Указание "*.js" с обеих сторон сопоставления ограничивает экспортируемые файлы пакета только JS.

Свойство статически перечислимых экспортов сохраняется и для шаблонов: отдельные экспорты пакета можно получить, рассматривая шаблон цели справа как глоб ** по списку файлов внутри пакета. Так как пути node_modules в целях exports запрещены, такое раскрытие опирается только на файлы самого пакета.

Чтобы исключить приватные подкаталоги из шаблонов, можно использовать цели null:

1
2
3
4
5
6
7
// ./node_modules/es-module-package/package.json
{
    "exports": {
        "./features/*.js": "./src/features/*.js",
        "./features/private-internal/*": null
    }
}
1
2
3
4
5
import featureInternal from 'es-module-package/features/private-internal/m.js';
// Throws: ERR_PACKAGE_PATH_NOT_EXPORTED

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

Условные экспорты

История
Версия Изменения
v13.7.0, v12.16.0 Снимите флажок условного экспорта.

Условные экспорты позволяют сопоставлять разные пути в зависимости от условий. Они поддерживаются и для импорта CommonJS, и для ES-модулей.

Например, пакет, который хочет отдавать разные точки входа для require() и import, можно описать так:

1
2
3
4
5
6
7
8
// package.json
{
    "exports": {
        "import": "./index-module.js",
        "require": "./index-require.cjs"
    },
    "type": "module"
}

Node.js реализует следующие условия; ниже они перечислены от более специфичных к менее специфичным — в таком порядке условия и следует задавать:

  • "node-addons" — по смыслу близко к "node", совпадает в любой среде Node.js. Его можно использовать для точки входа с нативными аддонами на C++ вместо более универсальной точки без нативных аддонов. Это условие можно отключить флагом --no-addons.
  • "node" — совпадает в любой среде Node.js. Цель может быть CommonJS или ES-модулем. В большинстве случаев явно выделять платформу Node.js не требуется.
  • "import" — совпадает, когда пакет загружают через import или import(), либо через любую операцию верхнего уровня импорта или разрешения загрузчика ECMAScript-модулей. Действует независимо от формата целевого файла. Всегда взаимоисключающе с "require".
  • "require" — совпадает, когда пакет загружают через require(). Указанный файл должен быть загружаем через require(), хотя условие совпадает независимо от формата цели. Ожидаемые форматы: CommonJS, JSON, нативные аддоны и ES-модули. Всегда взаимоисключающе с "import".
  • "module-sync" — совпадает независимо от того, загружают ли пакет через import, import() или require(). Ожидается формат ES-модулей без top-level await в графе модулей — иначе при require() будет выброшено ERR_REQUIRE_ASYNC_MODULE.
  • "default" — универсальный запасной вариант, всегда совпадает. Может указывать на CommonJS или ES-модуль. Это условие всегда должно идти последним.

В объекте "exports" порядок ключей важен. При сопоставлении условий более ранние записи имеют больший приоритет и перекрывают последующие. Обычно условия в объекте задают от более специфичных к менее специфичным.

Использование условий "import" и "require" связано с рисками; подробнее — в разделе о двойных пакетах CommonJS/ES-модулей.

Условие "node-addons" подходит для точки входа с нативными аддонами на C++. Его можно отключить флагом --no-addons. При "node-addons" имеет смысл рассматривать "default" как усиление с более универсальной точкой входа, например с WebAssembly вместо нативного аддона.

Условные экспорты можно распространять и на подпути, например:

1
2
3
4
5
6
7
8
9
{
    "exports": {
        ".": "./index.js",
        "./feature.js": {
            "node": "./feature-node.js",
            "default": "./feature.js"
        }
    }
}

Определяется пакет, в котором require('pkg/feature.js') и import 'pkg/feature.js' могут отдавать разные реализации в Node.js и в других средах JavaScript.

При ветвлении по средам по возможности всегда включайте условие "default". Оно гарантирует, что неизвестные среды JS смогут использовать универсальную реализацию и не придётся подстраиваться под уже существующие среды ради поддержки условных экспортов. Поэтому ветки "node" и "default" обычно предпочтительнее сочетания "node" и "browser".

Вложенные условия

Помимо прямых сопоставлений Node.js поддерживает вложенные объекты условий.

Например, чтобы задать пакет только с двойными точками входа для Node.js, но не для браузера:

1
2
3
4
5
6
7
8
9
{
    "exports": {
        "node": {
            "import": "./feature-node.mjs",
            "require": "./feature-node.cjs"
        },
        "default": "./feature.mjs"
    }
}

Условия по-прежнему сопоставляются по порядку, как при «плоских» условиях. Если у вложенного условия нет сопоставления, проверка продолжается по остальным условиям родителя. Так вложенные условия ведут себя как вложенные операторы if в JavaScript.

Разрешение пользовательских условий

При запуске Node.js пользовательские условия можно задать флагом --conditions:

1
node --conditions=development index.js

Тогда будет разрешаться условие "development" в импортах и экспортах пакетов, а существующие "node", "node-addons", "default", "import" и "require" — по правилам как обычно.

Произвольное число пользовательских условий задаётся повторением флага.

В типичных условиях допустимы только буквы и цифры; при необходимости разделители — :, - или =. Иные символы могут вызвать проблемы совместимости вне Node.js.

В Node.js у условий мало ограничений, в частности:

  1. Должен быть хотя бы один символ.
  2. Нельзя начинать с ., так как в некоторых контекстах допускаются относительные пути.
  3. Нельзя использовать , — часть CLI может воспринять это как список через запятую.
  4. Нельзя использовать целочисленные ключи вроде "10" — возможны неожиданные эффекты порядка ключей в объектах JS.

Определения условий сообщества

Строки условий, кроме "import", "require", "node", "module-sync", "node-addons" и "default", реализованных в ядре Node.js, по умолчанию игнорируются.

Другие платформы могут вводить свои условия; в Node.js пользовательские условия включаются флагом --conditions / -C.

Чтобы пользовательские условия пакетов использовались однозначно, ниже приведён список распространённых условий и их строгие определения для согласованности экосистемы.

  • "types" — системы типов могут по нему находить файл типов для данного экспорта. Это условие всегда должно идти первым.
  • "browser" — любая среда веб-браузера.
  • "development" — точка входа только для режима разработки, например с дополнительной отладочной информацией и более понятными сообщениями об ошибках. Всегда взаимоисключающе с "production".
  • "production" — точка входа для production-среды. Всегда взаимоисключающе с "development".

Для прочих сред определения платформенных ключей ведёт WinterCG в спецификации предложения Runtime Keys.

Новые определения условий можно добавить в этот список через pull request в документацию Node.js по этому разделу. Требования к включению нового определения:

  • Определение должно быть ясным и однозначным для всех реализаторов.
  • Должен быть чётко обоснован сценарий, зачем нужно условие.
  • Должно быть достаточно существующих реализаций в дикой природе.
  • Имя условия не должно конфликтовать с другим определением или широко используемым условием.
  • Публикация определения должна давать экосистеме выигрыш в согласовании, который иначе недостижим. Например, это не обязательно так для условий, специфичных для компании или приложения.
  • Пользователь Node.js ожидал бы увидеть условие в документации ядра. Хороший пример — "types": оно не очень подходит к предложению Runtime Keys, но хорошо смотрится в документации Node.js.

В будущем эти определения могут быть перенесены в отдельный реестр условий.

Самоссылка на пакет по имени

История
Версия Изменения
v13.6.0, v12.16.0 Снимите флажок со ссылки на пакет, используя его имя.

Внутри пакета значения из поля package.json "exports" можно запрашивать по имени пакета. Например, пусть package.json такой:

1
2
3
4
5
6
7
8
// package.json
{
    "name": "a-package",
    "exports": {
        ".": "./index.mjs",
        "./foo.js": "./foo.js"
    }
}

Тогда любой модуль в этом пакете может ссылаться на экспорт самого пакета:

1
2
// ./a-module.mjs
import { something } from 'a-package'; // Imports "something" from ./index.mjs.

Самоссылка возможна только если в package.json есть "exports", и разрешает импортировать только то, что разрешает это поле "exports"package.json). Поэтому приведённый ниже код для предыдущего пакета вызовет ошибку выполнения:

1
2
3
4
5
6
// ./another-module.mjs

// Imports "another" from ./m.mjs. Fails because
// the "package.json" "exports" field
// does not provide an export named "./m.mjs".
import { another } from 'a-package/m.mjs';

Самоссылка также работает с require и в ES-модуле, и в CommonJS. Например, такой код тоже допустим:

1
2
// ./a-module.js
const { something } = require('a-package/foo.js'); // Loads from ./foo.js.

Наконец, самоссылка работает и для пакетов со scope. Например, сработает такой код:

1
2
3
4
5
// package.json
{
    "name": "@my/package",
    "exports": "./index.js"
}
1
2
// ./index.js
module.exports = 42;
1
2
// ./other.js
console.log(require('@my/package'));
1
2
$ node other.js
42

Двойные пакеты CommonJS/ES-модулей

Подробности — в репозитории примеров пакетов.

Определения полей package.json в Node.js

Здесь описаны поля, которые использует среда выполнения Node.js. Другие инструменты (например npm) используют дополнительные поля: Node.js их игнорирует, и они здесь не документируются.

В Node.js используются следующие поля package.json:

  • "name" — нужно для именованных импортов внутри пакета; менеджеры пакетов используют его как имя пакета.
  • "main" — модуль по умолчанию при загрузке пакета по имени, если не задано exports, а также в версиях Node.js до появления exports.
  • "type" — тип пакета: как загружать файлы .js — как CommonJS или как ES-модули.
  • "exports" — экспорт пакета и условные экспорты; если задано, ограничивает, какие подмодули можно загрузить из пакета.
  • "imports" — импорты пакета для модулей внутри самого пакета.

"name"

История
Версия Изменения
v13.6.0, v12.16.0 Удалите опцию --experimental-resolve-self.
1
2
3
{
    "name": "package-name"
}

Поле "name" задаёт имя пакета. Для публикации в реестре npm имя должно удовлетворять определённым требованиям.

Поле "name" можно использовать вместе с "exports" для самоссылки на пакет по его имени.

"main"

1
2
3
{
    "main": "./index.js"
}

Поле "main" задаёт точку входа пакета при импорте по имени через поиск в node_modules. Его значение — путь.

Если задано поле "exports", оно имеет приоритет над "main" при импорте пакета по имени.

Оно же задаёт файл, который подставляется при загрузке каталога пакета через require().

1
2
// This resolves to ./path/to/directory/index.js.
require('./path/to/directory');

"type"

Добавлено в: v12.0.0

История
Версия Изменения
v13.2.0, v12.17.0 Снимите флаг --experimental-modules.

Поле "type" задаёт формат модулей, который Node.js применяет ко всем файлам .js, у которых ближайший родительский файл — этот package.json.

Файлы с расширением .js загружаются как ES-модули, если ближайший родительский package.json содержит поле верхнего уровня "type" со значением "module".

Ближайший родительский package.json — это первый найденный package.json при подъёме от текущей папки к родительским, пока не встретится каталог node_modules или корень тома.

1
2
3
4
// package.json
{
    "type": "module"
}
1
2
# In same folder as preceding package.json
node my-app.js # Runs as ES module

Если у ближайшего родительского package.json нет поля "type" или указано "type": "commonjs", файлы .js обрабатываются как CommonJS. Если дошли до корня тома и package.json не найден, файлы .js тоже считаются CommonJS.

Операторы import для файлов .js обрабатываются как ES-модули, если ближайший родительский package.json содержит "type": "module".

1
2
// my-app.js, part of the same example as above
import './startup.js'; // Loaded as ES module because of package.json

Независимо от значения "type" файлы .mjs всегда обрабатываются как ES-модули, а .cjs — как CommonJS.

"exports"

Добавлено в: v12.7.0

История
Версия Изменения
v14.13.0, v12.20.0 Добавьте поддержку шаблонов экспорта.
v13.7.0, v12.17.0 Снимите флажок условного экспорта.
v13.7.0, v12.16.0 Реализуйте логический условный порядок экспорта.
v13.7.0, v12.16.0 Удалите опцию --experimental-conditional-exports. В версии 12.16.0 условный экспорт по-прежнему отстает от --experimental-modules.
v13.2.0, v12.16.0 Реализуйте условный экспорт.
1
2
3
{
    "exports": "./index.js"
}

Поле "exports" задаёт точки входа пакета при импорте по имени через поиск в node_modules или через самоссылку по имени пакета. Поддерживается в Node.js 12+ как альтернатива "main": можно описать экспорт подпутей и условные экспорты, скрывая внутренние неэкспортируемые модули.

Условные экспорты внутри "exports" позволяют задавать разные точки входа по среде, в том числе в зависимости от того, обращаются к пакету через require или через import.

Все пути в "exports" должны быть относительными file URL, начинающимися с ./.

"imports"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// package.json
{
    "imports": {
        "#dep": {
            "node": "dep-node-native",
            "default": "./dep-polyfill.js"
        }
    },
    "dependencies": {
        "dep-node-native": "^1.0.0"
    }
}

Записи в поле imports должны быть строками, начинающимися с #.

Импорты пакета допускают сопоставление с внешними пакетами.

Это поле задаёт импорт подпутей для текущего пакета.

Комментарии