Модули: Пакеты
Введение
Пакет — это древовидная структура папок, описываемая файлом 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обрабатываются как 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 'path'; console.log(sep);"
echo "import { sep } from 'path'; console.log(sep);" | node --input-type=module Для полноты, есть также --input-type=commonjs, для явного запуска строкового ввода как CommonJS. Это поведение по умолчанию, если --input-type не указан.
Точки входа в пакет
В файле 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 любого абсолютного подпути пакета, например, 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 запрещены в целевых точках экспорта, это расширение зависит только от файлов самого пакета.
Упрощённая запись экспорта
Если экспорт "." является единственным экспортом, поле "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". -
"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", "default", "import", и "require" условия по мере необходимости.
Можно задать любое количество пользовательских условий с флагами повторения.
Определения условий
Условия "import", "require", "node" и "default" определены и реализованы в ядре Node.js, как указано выше.
Другие строковые условия неизвестны Node.js и поэтому по умолчанию игнорируются. Временные среды или инструменты, отличные от Node.js, могут использовать их по своему усмотрению.
Эти пользовательские условия могут быть включены в Node.js с помощью флага --conditions.
Следующие определения условий в настоящее время поддерживаются Node.js:
-
"browser"- любая среда, которая реализует стандартный подмножество глобальных браузерных API, доступных из JavaScript в веб-браузерах, включая DOM-API. -
"development"- может использоваться для определения точки входа в среду только для разработки. Должно всегда быть взаимно исключающим с"production". -
"production"- может использоваться для определения точки входа в среду производства. Должно всегда быть взаимно исключающим с"development".
Вышеперечисленные пользовательские условия могут быть включены в Node.js с помощью флага --conditions.
Платформенно-специфические условия, такие как "deno", "electron" или "react-native", могут быть использованы, но, поскольку на данный момент нет планов по реализации или интеграции с этими платформами, вышеуказанные условия не являются явно поддерживаемыми Node.js.
Новые определения условий могут быть добавлены в этот список, создав PR в документацию 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. Пакеты двойного типа 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' (или альтернативного пути main, как 'pkg/module'). Это «опасность двойного пакета», когда две версии одного пакета могут быть загружены в одной среде выполнения. Хотя маловероятно, что приложение или пакет намеренно загрузит обе версии напрямую, приложение часто загружает одну версию, в то время как зависимость приложения загружает другую версию. Эта опасность может возникнуть, потому что Node.js поддерживает смешивание CommonJS и ES модулей и может привести к неожиданному поведению.
Если экспорт пакета main — это конструктор, сравнение экземпляров, созданных двумя версиями, возвращает 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 модули.
- Точка входа пакета main, например
'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. -
"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" при импорте пакета по имени.
"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-v14.x/docs/api/packages.html