Модули: Пакеты
Введение
Пакет представляет собой древовидную структуру папок, описываемую файлом package.json. Пакет состоит из папки, содержащей файл package.json и всех подпапок до следующей папки, содержащей другой файл package.json или папку с именем node_modules.
Эта страница предоставляет руководство для авторов пакетов, пишущих файлы package.json, а также справку по полям package.json, определённым Node.js.
Определение системы модулей
Node.js будет обрабатывать следующие как модули ES, когда они передаются в node в качестве начального ввода или когда на них ссылаются import операторы в коде модуля ES:
-
Файлы, заканчивающиеся на
.mjs. -
Файлы, заканчивающиеся на
.js, когда ближайший родительский файлpackage.jsonсодержит поле верхнего уровня"type"со значением"module". -
Строки, передаваемые в качестве аргумента
--eval, или перенаправляемые вnodeчерезSTDIN, со флагом--input-type=module.
Node.js будет обрабатывать все другие формы ввода, такие как файлы .js, где ближайший родительский файл package.json не содержит поля верхнего уровня "type", или строковый ввод без флага --input-type, как модули CommonJS. Это поведение сохраняет обратную совместимость. Однако, теперь, когда Node.js поддерживает как модули CommonJS, так и модули ES, желательно указывать тип явно при возможности. Node.js будет обрабатывать следующее как CommonJS, когда они передаются в node в качестве начального ввода или когда на них ссылаются import операторы в коде модуля ES:
-
Файлы, заканчивающиеся на
.cjs. -
Файлы, заканчивающиеся на
.js, когда ближайший родительский файлpackage.jsonсодержит поле верхнего уровня"type"со значением"commonjs". -
Строки, передаваемые в качестве аргумента
--evalили--print, или перенаправляемые вnodeчерезSTDIN, со флагом--input-type=commonjs.
Авторы пакетов должны включать поле "type", даже в пакетах, где все источники являются CommonJS. Явное указание типа пакета повысит стабильность пакета в случае изменения значения по умолчанию типа Node.js, а также упростит определение способа интерпретации файлов пакета инструментами сборки и загрузчиками.
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".
Файлы, заканчивающиеся на .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.
Расширения .mjs и .cjs могут использоваться для смешения типов в одном пакете:
-
В пакете
"type": "module", Node.js может быть настроен на интерпретацию конкретного файла как CommonJS путём присвоения ему расширения.cjs(так как файлы с расширениями.jsи.mjsв пакете"module"обрабатываются как модули ES). -
В пакете
"type": "commonjs", Node.js может быть настроен на интерпретацию конкретного файла как модуля ES путём присвоения ему расширения.mjs(так как файлы с расширениями.jsи.cjsв пакете"commonjs"обрабатываются как CommonJS).
--input-type флаг
Строки, переданные в качестве аргумента --eval (или -e), или перенаправляемые в node через STDIN, обрабатываются как модули ES, если установлен флаг --input-type=module.
node --input-type=module --eval "import { sep } from 'path'; console.log(sep);"
echo "import { sep } from 'path'; console.log(sep);" | node --input-type=module Для полноты, также существует --input-type=commonjs, для явного выполнения строкового ввода как CommonJS. Это поведение по умолчанию, если --input-type не указано.
Определение менеджера пакетов
Хотя ожидается, что все проекты Node.js будут устанавливаться всеми менеджерами пакетов после публикации, их разработчикам часто необходимо использовать один конкретный менеджер пакетов. Чтобы упростить этот процесс, Node.js поставляется с инструментом под названием Corepack, который призван обеспечить прозрачный доступ ко всем менеджерам пакетов в вашей среде — при условии, что у вас установлен Node.js.
По умолчанию Corepack не навязывает ни одного конкретного менеджера пакетов и будет использовать общие "последние известные хорошие" версии, связанные с каждым выпуском Node.js, но вы можете улучшить этот опыт, установив поле "packageManager" в файле package.json вашего проекта.
Точки входа в пакет
В файле package.json пакета, две поля могут определять точки входа в пакет: "main" и "exports". Поле "main" поддерживается во всех версиях Node.js, но его возможности ограничены: оно определяет только основную точку входа в пакет.
Поле "exports" предоставляет альтернативу полю "main", где основная точка входа в пакет может быть определена, а также упакован пакет, препятствуя любым другим точкам входа, кроме тех, которые определены в "exports". Эта инкапсуляция позволяет авторам модулей определить публичный интерфейс для своего пакета.
Если оба поля "exports" и "main" определены, поле "exports" имеет приоритет над полем "main". Поля "exports" не специфичны для ES-модулей или CommonJS; поле "main" переопределяется полем "exports", если оно существует. Таким образом, "main" не может использоваться в качестве резервного варианта для CommonJS, но может использоваться в качестве резервного варианта для устаревших версий Node.js, которые не поддерживают поле "exports".
Условные экспорты могут использоваться в поле "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-mod",
"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.js": "./feature/index.js",
"./package.json": "./package.json"
}
} Альтернативно, проект может выбрать экспорт целых папок:
{
"name": "my-mod",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/*": "./lib/*.js",
"./feature": "./feature/index.js",
"./feature/*": "./feature/*.js",
"./package.json": "./package.json"
}
} В качестве последнего средства, инкапсуляцию пакета можно полностью отключить, создав экспорт для корня пакета "./*": "./*". Это экспортирует все файлы пакета, но при этом отключается инкапсуляция и потенциальные преимущества, которые она предоставляет инструментам. Поскольку загрузчик ES-модулей в Node.js требует использования полного пути спецификатора, экспорт корня вместо явного указания точек входа менее выразителен, чем любой из предыдущих примеров. При этом теряется инкапсуляция, а потребители модулей не могут import feature from 'my-mod/feature' так как им необходимо указать полный путь import feature from 'my-mod/feature/index.js.
Экспорт основной точки входа
Для установки основной точки входа в пакет рекомендуется определить как "exports", так и "main" в файле package.json пакета:
{
"main": "./main.js",
"exports": "./main.js"
} Когда поле "exports" определено, все подпути пакета инкапсулируются и больше не доступны импортерам. Например, require('pkg/subpath.js') выбросит ошибку ERR_PACKAGE_PATH_NOT_EXPORTED.
Эта инкапсуляция экспортов обеспечивает более надёжные гарантии относительно интерфейсов пакетов для инструментов и при работе с обновлениями semver для пакета. Это не полная инкапсуляция, так как прямой импорт любого абсолютного подпути пакета, такого как require('/path/to/node_modules/pkg/subpath.js') все равно загрузит subpath.js.
Экспорт подпутей
При использовании поля "exports", пользовательские подпути могут быть определены вместе с основной точкой входа, рассматривая основную точку входа как "." подпуть:
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./submodule": "./src/submodule.js"
}
} Теперь только определённый подпуть в "exports" может быть импортирован потребителем:
import submodule from 'es-module-package/submodule'; // Loads ./node_modules/es-module-package/src/submodule.js
В то время как другие подпути вызовут ошибку:
import submodule from 'es-module-package/private-module.js'; // Throws ERR_PACKAGE_PATH_NOT_EXPORTED
Импорт подпутей
В дополнение к полю "exports", можно определить внутренние карты импорта пакета, которые применяются только к спецификаторам импорта из самого пакета.
Элементы в поле imports всегда должны начинаться с # для того, чтобы избежать неоднозначности со спецификаторами пакета.
Например, поле imports может быть использовано для получения преимуществ условных экспортов для внутренних модулей:
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
} где import '#dep' не получает разрешение внешнего пакета dep-node-native (включая его экспорты), а вместо этого получает локальный файл ./dep-polyfill.js относительно пакета в других средах.
В отличие от поля "exports", поле "imports" допускает сопоставление с внешними пакетами.
Правила разрешения для поля imports аналогичны правилам для поля exports.
Шаблоны подпутей
Для пакетов с небольшим количеством экспортов или импортов рекомендуется явно перечислять каждый экспорт подпути. Но для пакетов с большим количеством подпутей это может привести к package.json разбуханию и проблемам с обслуживанием.
Для таких случаев вместо этого можно использовать шаблоны подпутей экспорта:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*": "./src/features/*.js"
},
"imports": {
"#internal/*": "./src/internal/*.js"
}
} * отображают вложенные подпути, так как это синтаксис подстановки строк только.
Все вхождения * в правой части будут затем заменены этим значением, включая в него любые разделители /.
import featureX from 'es-module-package/features/x'; // Loads ./node_modules/es-module-package/src/features/x.js import featureY from 'es-module-package/features/y/y'; // Loads ./node_modules/es-module-package/src/features/y/y.js import internalZ from '#internal/z'; // Loads ./node_modules/es-module-package/src/internal/z.js
Это прямая статическая замена без специальной обработки расширений файлов. В предыдущем примере pkg/features/x.json будет разрешено в ./src/features/x.json.js в соответствии с отображением.
Свойство экспортов, являющихся статически перечисляемыми, сохраняется с шаблонами экспортов, так как отдельные экспорты для пакета могут быть определены путём рассмотрения правой части целевого шаблона как ** шаблона для списка файлов внутри пакета. Поскольку node_modules пути запрещены в целевых шаблонах экспортов, это расширение зависит только от файлов самого пакета.
Чтобы исключить частные подпапки из шаблонов, можно использовать null целевые шаблоны:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*": "./src/features/*.js",
"./features/private-internal/*": null
}
} import featureInternal from 'es-module-package/features/private-internal/m'; // Throws: ERR_PACKAGE_PATH_NOT_EXPORTED import featureX from 'es-module-package/features/x'; // Loads ./node_modules/es-module-package/src/features/x.js
Сопоставления подпапок
Перед поддержкой шаблонов подпутей использовался суффикс "/" для поддержки сопоставлений папок:
{
"exports": {
"./features/": "./features/"
}
} Эта функция будет удалена в будущих версиях.
Вместо этого используйте прямые шаблоны подпутей:
{
"exports": {
"./features/*": "./features/*.js"
}
} Преимущества шаблонов по сравнению с экспортом папок заключаются в том, что пакеты всегда могут импортироваться потребителями без необходимости в расширениях подпутей файлов.
Упрощение экспортов
Если экспорт "." является единственным экспортом, поле "exports" обеспечивает удобный синтаксис для этого случая, являясь прямым значением поля "exports".
Если экспорт "." имеет резервный массив или строковое значение, то поле "exports" может быть установлено в это значение напрямую.
{
"exports": {
".": "./main.js"
}
} может быть записано так:
{
"exports": "./main.js"
} Условные экспорты
Условные экспорты предоставляют способ сопоставления с различными путями в зависимости от определенных условий. Они поддерживаются как для импорта CommonJS, так и для ES-модулей.
Например, пакет, который хочет предоставить различные экспорты ES-модулей для require() и import может быть записан так:
// package.json
{
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
} Node.js реализует следующие условия:
-
"import"— совпадает, когда пакет загружается черезimportилиimport(), или через любой импорт верхнего уровня или операцию разрешения загрузчиком модулей ECMAScript. Применяется независимо от формата модуля целевого файла. Всегда взаимно исключает"require". -
"require"— совпадает, когда пакет загружается черезrequire(). Ссылаемый файл должен загружаться с помощьюrequire(), хотя условие соответствует независимо от формата модуля целевого файла. Ожидаемые форматы включают CommonJS, JSON и нативные плагины, но не модули ES, так какrequire()их не поддерживает. Всегда взаимно исключает"import". -
"node"— соответствует любой среде Node.js. Может быть файлом CommonJS или ES-модуля. Это условие должно всегда следовать за"import"или"require". -
"node-addons"— аналогично"node"и соответствует любой среде Node.js. Это условие может быть использовано для предоставления точки входа, использующей нативные C++ плагины, в отличие от точки входа, которая более универсальна и не зависит от нативных плагинов. Это условие может быть отключено с помощью флага--no-addons. -
"default"— универсальный резервный вариант, который всегда соответствует. Может быть файлом CommonJS или ES-модуля. Это условие должно всегда стоять последним.
Внутри объекта "exports" порядок ключей важен. При сопоставлении условий более ранние записи имеют более высокий приоритет и имеют преимущество перед более поздними. Общее правило состоит в том, что условия должны быть от наиболее специфичных к наименее специфичным в порядке объекта.
Использование условий "import" и "require" может привести к некоторым проблемам, которые подробно описаны в разделе двойных пакетов CommonJS/ES-модулей.
Условные экспорты также могут быть расширены до экспорта подпутей, например:
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./feature": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
} Определяет пакет, где require('pkg/feature') и import 'pkg/feature' могут предоставлять разные реализации в Node.js и других средах JS.
При использовании ветвей среды всегда включайте условие "default", где это возможно. Предоставление условия "default" гарантирует, что любые неизвестные среды JS могут использовать эту универсальную реализацию, что помогает избежать ситуациям, в которых этим средам JS приходится притворяться существующими средами, чтобы поддерживать пакеты с условными экспортами. По этой причине использование условий "node" и "default" обычно предпочтительнее использования условий "node" и "browser".
Вложенные условия
В дополнение к прямым соответствиям, Node.js также поддерживает вложенные объекты условий.
Например, чтобы определить пакет, который имеет только точки входа в двойном режиме для использования в Node.js, но не в браузере:
{
"main": "./main.js",
"exports": {
"node": {
"import": "./feature-node.mjs",
"require": "./feature-node.cjs"
},
"default": "./feature.mjs",
}
} Условия по-прежнему сопоставляются в порядке, как и с плоскими условиями. Если вложенное условие не имеет никакого соответствия, оно продолжит проверку оставшихся условий родительского условия. Таким образом, вложенные условия ведут себя аналогично вложенным операторам JavaScript if.
Разрешение пользовательских условий
При запуске Node.js пользовательские условия можно добавить с флагом --conditions:
node --conditions=development main.js
что затем разрешит условие "development" в импортах и экспортах пакетов, а также разрешит существующие условия "node", "node-addons", "default", "import" и "require" соответственно.
Можно установить любое количество пользовательских условий с помощью повторяющихся флагов.
Определения условий
Условия "import" , "require" , "node" , "node-addons" и "default" определены и реализованы в ядре Node.js, как указано выше.
Условие "node-addons" можно использовать для предоставления точки входа, использующей нативные C++ плагины. Однако это условие можно отключить с помощью флага --no-addons. При использовании "node-addons", рекомендуется рассматривать "default" как расширение, которое обеспечивает более универсальную точку входа, например, используя WebAssembly вместо нативного плагина.
Другие строковые условия неизвестны Node.js и по умолчанию игнорируются. Другие среды выполнения или инструменты, помимо Node.js, могут использовать их по своему усмотрению.
Эти пользовательские условия можно включить в Node.js с помощью флага --conditions.
Следующие определения условий в настоящее время поддерживаются Node.js:
-
"browser"— любая среда, которая реализует стандартный подмножество глобальных API браузера, доступных из JavaScript в веб-браузерах, включая API DOM. -
"development"— может использоваться для определения точки входа в среду только для разработки. Должен всегда взаимно исключать"production". -
"production"— может использоваться для определения точки входа в среду производства. Должен всегда взаимно исключать"development".
Вышеуказанные пользовательские условия можно включить в Node.js с помощью флага --conditions.
Платформенно-специфические условия, такие как "deno", "electron" или "react-native", могут использоваться, но пока не существует намерения реализации или интеграции с этими платформами, вышеперечисленные не явно одобрены Node.js.
Новые определения условий могут быть добавлены в этот список путем создания запроса на вытягивание в документации Node.js по этому разделу. Требования к добавлению нового определения условия:
- Определение должно быть ясным и однозначным для всех реализаторов.
- Случай использования, для которого необходимо условие, должен быть четко обоснован.
- Должен существовать достаточный существующий пример использования.
- Имя условия не должно конфликтовать с другим определением условия или условием с широким использованием.
- Определение условия должно приносить пользу экосистеме в плане координации, которая невозможна в противном случае. Например, это не обязательно будет справедливо для условий, специфичных для компаний или приложений.
В дальнейшем вышеупомянутые определения могут быть перенесены в отдельный реестр условий.
Ссылка на пакет по его имени
Внутри пакета значения, определенные в поле package.json "exports" пакета, могут быть использованы через имя пакета. Например, предположим, что package.json это:
// package.json
{
"name": "a-package",
"exports": {
".": "./main.mjs",
"./foo": "./foo.js"
}
} Тогда любой модуль в этом пакете может ссылаться на экспорт в самом пакете:
// ./a-module.mjs
import { something } from 'a-package'; // Imports "something" from ./main.mjs. Ссылка доступна только если 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'; Ссылка также доступна при использовании require, как в ES-модуле, так и в CommonJS. Например, этот код также будет работать:
// ./a-module.js
const { something } = require('a-package/foo'); // Loads from ./foo.js. Наконец, ссылка также работает с пакетами со областями имен. Например, этот код также будет работать:
// package.json
{
"name": "@my/package",
"exports": "./index.js"
} // ./index.js module.exports = 42;
// ./other.js
console.log(require('@my/package')); $ node other.js 42
Двойные пакеты 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 модулей, и может привести к неожиданному поведению.
Если основной экспорт пакета — конструктор, то сравнение экземпляров, созданных двумя версиями, возвращает false, а если экспорт — объект, свойства, добавленные к одному (например, pkgInstance.foo = 3), отсутствуют в другом. Это отличается от того, как работают инструкции import и require в средах с полностью CommonJS или полностью ES модулями соответственно, и поэтому удивляет пользователей. Это также отличается от поведения, с которым пользователи знакомы при использовании транспиляции с помощью инструментов, таких как Babel или esm.
Написание двойных пакетов, избегая или минимизируя опасности
Во-первых, опасность, описанная в предыдущем разделе, возникает, когда пакет содержит как CommonJS, так и ES модули, и оба варианта предоставляются для использования в Node.js, либо через отдельные точки входа main, либо экспортируемые пути. Пакет можно написать так, чтобы любая версия Node.js получала только CommonJS источники, а отдельные ES модули, которые может содержать пакет, предназначены только для других сред, таких как браузеры. Такой пакет будет пригоден для любой версии Node.js, поскольку import может ссылаться на файлы CommonJS; но он не предоставит никаких преимуществ использования синтаксиса ES модулей.
Пакет также может переключиться с CommonJS на ES модули в обновлении версии разрушающего изменения. Это имеет недостаток, что новейшая версия пакета будет доступна только в поддерживающих ES модули версиях Node.js.
У каждого подхода есть свои компромиссы, но существуют два основных подхода, удовлетворяющих следующим условиям:
- Пакет можно использовать как с
require, так и сimport. - Пакет можно использовать как в текущей версии Node.js, так и в более старых версиях Node.js, не поддерживающих ES модули.
- Главная точка входа пакета, например
'pkg', может использоваться какrequireдля разрешения на файл CommonJS, так иimportдля разрешения на файл ES модуля. (Аналогично для экспортируемых путей, например'pkg/feature'.) - Пакет предоставляет именованные экспорты, например
import { name } from 'pkg', а неimport pkg from 'pkg'; pkg.name. - Пакет потенциально пригоден для использования в других средах ES модулей, таких как браузеры.
- Опасности, описанные в предыдущем разделе, предотвращаются или минимизируются.
Подход #1: Использование оболочки ES модуля
Напишите пакет на CommonJS или транспилируйте ES модули в CommonJS и создайте файл оболочки ES модуля, который определяет именованные экспорты. Используя Условные экспорты, оболочка ES модуля используется для import, а точка входа CommonJS — для require.
// ./node_modules/pkg/package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
"import": "./wrapper.mjs",
"require": "./index.cjs"
}
} В приведенном примере используются явные расширения .mjs и .cjs. Если ваши файлы используют расширение .js, "type": "module" заставит такие файлы обрабатываться как ES модули, точно так же, как "type": "commonjs" заставит их обрабатываться как CommonJS. Смотрите Включение.
// ./node_modules/pkg/index.cjs exports.name = 'value';
// ./node_modules/pkg/wrapper.mjs import cjsModule from './index.cjs'; export const name = cjsModule.name;
В этом примере 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;
Этот подход подходит для следующих случаев использования:
- Пакет в настоящее время написан на 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",
"main": "./index.cjs",
"exports": {
".": "./index.cjs",
"./module": "./wrapper.mjs"
}
} Подход #2: Изолировать состояние
Файл package.json может напрямую определить отдельные точки входа CommonJS и ES модулей:
// ./node_modules/pkg/package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
}
} Это можно сделать, если версии пакета CommonJS и ES модулей эквивалентны, например, потому что одна является транспилированным результатом другой; и управление состоянием пакета тщательно изолировано (или пакет бессостоятельный).
Причина, по которой состояние является проблемой, заключается в том, что обе версии пакета CommonJS и ES модулей могут быть использованы в приложении; например, код приложения пользователя может использовать версию ES модулей, в то время как зависимость использует версию CommonJS. Если это произойдёт, в памяти будут загружены две копии пакета, и, следовательно, будут присутствовать два отдельных состояния. Это, вероятно, приведёт к труднодиагностируемым ошибкам.
Помимо написания бессостоятельного пакета (например, JavaScript’s Math был бы бессостоятельным, так как все его методы статические), есть несколько способов изоляции состояния, чтобы оно делилось между потенциально загруженными экземплярами пакета CommonJS и ES модулей:
-
Если возможно, поместите всё состояние в экземпляр объекта. Например, JavaScript’s
Dateнужно создать, чтобы содержать состояние; если бы это был пакет, его использовали бы так:import Date from 'date'; const someDate = new Date(); // someDate contains state; Date does not
Ключевое слово
newне требуется; функция пакета может возвращать новый объект или изменять переданный объект, чтобы хранить состояние вне пакета. -
Изолируйте состояние в одном или нескольких файлах CommonJS, которые совместно используются между версиями CommonJS и ES модулей пакета. Например, если точки входа CommonJS и ES модулей —
index.cjsиindex.mjsсоответственно:// ./node_modules/pkg/index.cjs const state = require('./state.cjs'); module.exports.state = state;// ./node_modules/pkg/index.mjs import state from './state.cjs'; export { state };Даже если
pkgиспользуется как черезrequire, так и черезimportв приложении (например, черезimportв коде приложения и черезrequireв зависимости) каждая ссылка наpkgбудет содержать одинаковое состояние; и изменение состояния из любой системы модулей повлияет на оба.
Любые плагины, которые подключаются к синглтону пакета, должны отдельно подключиться к синглтонам CommonJS и ES модулей.
Этот подход подходит для следующих случаев использования:
- Пакет в настоящее время написан на синтаксисе ES модулей, и автор хочет, чтобы эта версия использовалась везде, где такой синтаксис поддерживается.
- Пакет бессостоятельный, или его состояние можно изолировать без особых трудностей.
- Пакет вряд ли будет иметь другие публичные пакеты, зависящие от него, или, если это произойдет, пакет бессостоятельный или его состояние не обязательно должно делиться между зависимостями или с приложением в целом.
Даже с изолированным состоянием всё ещё есть затраты на возможную дополнительную обработку кода между версиями CommonJS и ES модулей пакета.
Как и в предыдущем подходе, вариант этого подхода, не требующий условных экспортов для потребителей, может заключаться в добавлении экспорта, например "./module", указывающего на версию пакета, полностью написанную на синтаксисе ES модулей:
// ./node_modules/pkg/package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
".": "./index.cjs",
"./module": "./index.mjs"
}
} Определения полей 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"
- Тип: <строка>
{
"name": "package-name"
} Поле "name" определяет имя вашего пакета. Публикация в реестре npm требует имени, удовлетворяющего определенным требованиям.
Поле "name" может использоваться дополнительно к полю "exports" для ссылок на пакет по имени.
"main"
- Тип: <строка>
{
"main": "./main.js"
} Поле "main" определяет скрипт, используемый при загрузке каталога пакета через require(). Его значение — путь.
require('./path/to/directory'); // This resolves to ./path/to/directory/main.js. Когда у пакета есть поле "exports", оно имеет приоритет над полем "main" при импорте пакета по имени.
"packageManager"
- Тип: <строка>
{
"packageManager": "<package manager name>@<version>"
} Поле "packageManager" определяет, какой менеджер пакетов ожидается при работе с текущим проектом. Оно может принимать любое из поддерживаемых значений и обеспечит, что ваши команды используют одинаковые версии менеджеров пакетов, не устанавливая ничего помимо Node.js.
Это поле сейчас экспериментальное и требует включения; обратитесь к странице Corepack для получения подробностей о процедуре.
"type"
- Тип: <строка>
Поле "type" определяет формат модуля, который Node.js использует для всех файлов .js, у которых файл с этим package.json значением является ближайшим родительским.
Файлы, заканчивающиеся на .js загружаются как ES модули, если ближайший родительский package.json содержит поле "type" со значением "module".
Ближайший родительский package.json определяется как первый package.json найденный в текущей папке, в родительской папке и так далее, пока не будет достигнута папка node_modules или корень тома.
// package.json
{
"type": "module"
} # 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".
// 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"
- Тип: <Объект> | <строка> | <массив строк>
{
"exports": "./index.js"
} Поле "exports" позволяет определять точки входа пакета при импорте по имени, загружаемого либо с помощью поиска node_modules, либо ссылкой по имени. Поддерживается в Node.js 12+ как альтернатива полю "main", способному определять экспорт по подпутям и условные экспортные точки и одновременно изолировать внутренние неэкспортированные модули.
Условные экспортные точки также могут использоваться в "exports" для определения различных точек входа пакета в зависимости от среды, включая то, ссылается ли пакет на require или на import.
Все пути, определённые в "exports" , должны быть относительными URL-адресами файлов, начинающимися с ./.
"imports"
- Тип: <Объект>
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
} Элементы в поле 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-v16.x/docs/api/packages.html