Spec-Zone.ru › Node.js 20 LTS

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

История
Версия Изменения
v14.13.0, v12.20.0

Добавлена поддержка "exports" шаблонов.

v14.6.0, v12.19.0

Добавлено поле пакета "imports".

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 расширения файлов через package.json "type" поле.

Введение

Пакет представляет собой древовидную структуру папок, описываемую файлом 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.

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

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

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

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

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

Помимо этих явных случаев, существуют другие случаи, когда Node.js по умолчанию выбирает одну из систем модулей или другую на основе значения флага --experimental-default-type:

  • Файлы, оканчивающиеся на .js или не имеющие расширения, если в той же папке или любой родительской папке нет файла package.json.

  • Файлы, оканчивающиеся на .js или не имеющие расширения, если ближайшее родительское поле package.json не содержит поля "type"; за исключением случаев, когда папка находится внутри папки node_modules (области пакетов в node_modules всегда обрабатываются как CommonJS, когда файл package.json не содержит поля "type", независимо от --experimental-default-type, для обратной совместимости).

  • Строки, переданные в качестве аргумента --eval или перенаправленные в node через STDIN, когда --input-type не указан.

Этот флаг в настоящее время по умолчанию равен "commonjs", но в будущем он может быть изменён на "module". По этой причине рекомендуется использовать явные значения всякий раз, когда это возможно; в частности, авторы пакетов всегда должны включать поле "type" в свои файлы package.json даже в пакетах, где все источники являются CommonJS. Явное указание типа пакета защитит пакет в случае изменения значения по умолчанию в Node.js, а также упростит работу инструментам сборки и загрузчикам при определении способа интерпретации файлов пакета.

Загрузчики модулей

Node.js имеет две системы для разрешения спецификатора и загрузки модулей.

Существует загрузчик модулей CommonJS:

  • Он полностью синхронный.
  • Он отвечает за обработку require() вызовов.
  • Он поддаётся подмене.
  • Он поддерживает папки в качестве модулей.
  • При разрешении спецификатора, если точное совпадение не найдено, он попытается добавить расширения (.js, .json, и, наконец, .node) и затем попытается разрешить папки в качестве модулей.
  • Он обрабатывает .json как текстовые файлы JSON.
  • Файлы .node интерпретируются как скомпилированные модули дополнений, загруженные с помощью process.dlopen().
  • Он обрабатывает все файлы, у которых отсутствуют расширения .json или .node, как текстовые файлы JavaScript.
  • Он не может использоваться для загрузки модулей ECMAScript (хотя возможно загружать модули ECMAScript из модулей CommonJS). При использовании для загрузки текстового файла JavaScript, который не является модулем ECMAScript, он загружает его как модуль CommonJS.

Существует загрузчик модулей ECMAScript:

  • Он асинхронный.
  • Он отвечает за обработку import операторов и import() выражений.
  • Он не поддаётся подмене, может настраиваться с помощью загрузчиков.
  • Он не поддерживает папки в качестве модулей, индексы каталогов (например, './startup/index.js') должны быть полностью указаны.
  • Он не выполняет поиск расширений. Расширение файла должно быть указано, когда спецификатор представляет собой относительный или абсолютный URL файла.
  • Он может загружать модули JSON, но требуется утверждение импорта.
  • Он принимает только расширения .js, .mjs, и .cjs для текстовых файлов JavaScript.
  • Он может использоваться для загрузки модулей JavaScript CommonJS. Такие модули пропускаются через cjs-module-lexer для попытки идентифицировать именованные экспорты, которые доступны, если их можно определить при статическом анализе. Импортированные модули CommonJS преобразуют свои URL в абсолютные пути, а затем загружаются с помощью загрузчика модулей CommonJS.

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

В рамках пакета поле package.json "type" определяет, как Node.js должен интерпретировать файлы .js. Если у файла package.json нет поля "type" , файлы .js обрабатываются как CommonJS.

Значение поля package.json "type" равно "module" Node.js интерпретирует файлы .js в рамках этого пакета как использующие синтаксис ES-модулей.

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

// my-app.js, treated as an ES module because there is a package.json
// file in the same folder with "type": "module".

import './startup/init.js';
// Loaded as ES module since ./startup contains no package.json file,
// and therefore inherits the "type" value from one level up.

