Модули: Пакеты
Введение
Пакет представляет собой древовидную структуру папок, описываемую файлом 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.
Node.js будет интерпретировать все остальные типы ввода, такие как файлы с расширением .js, где ближайший родительский файл package.json не содержит поле верхнего уровня "type", или строковый ввод без флага --input-type, как CommonJS. Это делается для обеспечения обратной совместимости. Однако, теперь, когда Node.js поддерживает как CommonJS, так и модули ES, лучше указывать тип явно, когда это возможно. Node.js будет интерпретировать следующее как CommonJS, когда это передаётся в node в качестве начального ввода или когда на них ссылаются инструкции import, выражения import(), или выражения require():
-
Файлы с расширением
.cjs. -
Файлы с расширением
.js, когда ближайший родительский файлpackage.jsonсодержит поле верхнего уровня"type"со значением"commonjs". -
Строки, переданные в качестве аргумента функции
--evalили--print, или перенаправленные вnodeчерезSTDIN, с флагом--input-type=commonjs.
Авторы пакетов должны включать поле "type", даже в пакетах, где все источники являются 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внутри пакета"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 '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 не указан.
Определение менеджера пакетов
Хотя ожидается, что все проекты 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 Экспорт подпутей
При использовании поля "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-карты сейчас обеспечивают стандарт для разрешения пакетов в браузерах и других средах выполнения JavaScript, использование стиля без расширений может привести к раздуванию определений import-карт. Явные расширения файлов могут избежать этой проблемы, позволяя import-карте использовать отображение папок пакетов, чтобы сопоставить несколько подпутей, где это возможно, вместо отдельной записи в карте для каждого экспорта подпути пакета. Это также отражает требование использования полного пути спецификатора в относительных и абсолютных импортных спецификаторах.
Сахар для экспорта
Если экспорт "." является единственным экспортом, поле "exports" предоставляет удобный синтаксис для этого случая, являясь прямым значением поля "exports".
{
"exports": {
".": "./index.js"
}
} copy может быть записано:
{
"exports": "./index.js"
} copy Импорт подпутей
В дополнение к полю "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.
Шаблоны подпутей
Для пакетов с небольшим количеством экспортов или импортов рекомендуется явно перечислить каждый элемент экспорта подпути. Но для пакетов с большим количеством подпутей это может привести к раздуванию 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.
Свойство экспортов, являющихся статически перечисляемыми, сохраняется с шаблонами экспорта, поскольку отдельные экспорты для пакета могут быть определены путем обработки целевого шаблона справа как ** глоба относительно списка файлов внутри пакета. Поскольку пути 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
Условные экспорты
Условные экспорты обеспечивают способ сопоставления с различными путями в зависимости от определенных условий. Они поддерживаются как для импортов 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 модули, так как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.
Разрешение пользовательских условий
При запуске 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.
Новые определения условий могут быть добавлены в этот список, создав запрос на вытягивание в документацию Node.js по этому разделу. Требования для включения нового определения условия здесь следующие:
- Определение должно быть ясным и однозначным для всех реализаторов.
- Случай использования, для которого необходимо условие, должен быть четко обоснован.
- Должно существовать достаточное существующее использование реализации.
- Имя условия не должно конфликтовать с другим определением условия или условием широкого использования.
- Перечисление определения условия должно приносить пользу экосистеме, которая невозможна в противном случае. Например, это не обязательно будет так для условий, специфичных для компании или приложения.
- Условие должно быть таким, чтобы пользователь Node.js ожидал его увидеть в документации Node.js. Условие
"types"— хороший пример: оно не очень подходит для предложения Runtime Keys, но хорошо подходит сюда, в документацию Node.js.
Вышеупомянутые определения могут быть перенесены в специальный реестр условий со временем.
Ссылка на пакет по его имени
Внутри пакета значения, определенные в поле 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-модулей и может привести к неожиданному поведению.
Если основной экспорт пакета — это конструктор, сравнение экземпляров, созданных двумя версиями, возвращает 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-модульный синтаксис в обновлении версии esm. Это имеет недостаток, что новейшая версия пакета будет пригодна только для поддерживающих 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",
"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. Если это произойдёт, две копии пакета будут загружены в памяти, и, следовательно, будет присутствовать два отдельных состояния. Это, вероятно, приведёт к трудно диагностируемым ошибкам.
Помимо написания бессостоятельного пакета (если, например, Math JavaScript был пакетом, он был бы бессостоятельным, так как все его методы статические), существуют способы изоляции состояния, чтобы оно разделялось между потенциально загруженными экземплярами пакета CommonJS и ES-модуля:
-
Если возможно, поместите всё состояние внутри экземпляра объекта. Например,
DateJavaScript требует создания экземпляра для хранения состояния; если бы это был пакет, он использовался бы так:import Date from 'date'; const someDate = new Date(); // someDate contains state; Date does not copy
Ключевое слово
newне обязательно; функция пакета может возвращать новый объект или изменять переданный объект, чтобы состояние оставалось внешним для пакета. -
Изолируйте состояние в одном или нескольких файлах 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"
- Тип: <строка>
{
"name": "package-name"
} copy Поле "name" определяет имя вашего пакета. Публикация в реестре npm требует имени, удовлетворяющего определенным требованиям.
Поле "name" может использоваться дополнительно к полю "exports" для ссылок на пакет по его имени.
"main"
- Тип: <строка>
{
"main": "./index.js"
} copy Поле "main" определяет точку входа в пакет при импорте по имени с помощью node_modules поиска. Его значение — путь.
Когда у пакета есть поле "exports", оно будет иметь приоритет над полем "main" при импорте пакета по имени.
Также оно определяет сценарий, используемый при загрузке каталога пакета через require().
// This resolves to ./path/to/directory/index.js.
require('./path/to/directory'); copy
"packageManager"
- Тип: <строка>
{
"packageManager": "<package manager name>@<version>"
} copy Поле "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"
} 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"
- Тип: <Объект> | <строка> | <массив строк>
{
"exports": "./index.js"
} copy Поле "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"
}
} 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-v18.x/docs/api/packages.html