Spec-Zone.ru › Node.js

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

История
Версия Изменения
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. Явное указание типа type пакета защитит его от будущих изменений поведения Node.js по умолчанию, а также упростит определение способа интерпретации файлов в пакете инструментами сборки и загрузчиками.

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

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

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

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

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

  • Он асинхронный, если он не используется для загрузки модулей для require().
  • Он отвечает за обработку операторов 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') в своих экспортах. Это гарантирует, что для каждого экспортируемого модуля существует только один подпуть, чтобы все зависимые импортировали один и тот же согласованный идентификатор, сохраняя контракт пакета ясным для потребителей и упрощая завершение подпутей пакета.

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

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

Сахар для экспортов

Добавлен в: 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(), или с помощью любой операции импорта или разрешения верхнего уровня загрузчиком модулей ECMAScript. Применяется независимо от формата модуля целевого файла. Всегда взаимно исключающее с "require".
  • "require" - соответствует случаю, когда пакет загружается с помощью require(). Ссылаемый файл должен загружаться с помощью require(), хотя условие соответствует независимо от формата модуля целевого файла. Ожидаемые форматы включают CommonJS, JSON, нативные плагины и ES-модули, если --experimental-require-module включен. Всегда взаимно исключающее с "import".
  • "default" - универсальный резервный вариант, который всегда соответствует. Может быть файлом модуля CommonJS или ES. Это условие должно стоять всегда последним.

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

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

Условие "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 в спецификации предложения Ключи среды выполнения.

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

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

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

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

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

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

v13.1.0, v12.16.0

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

Внутри пакета значения, определенные в поле package.json пакета, могут быть обращены по имени пакета. Например, предположим, что 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 экземпляров, созданных двумя версиями, возвращает false, а если экспорт — это объект, свойства, добавленные к одному (например, 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-модулей могут использоваться в приложении; например, код приложения пользователя может import версию ES-модуля, в то время как зависимость require версию 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

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

В этом разделе описываются поля, используемые исполняемой средой 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/api/packages.html

Spec-Zone.ru

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