Модули: Пакеты
Введение
Пакет — это древовидная структура папок, описанная файлом 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 флаг
Строки, переданные в качестве аргумента в --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') в своих экспортах. Это гарантирует, что для каждого экспортируемого модуля существует только один подпуть, чтобы все зависимые импортировали один и тот же согласованный идентификатор, сохраняя контракт пакета ясным для потребителей и упрощая завершение подпутей пакета.
Традиционно пакеты использовали стиль без расширений, что имеет преимущества в читабельности и скрывает фактический путь к файлу внутри пакета.
С картами импорта, теперь обеспечивающими стандарт для разрешения пакетов в браузерах и других средах выполнения JavaScript, использование стиля без расширений может привести к большому объему определений карты импорта. Явные расширения файлов могут избежать этой проблемы, разрешив карте импорта использовать отображение папок пакетов для отображения нескольких подпутей, когда это возможно, вместо отдельной записи в карте для каждого экспорта подпути пакета. Это также отражает требование использования полного пути спецификатора в относительных и абсолютных спецификаторах импорта.
Сахар для экспортов
Если экспорт "." является единственным экспортом, поле "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.
Свойство статической перечислимости экспортов сохраняется с шаблонами экспортов, так как отдельные экспорты пакета могут быть определены путем обработки правой части шаблона назначения как ** 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
Условные экспорты
Условные экспорты предоставляют способ отображения на различные пути в зависимости от определенных условий. Они поддерживаются как для импорта 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.
Разрешение пользовательских условий
При выполнении 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.
Вышеперечисленные определения могут быть перенесены в специальный реестр условий со временем.
Самоссылка на пакет по его имени
Внутри пакета значения, определенные в поле 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.
У каждого подхода есть свои компромиссы, но есть два основных подхода, которые удовлетворяют следующим условиям:
- Пакет доступен как через
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. Если это произойдет, две копии пакета будут загружены в памяти, и, следовательно, будут присутствовать два отдельных состояния. Это, вероятно, вызовет трудноотслеживаемые ошибки.
Помимо написания бессостоятельного пакета (если JavaScript's Math был пакетом, например, он был бы бессостоятельным, так как все его методы статические), есть несколько способов изоляции состояния, чтобы оно делилось между потенциально загруженными экземплярами пакета CommonJS и ES-модулей:
-
Если это возможно, поместите все состояние в экземплярируемый объект. JavaScript's
Date, например, должен быть экземпляризирован для хранения состояния; если бы это был пакет, он использовался бы так: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/api/packages.html