import 'commonjs-package';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".

import './node_modules/commonjs-package/index.js';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs". copy

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

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

import './legacy-file.cjs';
// Loaded as CommonJS since .cjs is always loaded as CommonJS.

import 'commonjs-package/src/index.mjs';
// Loaded as ES module since .mjs is always loaded as ES module. copy

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

  • В пакете "type": "module", Node.js может быть настроен на интерпретацию определённого файла как CommonJS путём присвоения ему расширения .cjs (поскольку файлы с расширениями .js и .mjs обрабатываются как ES-модули в пакете "module").

  • В пакете "type": "commonjs", Node.js может быть настроен на интерпретацию определённого файла как ES-модуля путём присвоения ему расширения .mjs (поскольку файлы с расширениями .js и .cjs обрабатываются как CommonJS в пакете "commonjs").

--input-type флаг

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

Строки, переданные в качестве аргумента в --eval (или -e) или перенаправленные в node через STDIN, обрабатываются как ES-модули, когда установлен флаг --input-type=module.

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 copy

Для полноты, также существует --input-type=commonjs, для явного запуска входных строк как CommonJS. Это поведение по умолчанию, если --input-type не указано.

Определение менеджера пакетов

Устойчивость: 1 - Экспериментальная

Хотя ожидается, что все проекты Node.js будут устанавливаться всеми менеджерами пакетов после публикации, их команды разработчиков часто обязаны использовать один конкретный менеджер пакетов. Чтобы упростить этот процесс, Node.js поставляется со средством, называемым Corepack, которое призвано сделать все менеджеры пакетов прозрачно доступными в вашей среде — при условии, что у вас установлен Node.js.

По умолчанию Corepack не будет навязывать какой-либо конкретный менеджер пакетов и будет использовать общие версии "Последний рабочий", связанные с каждым выпуском Node.js, но вы можете улучшить этот опыт, установив поле "packageManager" в вашем проекте package.json.

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

В файле package.json пакета могут быть определены две точки входа: "main" и "exports". Оба поля применимы как к точкам входа модулей ES, так и к модулям CommonJS.

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

Поле "exports" предоставляет современную альтернативу полю "main", позволяя определять несколько точек входа, поддерживать условное разрешение точек входа между средами и **предотвращать любые другие точки входа, кроме тех, которые определены в "exports"**. Эта оболочка позволяет авторам модулей четко определять публичный интерфейс своего пакета.

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

Условные экспорты могут быть использованы в "exports" для определения различных точек входа в пакет в зависимости от среды, включая то, ссылается ли на пакет require или import. Для получения дополнительной информации о поддержке как CommonJS, так и ES-модулей в одном пакете, см. раздел пакеты двойного типа CommonJS/ES-модулей.

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

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

{
  "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"
  }
} copy

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

