Модули: пакеты¶
Введение¶
Пакет — это дерево каталогов, описанное файлом 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; -
код, который синтаксически разбирается только как ES-модули, например операторы
import/exportилиimport.meta, без явного указания интерпретации. Явные маркеры — расширения.mjs/.cjs, поле"type"вpackage.jsonсо значениями"module"или"commonjs"или флаг--input-type. Динамические выраженияimport()допустимы и в CommonJS, и в ES-модулях и сами по себе не заставляют файл считаться ES-модулем. См. обнаружение синтаксиса.
Node.js будет считать следующее CommonJS, если передать это в node как начальный ввод или сослаться через import/import():
-
файлы с расширением
.cjs; -
файлы с расширением
.js, если ближайший родительскийpackage.jsonсодержит поле"type"со значением"commonjs"; -
строки для
--evalили--print, а также ввод черезSTDINс флагом--input-type=commonjs; -
файлы
.jsбез родительскогоpackage.jsonили при отсутствии поляtypeв ближайшемpackage.json, если код успешно выполняется как CommonJS. Иными словами, Node.js сначала пытается выполнить такие «двусмысленные» файлы как CommonJS и повторно оценивает их как ES-модули, если разбор как CommonJS не удался из-за синтаксиса ES-модуля.
Использование синтаксиса ES-модулей в «двусмысленных» файлах даёт накладные расходы, поэтому рекомендуется везде, где возможно, быть явным. В частности, авторам пакетов следует всегда указывать поле "type" в package.json, даже если все исходники — CommonJS. Явный type защитит пакет, если когда-нибудь изменится тип по умолчанию в Node.js, и упростит работу сборщиков и загрузчиков при определении интерпретации файлов.
Обнаружение синтаксиса¶
Стабильность: 1.2 — кандидат на выпуск
Node.js просматривает исходный код двусмысленного ввода и, если обнаружен синтаксис ES-модуля, обрабатывает ввод как ES-модуль.
Двусмысленный ввод — это:
- файлы с расширением
.jsили без расширения, при отсутствии управляющегоpackage.jsonили при отсутствии в нём поляtype; - строковый ввод (
--evalилиSTDIN), если не указан--input-type.
Синтаксис ES-модуля — это синтаксис, который при выполнении как CommonJS привёл бы к ошибке. К нему относятся:
- операторы
import(но не выраженияimport()— они допустимы в CommonJS); - операторы
export; - обращения к
import.meta; awaitна верхнем уровне модуля;- повторные лексические объявления переменных обёртки CommonJS (
require,module,exports,__dirname,__filename).
Разрешение и загрузка модулей¶
У Node.js два вида разрешения и загрузки модулей — в зависимости от способа запроса.
Когда модуль запрашивается через require() (по умолчанию в CommonJS и может быть создан через createRequire() и в CommonJS, и в ES-модулях):
- Разрешение:
- разрешение через
require()поддерживает папки как модули; - при отсутствии точного совпадения
require()подставляет расширения (.js,.json, затем.node) и снова пытается разрешить папки как модули; - по умолчанию URL в качестве спецификаторов не поддерживаются.
- разрешение через
- Загрузка:
.jsonобрабатываются как JSON-текст;.node— скомпилированные аддоны, загружаемые черезprocess.dlopen();.ts,.mts,.cts— как TypeScript;- прочие расширения или отсутствие расширения — как JavaScript-текст;
require()может загружать ES-модули из CommonJS только если ES-модуль и его зависимости синхронны (нет top-levelawait).
Когда модуль запрашивается статическим import (только в ES-модулях) или выражением import() (в CommonJS и ES-модулях):
- Разрешение:
- у
import/import()нет «папок как модулей», индекс каталога (например'./startup/index.js') задаётся полностью; - поиск расширений не выполняется: для относительного или абсолютного file URL нужно указать расширение;
- по умолчанию поддерживаются спецификаторы
file://иdata:.
- у
- Загрузка:
.json— JSON-текст; при импорте JSON-модулей нужен атрибут типа импорта (напримерimport json from './data.json' with { type: 'json' });.node— аддоны черезprocess.dlopen(), если включён--experimental-addon-modules;.ts,.mts,.cts— как TypeScript;- для JavaScript-текста допускаются только расширения
.js,.mjs,.cjs; .wasm— WebAssembly-модули;- иные расширения дают ошибку
ERR_UNKNOWN_FILE_EXTENSION; дополнительные расширения — через хуки настройки; import/import()могут загружать JavaScript-модули CommonJS; для них используется merve, чтобы по возможности вывести именованные экспорты статическим анализом.
Независимо от способа запроса разрешение и загрузку можно настроить через хуки настройки.
package.json и расширения файлов¶
В пакете поле package.json "type" задаёт, как Node.js интерпретирует файлы .js. Без "type" файлы .js считаются CommonJS.
"type": "module" означает, что .js в этом пакете — ES module.
"type" действует не только на точку входа (node my-app.js), но и на файлы, подключаемые через import и import().
1 2 3 4 5 6 7 8 9 10 | |
Файлы .mjs всегда загружаются как ES modules, независимо от родительского package.json.
Файлы .cjs всегда загружаются как CommonJS, независимо от родительского package.json.
1 2 3 4 5 | |
Расширения .mjs и .cjs позволяют смешивать типы в одном пакете:
-
в пакете с
"type": "module"отдельный файл можно пометить как CommonJS, давав ему.cjs(и.js, и.mjsв таком пакете — ES-модули); -
в пакете с
"type": "commonjs"отдельный файл можно пометить как ES module, давав ему.mjs(и.js, и.cjsв таком пакете — CommonJS).
Флаг --input-type¶
Строки для --eval/-e или из STDIN обрабатываются как ES modules, если задано --input-type=module.
1 2 3 | |
Также есть --input-type=commonjs для явного запуска строки как CommonJS. Если --input-type не указан, по умолчанию это поведение CommonJS.
Точки входа пакета¶
В package.json точки входа задают поля "main" и "exports". Оба подходят и для ES-модулей, и для CommonJS.
"main" поддерживается во всех версиях Node.js, но задаёт только главную точку входа.
"exports" — современная замена "main": несколько точек входа, условное разрешение в разных средах и запрет любых путей вне перечисленных в "exports". Так проще явно описать публичный API пакета.
Для новых пакетов под актуальные версии Node.js рекомендуется "exports". Для поддержки Node.js 10 и ниже нужен "main". Если заданы оба, "exports" имеет приоритет над "main" в поддерживаемых версиях.
Условные экспорты внутри "exports" задают разные точки входа по среде, включая require и import. Про совместимый CommonJS и ES в одном пакете см. раздел про dual-пакеты.
Если у существующего пакета появляется "exports", потребители не смогут использовать непроэкспортированные пути, в том числе package.json (например require('your-package/package.json')). Это обычно ломающее изменение.
Чтобы ввести "exports" без поломки, экспортируйте все ранее поддерживаемые пути; лучше явно перечислить точки входа. Например, если раньше были main, lib, feature и package.json, можно задать:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Либо экспортировать целые каталоги с шаблонами и с расширениями, и без:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Так сохраняется обратная совместимость в минорных версиях; в следующем major можно сузить экспорты только до нужных путей:
1 2 3 4 5 6 7 8 | |
Экспорт главной точки входа¶
Для нового пакета рекомендуется поле "exports":
1 2 3 | |
Если задано "exports", подпути пакета инкапсулированы и недоступны импортёрам. Например, require('pkg/subpath.js') даёт ERR_PACKAGE_PATH_NOT_EXPORTED.
Так проще гарантировать контракт API и semver; полной изоляции нет: прямой require('/path/to/node_modules/pkg/subpath.js') по-прежнему может загрузить файл.
Актуальные Node.js и сборщики поддерживают "exports". Для старых версий можно дублировать "main" тем же путём, что и "exports":
1 2 3 4 | |
Экспорт подпутей¶
При использовании поля "exports" можно задать пользовательские подпути наряду с главной точкой входа, считая главную точку входа подпутём ".":
1 2 3 4 5 6 | |
Тогда потребитель может импортировать только подпуть, явно заданный в "exports":
1 2 | |
Другие подпути приведут к ошибке:
1 2 | |
Расширения в подпутях¶
Авторам пакетов следует указывать в экспорте либо подпути с расширением (import 'pkg/subpath.js'), либо без него (import 'pkg/subpath'). Так для каждого экспортируемого модуля остаётся единственный подпуть, все зависимости используют один и тот же спецификатор, контракт пакета ясен потребителям, а автодополнение подпутей упрощается.
Традиционно пакеты чаще использовали стиль без расширения: он удобнее читается и скрывает реальный путь к файлу внутри пакета.
Теперь, когда карты импорта задают стандарт разрешения пакетов в браузерах и других средах JavaScript, стиль без расширения может раздувать определения карт импорта. Явные расширения файлов помогают избежать этого, позволяя карте импорта использовать отображение папки пакетов и по возможности сопоставлять несколько подпутей вместо отдельной записи карты на каждый экспортируемый подпуть. Это же согласуется с требованием указывать полный путь спецификатора в относительных и абсолютных спецификаторах импорта.
Правила путей и проверка целей экспорта¶
Задавая пути как цели в поле "exports", Node.js применяет несколько правил ради безопасности, предсказуемости и инкапсуляции. Понимание этих правил важно для авторов, публикующих пакеты.
Цели должны быть относительными URL¶
Все целевые пути в карте "exports" (значения, сопоставленные ключам экспорта) должны быть строками относительных URL, начинающимися с ./.
1 2 3 4 5 6 7 8 9 10 11 | |
Причины такого поведения:
- Безопасность: нельзя экспортировать произвольные файлы за пределами собственного каталога пакета.
- Инкапсуляция: все экспортируемые пути разрешаются относительно корня пакета, пакет остаётся самодостаточным.
Без обхода каталогов и недопустимых сегментов¶
Цели экспорта не должны разрешаться в расположение вне корня пакета. Кроме того, сегменты пути вроде . (одна точка), .. (две точки) или node_modules (и их эквиваленты в URL-кодировании) как правило запрещены в строке target после начального ./ и в любой части subpath, подставляемой в шаблон цели.
1 2 3 4 5 6 7 8 9 10 | |
Синтаксический сахар для exports¶
Если экспорт "." — единственный, поле "exports" допускает сокращённую запись: вместо объекта можно указать непосредственно значение "exports".
1 2 3 4 5 | |
эквивалентно:
1 2 3 | |
Импорт подпутей¶
Помимо поля "exports" в пакете есть поле "imports" — для приватных сопоставлений, которые действуют только для спецификаторов импорта изнутри самого пакета.
Записи в поле "imports" всегда должны начинаться с #, чтобы их можно было отличить от спецификаторов внешних пакетов.
Например, поле imports позволяет получить преимущества условных экспортов для внутренних модулей:
1 2 3 4 5 6 7 8 9 10 11 12 | |
здесь import '#dep' не получает разрешение внешнего пакета dep-node-native (включая его собственные exports), а в других средах получает локальный файл ./dep-polyfill.js относительно пакета.
В отличие от поля "exports", поле "imports" допускает сопоставление с внешними пакетами.
Правила разрешения для поля imports в остальном аналогичны полю exports.
Шаблоны подпутей¶
Для пакетов с небольшим числом экспортов или импортов рекомендуется явно перечислять каждый подпуть в exports. Если же подпутей очень много, это может раздувать package.json и усложнять сопровождение.
В таких случаях вместо этого можно использовать шаблоны подпутей экспорта:
1 2 3 4 5 6 7 8 9 | |
Сопоставления с * раскрывают вложенные подпути как синтаксис простой подстановки строк.
Все вхождения * в правой части заменяются этим значением, в том числе если в нём есть разделители /.
1 2 3 4 5 6 7 8 | |
Это прямое статическое сопоставление и замена без особой обработки расширений файлов. Указание "*.js" с обеих сторон сопоставления ограничивает экспортируемые файлы пакета только JS.
Свойство статически перечислимых экспортов сохраняется и для шаблонов: отдельные экспорты пакета можно получить, рассматривая шаблон цели справа как глоб ** по списку файлов внутри пакета. Так как пути node_modules в целях exports запрещены, такое раскрытие опирается только на файлы самого пакета.
Чтобы исключить приватные подкаталоги из шаблонов, можно использовать цели null:
1 2 3 4 5 6 7 | |
1 2 3 4 5 | |
Условные экспорты¶
Условные экспорты позволяют сопоставлять разные пути в зависимости от условий. Они поддерживаются и для импорта CommonJS, и для ES-модулей.
Например, пакет, который хочет отдавать разные точки входа для require() и import, можно описать так:
1 2 3 4 5 6 7 8 | |
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-модули. Всегда взаимоисключающе с"import"."module-sync"— совпадает независимо от того, загружают ли пакет черезimport,import()илиrequire(). Ожидается формат ES-модулей без top-levelawaitв графе модулей — иначе приrequire()будет выброшеноERR_REQUIRE_ASYNC_MODULE."default"— универсальный запасной вариант, всегда совпадает. Может указывать на CommonJS или ES-модуль. Это условие всегда должно идти последним.
В объекте "exports" порядок ключей важен. При сопоставлении условий более ранние записи имеют больший приоритет и перекрывают последующие. Обычно условия в объекте задают от более специфичных к менее специфичным.
Использование условий "import" и "require" связано с рисками; подробнее — в разделе о двойных пакетах CommonJS/ES-модулей.
Условие "node-addons" подходит для точки входа с нативными аддонами на C++. Его можно отключить флагом --no-addons. При "node-addons" имеет смысл рассматривать "default" как усиление с более универсальной точкой входа, например с WebAssembly вместо нативного аддона.
Условные экспорты можно распространять и на подпути, например:
1 2 3 4 5 6 7 8 9 | |
Определяется пакет, в котором require('pkg/feature.js') и import 'pkg/feature.js' могут отдавать разные реализации в Node.js и в других средах JavaScript.
При ветвлении по средам по возможности всегда включайте условие "default". Оно гарантирует, что неизвестные среды JS смогут использовать универсальную реализацию и не придётся подстраиваться под уже существующие среды ради поддержки условных экспортов. Поэтому ветки "node" и "default" обычно предпочтительнее сочетания "node" и "browser".
Вложенные условия¶
Помимо прямых сопоставлений Node.js поддерживает вложенные объекты условий.
Например, чтобы задать пакет только с двойными точками входа для Node.js, но не для браузера:
1 2 3 4 5 6 7 8 9 | |
Условия по-прежнему сопоставляются по порядку, как при «плоских» условиях. Если у вложенного условия нет сопоставления, проверка продолжается по остальным условиям родителя. Так вложенные условия ведут себя как вложенные операторы if в JavaScript.
Разрешение пользовательских условий¶
При запуске Node.js пользовательские условия можно задать флагом --conditions:
1 | |
Тогда будет разрешаться условие "development" в импортах и экспортах пакетов, а существующие "node", "node-addons", "default", "import" и "require" — по правилам как обычно.
Произвольное число пользовательских условий задаётся повторением флага.
В типичных условиях допустимы только буквы и цифры; при необходимости разделители — :, - или =. Иные символы могут вызвать проблемы совместимости вне Node.js.
В Node.js у условий мало ограничений, в частности:
- Должен быть хотя бы один символ.
- Нельзя начинать с
., так как в некоторых контекстах допускаются относительные пути. - Нельзя использовать
,— часть CLI может воспринять это как список через запятую. - Нельзя использовать целочисленные ключи вроде
"10"— возможны неожиданные эффекты порядка ключей в объектах JS.
Определения условий сообщества¶
Строки условий, кроме "import", "require", "node", "module-sync", "node-addons" и "default", реализованных в ядре Node.js, по умолчанию игнорируются.
Другие платформы могут вводить свои условия; в Node.js пользовательские условия включаются флагом --conditions / -C.
Чтобы пользовательские условия пакетов использовались однозначно, ниже приведён список распространённых условий и их строгие определения для согласованности экосистемы.
"types"— системы типов могут по нему находить файл типов для данного экспорта. Это условие всегда должно идти первым."browser"— любая среда веб-браузера."development"— точка входа только для режима разработки, например с дополнительной отладочной информацией и более понятными сообщениями об ошибках. Всегда взаимоисключающе с"production"."production"— точка входа для production-среды. Всегда взаимоисключающе с"development".
Для прочих сред определения платформенных ключей ведёт WinterCG в спецификации предложения Runtime Keys.
Новые определения условий можно добавить в этот список через pull request в документацию Node.js по этому разделу. Требования к включению нового определения:
- Определение должно быть ясным и однозначным для всех реализаторов.
- Должен быть чётко обоснован сценарий, зачем нужно условие.
- Должно быть достаточно существующих реализаций в дикой природе.
- Имя условия не должно конфликтовать с другим определением или широко используемым условием.
- Публикация определения должна давать экосистеме выигрыш в согласовании, который иначе недостижим. Например, это не обязательно так для условий, специфичных для компании или приложения.
- Пользователь Node.js ожидал бы увидеть условие в документации ядра. Хороший пример —
"types": оно не очень подходит к предложению Runtime Keys, но хорошо смотрится в документации Node.js.
В будущем эти определения могут быть перенесены в отдельный реестр условий.
Самоссылка на пакет по имени¶
Внутри пакета значения из поля package.json "exports" можно запрашивать по имени пакета. Например, пусть package.json такой:
1 2 3 4 5 6 7 8 | |
Тогда любой модуль в этом пакете может ссылаться на экспорт самого пакета:
1 2 | |
Самоссылка возможна только если в package.json есть "exports", и разрешает импортировать только то, что разрешает это поле "exports" (в package.json). Поэтому приведённый ниже код для предыдущего пакета вызовет ошибку выполнения:
1 2 3 4 5 6 | |
Самоссылка также работает с require и в ES-модуле, и в CommonJS. Например, такой код тоже допустим:
1 2 | |
Наконец, самоссылка работает и для пакетов со scope. Например, сработает такой код:
1 2 3 4 5 | |
1 2 | |
1 2 | |
1 2 | |
Двойные пакеты CommonJS/ES-модулей¶
Подробности — в репозитории примеров пакетов.
Определения полей package.json в Node.js¶
Здесь описаны поля, которые использует среда выполнения Node.js. Другие инструменты (например npm) используют дополнительные поля: Node.js их игнорирует, и они здесь не документируются.
В Node.js используются следующие поля package.json:
"name"— нужно для именованных импортов внутри пакета; менеджеры пакетов используют его как имя пакета."main"— модуль по умолчанию при загрузке пакета по имени, если не заданоexports, а также в версиях Node.js до появленияexports."type"— тип пакета: как загружать файлы.js— как CommonJS или как ES-модули."exports"— экспорт пакета и условные экспорты; если задано, ограничивает, какие подмодули можно загрузить из пакета."imports"— импорты пакета для модулей внутри самого пакета.
"name"¶
- Тип:
<string>
1 2 3 | |
Поле "name" задаёт имя пакета. Для публикации в реестре npm имя должно удовлетворять определённым требованиям.
Поле "name" можно использовать вместе с "exports" для самоссылки на пакет по его имени.
"main"¶
- Тип:
<string>
1 2 3 | |
Поле "main" задаёт точку входа пакета при импорте по имени через поиск в node_modules. Его значение — путь.
Если задано поле "exports", оно имеет приоритет над "main" при импорте пакета по имени.
Оно же задаёт файл, который подставляется при загрузке каталога пакета через require().
1 2 | |
"type"¶
Добавлено в: v12.0.0
- Тип:
<string>
Поле "type" задаёт формат модулей, который Node.js применяет ко всем файлам .js, у которых ближайший родительский файл — этот package.json.
Файлы с расширением .js загружаются как ES-модули, если ближайший родительский package.json содержит поле верхнего уровня "type" со значением "module".
Ближайший родительский package.json — это первый найденный package.json при подъёме от текущей папки к родительским, пока не встретится каталог node_modules или корень тома.
1 2 3 4 | |
1 2 | |
Если у ближайшего родительского package.json нет поля "type" или указано "type": "commonjs", файлы .js обрабатываются как CommonJS. Если дошли до корня тома и package.json не найден, файлы .js тоже считаются CommonJS.
Операторы import для файлов .js обрабатываются как ES-модули, если ближайший родительский package.json содержит "type": "module".
1 2 | |
Независимо от значения "type" файлы .mjs всегда обрабатываются как ES-модули, а .cjs — как CommonJS.
"exports"¶
Добавлено в: v12.7.0
- Тип:
<Object>|<string>|<string[]>
1 2 3 | |
Поле "exports" задаёт точки входа пакета при импорте по имени через поиск в node_modules или через самоссылку по имени пакета. Поддерживается в Node.js 12+ как альтернатива "main": можно описать экспорт подпутей и условные экспорты, скрывая внутренние неэкспортируемые модули.
Условные экспорты внутри "exports" позволяют задавать разные точки входа по среде, в том числе в зависимости от того, обращаются к пакету через require или через import.
Все пути в "exports" должны быть относительными file URL, начинающимися с ./.
"imports"¶
- Тип:
<Object>
1 2 3 4 5 6 7 8 9 10 11 12 | |
Записи в поле imports должны быть строками, начинающимися с #.
Импорты пакета допускают сопоставление с внешними пакетами.
Это поле задаёт импорт подпутей для текущего пакета.