{
  "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"
  }
} copy

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

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./feature/*.js": "./feature/*.js",
    "./feature/internal/*": null
  }
} copy

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

При написании нового пакета рекомендуется использовать поле "exports":

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

Когда поле "exports" определено, все подпути пакета оборачиваются и больше недоступны импортёрам. Например, require('pkg/subpath.js') вызывает ошибку ERR_PACKAGE_PATH_NOT_EXPORTED.

Эта оболочка экспортов обеспечивает более надёжные гарантии относительно интерфейсов пакетов для инструментов и при работе с обновлениями semver для пакета. Это не жёсткая оболочка, так как прямой require любого абсолютного подпути пакета, такого как require('/path/to/node_modules/pkg/subpath.js') по-прежнему загрузит subpath.js.

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

{
  "main": "./index.js",
  "exports": "./index.js"
} copy

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

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

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

{
  "exports": {
    ".": "./index.js",
    "./submodule.js": "./src/submodule.js"
  }
} copy

Теперь только определённые подпути в "exports" могут быть импортированы потребителем:

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

В то время как другие подпути вызовут ошибку:

import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED copy
Расширения в подпутях

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

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

С import maps, теперь предоставляющим стандарт для разрешения пакетов в браузерах и других средах выполнения JavaScript, использование стиля без расширения может привести к увеличению объёма определений import map. Явные расширения файлов могут избежать этой проблемы, позволяя import map использовать отображение папки пакетов для отображения нескольких подпутей, где это возможно, вместо отдельной записи карты для каждого экспорта подпути пакета. Это также отражает требование использования полного пути спецификатора в относительных и абсолютных спецификаторах импорта.

Экспорт сахара

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

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

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

может быть записано:

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

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

Добавлено в: v14.6.0, v12.19.0

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

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

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

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
} copy

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

В отличие от поля "exports", поле "imports" позволяет отображение на внешние пакеты.

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

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

История
Версия Изменения
v16.10.0, v14.19.0

Поддержка шаблонов-трейлеров в поле "imports".

v16.9.0, v14.19.0

Поддержка шаблонов-трейлеров.

v14.13.0, v12.20.0

Добавлено в: v14.13.0, v12.20.0

Для пакетов с небольшим количеством экспортов или импортов рекомендуется явно указывать каждую запись подпути экспорта. Но для пакетов с большим количеством подпутей это может привести к увеличению объёма package.json и проблемам с обслуживанием.

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

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js"
  },
  "imports": {
    "#internal/*.js": "./src/internal/*.js"
  }
} copy

*** карты экспортируют вложенные подпути, так как это синтаксис подстановки строк только.**

Все экземпляры * в правой части будут затем заменены этим значением, включая случай, когда он содержит разделители /.

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 ./node_modules/es-module-package/src/internal/z.js copy

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

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

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

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js",
    "./features/private-internal/*": null
  }
} copy
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 copy

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

История
Версия Изменения
v13.7.0, v12.16.0

Убрать условные экспорты.

v13.2.0, v12.16.0

Добавлено в: v13.2.0, v12.16.0

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

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

// package.json
{
  "exports": {
    "import": "./index-module.js",
    "require": "./index-require.cjs"
  },
  "type": "module"
} copy

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

  • "node-addons" — аналогично "node" и соответствует любой среде Node.js. Это условие может использоваться для предоставления точки входа, которая использует родные плагины C++, в отличие от точки входа, которая более универсальна и не полагается на родные плагины. Это условие можно отключить с помощью флага --no-addons.
  • "node" — соответствует любой среде Node.js. Может быть файлом CommonJS или ES-модуля. В большинстве случаев явное указание платформы Node.js не требуется.
  • "import" — соответствует случаю, когда пакет загружается с помощью import или import(), или с помощью любой операции импорта или разрешения верхнего уровня загрузчиком ES-модулей. Применяется независимо от формата модуля целевого файла. Всегда взаимно исключает "require".
  • "require" — соответствует случаю, когда пакет загружается с помощью require(). Ссылаемый файл должен быть загружаем с помощью require(), хотя условие соответствует независимо от формата модуля целевого файла. Ожидаемые форматы включают CommonJS, JSON и родные плагины, но не ES-модули, так как require() их не поддерживает. Всегда взаимно исключает "import".
  • "default" — универсальное исключительное условие, всегда соответствующее. Может быть файлом CommonJS или ES-модуля. Это условие должно быть всегда последним.

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

Использование условий "import" и "require" может привести к некоторым проблемам, которые подробно описаны в разделе dual CommonJS/ES module packages.

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

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

{
  "exports": {
    ".": "./index.js",
    "./feature.js": {
      "node": "./feature-node.js",
      "default": "./feature.js"
    }
  }
} copy

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

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

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

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

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

{
  "exports": {
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
    "default": "./feature.mjs"
  }
} copy

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

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

Добавлено в: v14.9.0, v12.19.0

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

node --conditions=development index.js copy

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

Любое количество пользовательских условий может быть задано с помощью повторяющихся флагов.

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

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

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

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

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

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

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

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

Вышеперечисленные определения могут быть перенесены в специализированный реестр условий в будущем.

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

История
Версия Изменения
v13.6.0, v12.16.0

Отменить самоссылку на пакет по его имени.

v13.1.0, v12.16.0

Добавлено в: v13.1.0, v12.16.0

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

// package.json
{
  "name": "a-package",
  "exports": {
    ".": "./index.mjs",
    "./foo.js": "./foo.js"
  }
} copy

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

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

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

// ./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'; copy

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

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

Наконец, самоссылка также работает с пакетами с префиксом. Например, этот код также будет работать:

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

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

До появления поддержки ES модулей в Node.js, авторы пакетов часто включали в свои пакеты как CommonJS, так и ES модули JavaScript, используя package.json "main" для указания точки входа CommonJS и package.json "module" для указания точки входа ES модуля. Это позволяло Node.js запускать точку входа CommonJS, а инструментам сборки, таким как бандлеры, использовать точку входа ES модуля, так как Node.js игнорировал (и по-прежнему игнорирует) поле "module" верхнего уровня.

Теперь Node.js может запускать точки входа ES модулей, и пакет может содержать как точки входа CommonJS, так и ES модулей (либо через отдельные спецификаторы, такие как 'pkg' и 'pkg/es-module', либо оба в одном спецификаторе через условные экспорты). В отличие от сценария, где "module" используется только бандлерами, или ES модули транспилируются в CommonJS на лету перед оценкой Node.js, файлы, на которые ссылается точка входа ES модуля, оцениваются как ES модули.

Проблема с двойными пакетами

Когда приложение использует пакет, предоставляющий как CommonJS, так и ES модули, существует риск возникновения определенных ошибок, если загружаются обе версии пакета. Это происходит из-за того, что pkgInstance, созданный const pkgInstance = require('pkg'), отличается от pkgInstance, созданного import pkgInstance from 'pkg' (или альтернативного основного пути, например, 'pkg/module'). Это и есть «проблема с двойными пакетами», когда две версии одного пакета могут загружаться в одной среде выполнения. Хотя маловероятно, что приложение или пакет намеренно загрузит обе версии напрямую, часто случается, что приложение загружает одну версию, а зависимость приложения загружает другую. Это происходит потому, что Node.js поддерживает смешивание CommonJS и ES модулей, и может привести к неожиданному поведению.

Если основной экспорт пакета — это конструктор, то сравнение экземпляров, созданных двумя версиями, возвращает instanceof, а если экспорт — это объект, то свойства, добавленные к одному (например, pkgInstance.foo = 3), отсутствуют в другом. Это отличается от того, как работают операторы import и require в средах с использованием только CommonJS или только ES модулей соответственно, и поэтому вызывает удивление у пользователей. Это также отличается от поведения, к которому привыкли пользователи при использовании транспиляции с помощью инструментов, таких как Babel или esm.

Создание двойных пакетов с уменьшением или исключением проблем

Во-первых, проблема, описанная в предыдущем разделе, возникает, когда пакет содержит как CommonJS, так и ES модули, и оба типа источников предназначены для использования в Node.js, либо через отдельные главные точки входа, либо через экспортируемые пути. Вместо этого пакет можно написать так, чтобы любая версия Node.js получала только CommonJS источники, а отдельные ES модули, которые могут содержаться в пакете, предназначены только для других сред, таких как браузеры. Такой пакет будет пригоден для любой версии Node.js, так как import может ссылаться на файлы CommonJS; но он не предоставит никаких преимуществ использования синтаксиса ES модулей.

Пакет также может перейти от CommonJS к ES модулям в обновлении версии с изменением совместимости. Это имеет недостаток, что самая последняя версия пакета будет работоспособна только в поддерживающих ES модули версиях Node.js.

Каждая методика имеет свои компромиссы, но существует два основных подхода, которые удовлетворяют следующим условиям:

  1. Пакет пригоден для использования как в require, так и в import.
  2. Пакет пригоден для использования как в текущей Node.js, так и в более старых версиях Node.js, не поддерживающих ES модули.
  3. Главная точка входа пакета, например 'pkg', может использоваться как require, чтобы разрешить ссылку на файл CommonJS, так и import, чтобы разрешить ссылку на файл ES модуля. (Аналогично и для экспортируемых путей, например, 'pkg/feature').
  4. Пакет предоставляет именованные экспорты, например import { name } from 'pkg', а не import pkg from 'pkg'; pkg.name.
  5. Пакет, возможно, пригоден для использования в других средах ES модулей, таких как браузеры.
  6. Проблемы, описанные в предыдущем разделе, исключаются или минимизируются.
Подход №1: Использование обертки ES модуля

Напишите пакет в CommonJS или транспилируйте ES модули в CommonJS, и создайте файл обертки ES модуля, который определяет именованные экспорты. С помощью условных экспортов оболочка ES модуля используется для import, а точка входа CommonJS для require.

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./wrapper.mjs",
    "require": "./index.cjs"
  }
} copy

В приведенном примере используются явные расширения .mjs и .cjs. Если ваши файлы используют расширение .js, то "type": "module" приведет к тому, что такие файлы будут обрабатываться как ES модули, точно так же, как "type": "commonjs" приведет к их обработке как CommonJS. См. Включение.

// ./node_modules/pkg/index.cjs
exports.name = 'value'; copy
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name; copy

В этом примере name из import { name } from 'pkg' — это тот же синглтон, что и name из const { name } = require('pkg') . Поэтому === возвращает true при сравнении двух name и проблема с различными спецификаторами избегается.

Если модуль не просто список именованных экспортов, а содержит уникальный экспорт функции или объекта, например module.exports = function () { ... }, или если желательно иметь поддержку в оболочке для import pkg from 'pkg' шаблона, то оболочка вместо этого будет экспортировать по умолчанию дополнительно к именованным экспортам:

import cjsModule from './index.cjs';
export const name = cjsModule.name;
export default cjsModule; copy

Этот подход подходит для следующих случаев:

  • Пакет сейчас написан на CommonJS, и автору не хотелось бы переписывать его на ES модули, но он хочет предоставить именованные экспорты для потребителей ES модулей.
  • У пакета есть другие зависимости, и конечный пользователь может установить как этот пакет, так и эти другие пакеты. Например, пакет utilities используется напрямую в приложении, а пакет utilities-plus добавляет несколько дополнительных функций к utilities. Поскольку оболочка экспортирует лежащие в основе файлы CommonJS, не имеет значения, написан ли utilities-plus на CommonJS или ES модулях; он будет работать в любом случае.
  • Пакет хранит внутреннее состояние, и автору пакета не хотелось бы переписывать пакет, чтобы изолировать управление состоянием. См. следующий раздел.

Вариант этого подхода, не требующий условных экспортов для потребителей, мог бы заключаться в добавлении экспорта, например, "./module", для указания на версию пакета, полностью написанного на ES модулях. Он может использоваться через import 'pkg/module' пользователями, уверенными, что версия CommonJS не будет загружена нигде в приложении, например, зависимостями; или если версия CommonJS может загружаться, но не влияет на версию ES модулей (например, потому что пакет бессостоятельный):

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./wrapper.mjs"
  }
} copy
Подход №2: Изоляция состояния

package.json файл может определять отдельные точки входа CommonJS и ES модулей напрямую:

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  }
} copy

Это можно сделать, если версии пакета CommonJS и ES модулей эквивалентны, например, если одна является транспилированным выводом другой; и управление состоянием пакета тщательно изолировано (или пакет бессостоятельный).

Причина, по которой состояние является проблемой, заключается в том, что и версии пакета CommonJS, и ES модулей могут быть использованы в приложении; например, код приложения пользователя может использовать версию ES модуля, в то время как зависимость использует версию CommonJS. Если это произойдёт, в памяти будут загружены две копии пакета, а значит, будет присутствовать два отдельных состояния. Это, скорее всего, приведёт к труднодиагностируемым ошибкам.

Помимо написания бессостоятельного пакета (если бы JavaScript's Math был пакетом, например, он был бы бессостоятельным, так как все его методы статические), есть несколько способов изолировать состояние так, чтобы оно разделялось между потенциально загруженными экземплярами пакета CommonJS и ES модулей:

  1. Если возможно, поместите всё состояние внутри экземпляра объекта. Например, JavaScript's Date, для работы со своим состоянием, должен быть экземпляризован; если бы это был пакет, он использовался бы так:

    import Date from 'date';
    const someDate = new Date();
    // someDate contains state; Date does not copy

    Ключевое слово new не требуется; функция пакета может возвращать новый объект или модифицировать переданный объект, чтобы сохранить состояние вне пакета.

  2. Изолируйте состояние в одном или нескольких файлах CommonJS, которые разделяются между версиями пакета CommonJS и ES модулей. Например, если точки входа CommonJS и ES модулей — index.cjs и index.mjs соответственно:

    // ./node_modules/pkg/index.cjs
    const state = require('./state.cjs');
    module.exports.state = state; copy
    // ./node_modules/pkg/index.mjs
    import state from './state.cjs';
    export {
      state,
    }; copy

    Даже если pkg используется как через require, так и через import в приложении (например, через import в коде приложения и через require зависимостью), каждый экземпляр pkg будет содержать то же состояние; и изменение состояния из любой системы модулей будет применимо к обеим.

Любые плагины, которые прикрепляются к синглтону пакета, должны отдельно прикрепляться как к синглтонам CommonJS, так и к синглтонам ES модулей.

Этот подход подходит для следующих случаев:

  • Пакет написан на ES модулях, и автору пакета хотелось бы, чтобы эта версия использовалась там, где такой синтаксис поддерживается.
  • Пакет бессостоятельный, или его состояние можно изолировать без особых трудностей.
  • Вероятность наличия других общедоступных пакетов, зависящих от этого, невелика, или, если такие пакеты есть, пакет бессостоятельный, или его состояние не должно разделяться с зависимостями или с приложением в целом.

Даже с изолированным состоянием всё равно есть затраты на возможную дополнительную обработку кода между версиями пакета CommonJS и ES модулей.

Как и в предыдущем подходе, вариант этого подхода, не требующий условных экспортов для потребителей, мог бы заключаться в добавлении экспорта, например "./module", для указания на версию пакета, полностью написанного на ES модулях:

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./index.mjs"
  }
} copy

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

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

Следующие поля в файлах package.json используются в Node.js:

  • "name" - Актуально при использовании именованных импортов в пакете. Также используется менеджерами пакетов как имя пакета.
  • "main" - Модуль по умолчанию при загрузке пакета, если exports не указан, и в версиях Node.js до появления exports.
  • "packageManager" - Рекомендуемый менеджер пакетов при внесении вклада в пакет. Используется плагинами Corepack.
  • "type" - Тип пакета, определяющий, как загружать файлы .js как CommonJS или ES модули.
  • "exports" - Экспорт пакета и условный экспорт. При наличии, ограничивает, какие подмодули можно загрузить из пакета.
  • "imports" - Импорты пакета, для использования модулями внутри самого пакета.

"name"

История
Версия Изменения
v13.6.0, v12.16.0

Удалить опцию --experimental-resolve-self.

v13.1.0, v12.16.0

Добавлен в: v13.1.0, v12.16.0

  • Тип: <строка>
{
  "name": "package-name"
} copy

Поле "name" определяет имя вашего пакета. Публикация в реестре npm требует имени, удовлетворяющего определенным требованиям.

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

"main"

Добавлен в: v0.4.0
  • Тип: <строка>
{
  "main": "./index.js"
} copy

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

Когда у пакета есть поле "exports", это поле будет иметь приоритет над полем "main" при импорте пакета по имени.

Также оно определяет скрипт, используемый при загрузке каталога пакета по пути require().

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

"packageManager"

Добавлен в: v16.9.0, v14.19.0
Уровень стабильности: 1 - Экспериментально
  • Тип: <строка>
{
  "packageManager": "<package manager name>@<version>"
} copy

Поле "packageManager" определяет, какой менеджер пакетов ожидается при работе с текущим проектом. Оно может быть установлено на любой из поддерживаемых менеджеров пакетов и обеспечит использование вашей командой одних и тех же версий менеджеров пакетов, без необходимости установки чего-либо помимо Node.js.

Это поле в настоящее время экспериментальное и требует включения. Подробности о процедуре см. на странице Corepack.

"type"

История
Версия Изменения
v13.2.0, v12.17.0

Снять флаг --experimental-modules.

v12.0.0

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

  • Тип: <строка>

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

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

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

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

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

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

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

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

"exports"

История
Версия Изменения
v14.13.0, v12.20.0

Добавлена поддержка шаблонов "exports".

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

Реализация условных экспортов.

v12.7.0

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

  • Тип: <объект> | <строка> | <массив строк>
{
  "exports": "./index.js"
} copy

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

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

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

"imports"

Добавлен в: v14.6.0, v12.19.0
  • Тип: <объект>
// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
} copy

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

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

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

© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v20.x/docs/api/packages.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API