Перейти к содержанию

Пакеты

v18.x.x

Введение

Пакет - это дерево папок, описываемое файлом package.json. Пакет состоит из папки, содержащей файл package.json, и всех вложенных папок до следующей папки, содержащей другой файл package.json, или папки с именем node_modules.

Эта страница содержит руководство для авторов пакетов, пишущих файлы package.json, а также ссылку на поля package.json, определенные Node.js.

Определение системы модулей

Node.js будет рассматривать следующие файлы как ES модули, когда они передаются в node в качестве начального ввода, или когда на них ссылаются операторы import или выражения import():

  • Файлы с расширением .mjs.

  • Файлы с расширением .js, если ближайший родительский файл package.json содержит поле верхнего уровня "type" со значением "module".

  • Строки, переданные в качестве аргумента в --eval, или переданные в node через STDIN с флагом --input-type=module.

Node.js будет рассматривать как CommonJS все другие формы ввода, такие как файлы .js, где ближайший родительский файл package.json не содержит поля верхнего уровня "type", или строковый ввод без флага --input-type. Такое поведение сохраняет обратную совместимость. Однако теперь, когда Node.js поддерживает как CommonJS, так и ES модули, лучше быть явным, когда это возможно. Node.js будет рассматривать следующие файлы как CommonJS, когда они передаются в node в качестве начального ввода, или когда на них ссылаются заявления import, выражения import() или выражения require():

  • Файлы с расширением .cjs.

  • Файлы с расширением .js, если ближайший родительский файл package.json содержит поле верхнего уровня "type" со значением "commonjs".

  • Строки, переданные в качестве аргумента в --eval или --print, или переданные в node через STDIN с флагом --input-type=commonjs.

Авторы пакетов должны включать поле тип, даже в пакетах, где все источники являются CommonJS. Явное указание типа пакета защитит пакет в будущем на случай, если тип Node.js по умолчанию когда-либо изменится, а также облегчит инструментам сборки и загрузчикам определение того, как следует интерпретировать файлы в пакете.

Загрузчики модулей

Node.js имеет две системы для разрешения спецификатора и загрузки модулей.

Существует загрузчик модулей CommonJS:

  • Он полностью синхронный.
  • Он отвечает за обработку вызовов require().
  • Его можно патчить по-обезьяньи.
  • Он поддерживает папки как модули.
  • При разрешении спецификатора, если не найдено точного соответствия, он попытается добавить расширения (.js, .json, и наконец .node), а затем попытается разрешить папки как модули.
  • Он рассматривает .json как текстовые файлы JSON.
  • Файлы .node интерпретируются как скомпилированные модули аддонов, загруженные с помощью process.dlopen().
  • Все файлы, не имеющие расширений .json или .node, рассматриваются как текстовые файлы JavaScript.
  • Его нельзя использовать для загрузки модулей ECMAScript (хотя можно загрузить модули ECMASCript из модулей CommonJS). При использовании для загрузки текстового файла JavaScript, который не является модулем ECMAScript, он загружается как модуль CommonJS.

Существует загрузчик модулей ECMAScript:

  • Он является асинхронным.
  • Он отвечает за обработку утверждений import и выражений import().
  • Он не поддается обезьяньим исправлениям, может быть настроен с помощью loader hooks.
  • Не поддерживает папки в качестве модулей, индексы директорий (например, './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 module.

Поле "type" применяется не только к начальным точкам входа (node my-app.js), но и к файлам, на которые ссылаются операторы import и выражения import().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// my-app.js, рассматриваемый как ES-модуль, поскольку в той же папке есть package.json
// файл в той же папке с "type": "module".

import './startup/init.js';
// Загружается как ES-модуль, поскольку ./startup не содержит файла package.json,
// и поэтому наследует значение "type" от одного уровня выше.

import 'commonjs-package';
// Загружается как CommonJS, поскольку ./node_modules/commonjs-package/package.json
// не имеет поля "type" или содержит "type": "commonjs".

import './node_modules/commonjs-package/index.js';
// Загружается как CommonJS с ./node_modules/commonjs-package/package.json
// не имеет поля "type" или содержит "type": "commonjs".

Файлы, заканчивающиеся на .mjs, всегда загружаются как ES модули, независимо от ближайшего родителя package.json.

Файлы, заканчивающиеся на .cjs, всегда загружаются как CommonJS независимо от ближайшего родителя package.json.

1
2
3
4
5
import './legacy-file.cjs';
// Загружается как CommonJS, поскольку .cjs всегда загружается как CommonJS.

import 'commonjs-package/src/index.mjs';
// Загружается как ES-модуль, поскольку .mjs всегда загружается как ES-модуль.

Расширения .mjs и .cjs могут быть использованы для смешивания типов в одном пакете:

  • Внутри пакета `тип'': "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.

1
2
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

Для полноты картины существует также --input-type=commonjs, для явного запуска строкового ввода как CommonJS. Это поведение по умолчанию, если --input-type не указан.

Определение менеджера пакетов

Стабильность: 1 – Экспериментальная

Фича изменяется и не допускается флагом командной строки. Может быть изменена или удалена в последующих версиях.

Хотя ожидается, что все проекты Node.js будут устанавливаться всеми пакетными менеджерами после публикации, их команды разработчиков часто должны использовать один конкретный пакетный менеджер. Чтобы облегчить этот процесс, Node.js поставляется с инструментом под названием Corepack, цель которого - сделать все пакетные менеджеры прозрачно доступными в вашей среде - при условии, что у вас установлен Node.js.

По умолчанию Corepack не будет применять какой-либо конкретный менеджер пакетов и будет использовать общие версии "Last Known Good", связанные с каждым выпуском 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.

Conditional 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "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"
    }
}

В качестве альтернативы проект может выбрать экспорт целых папок как с расширенными подпапками, так и без них, используя шаблоны экспорта:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "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"
    }
}

Благодаря тому, что вышеприведенное обеспечивает обратную совместимость для любых второстепенных версий пакета, будущее серьезное изменение для пакета может затем правильно ограничить экспорт только определенным экспортом функций:

1
2
3
4
5
6
7
8
{
    "name": "my-package",
    "exports": {
        ".": "./lib/index.js",
        "./feature/*.js": "./feature/*.js",
        "./feature/internal/*": null
    }
}

Экспорт главной точки входа

При написании нового пакета рекомендуется использовать поле exports:

1
2
3
{
    "exports": "./index.js"
}

Когда определено поле 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", указывающим на тот же модуль:

1
2
3
4
{
    "main": "./index.js",
    "exports": "./index.js"
}

Экспорт подпутей

При использовании поля exports пользовательские подпути могут быть определены вместе с основной точкой входа, рассматривая основную точку входа как "." подпуть:

1
2
3
4
5
6
{
    "exports": {
        ".": "./index.js",
        "./submodule.js": "./src/submodule.js"
    }
}

Теперь только определенный подпуть в exports может быть импортирован потребителем:

1
2
import submodule from 'es-module-package/submodule.js';
// Загружает ./node_modules/es-module-package/src/submodule.js

В то время как другие подпути приведут к ошибке:

1
2
import submodule from 'es-module-package/private-module.js';
// Выброс ERR_PACKAGE_PATH_NOT_EXPORTED

Расширения в подпакетах

Авторы пакетов должны предоставлять либо расширенные (import 'pkg/subpath.js'), либо нерасширенные (import 'pkg/subpath') подпути в своих экспортируемых модулях. Это гарантирует, что для каждого экспортируемого модуля существует только один подпуть, так что все зависимые модули импортируют один и тот же согласованный спецификатор, сохраняя контракт пакета понятным для потребителей и упрощая заполнение подпутей пакета.

Традиционно в пакетах обычно используется стиль без расширения, который имеет преимущества в удобстве чтения и маскировке истинного пути к файлу внутри пакета.

Поскольку import maps теперь является стандартом для разрешения пакетов в браузерах и других средах выполнения JavaScript, использование стиля без расширения может привести к раздутым определениям карт импорта. Явные расширения файлов позволяют избежать этой проблемы, позволяя карте импорта использовать packages folder mapping для отображения нескольких подпутей, где это возможно, вместо отдельной записи карты для экспорта каждого подпути пакета. Это также отражает требование использования полного пути спецификатора в относительных и абсолютных спецификаторах импорта.

Экспорт сахара

Если экспорт "." является единственным экспортом, поле exports предоставляет сахар для этого случая, являясь прямым значением поля exports.

1
2
3
4
5
{
    "exports": {
        ".": "./index.js"
    }
}

можно записать:

1
2
3
{
    "exports": "./index.js"
}

Подпункт импорта

В дополнение к полю exports, существует поле пакета imports для создания частных отображений, которые применяются только к спецификаторам импорта из самого пакета.

Записи в поле "imports" всегда должны начинаться с #, чтобы гарантировать, что они не отличаются от внешних спецификаторов пакетов.

Например, поле imports можно использовать для получения преимуществ условного экспорта для внутренних модулей:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 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.

Шаблоны подпутей

Для пакетов с небольшим количеством экспортов или импортов мы рекомендуем явно перечислять каждую запись подпути exports. Но для пакетов с большим количеством подпутей это может привести к раздуванию package.json и проблемам с обслуживанием.

Для таких случаев вместо этого можно использовать шаблоны экспорта подпутей:

1
2
3
4
5
6
7
8
9
// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js".
  },
  "imports": {
    "#internal/*.js": "./src/internal/*.js"
  }
}

* отображает вложенные подпути, поскольку это синтаксис замены только строк..

Все экземпляры * в правой части будут заменены на это значение, включая те, которые содержат разделители /.

1
2
3
4
5
6
7
8
import featureX from 'es-module-package/features/x.js';
// Загружает ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y.js';
// Загружает ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z.js';
// Загружается ./node_modules/es-module-package/src/internal/z.js

Это прямое статическое сопоставление и замена без какой-либо специальной обработки расширений файлов. Включение *.js с обеих сторон сопоставления ограничивает экспорт экспортируемых пакетов только JS-файлами.

Свойство экспорта быть статически перечислимым сохраняется в шаблонах exports, поскольку индивидуальный экспорт для пакета можно определить, рассматривая правый целевой шаблон как ** glob в списке файлов пакета. Поскольку пути node_modules запрещены в целях exports, это расширение зависит только от файлов самого пакета.

Чтобы исключить личные подпапки из шаблонов, можно использовать цели null:

1
2
3
4
5
6
7
// ./node_modules/es-module-package/package.json
{
    "exports": {
        "./features/*.js": "./src/features/*.js",
        "./features/private-internal/*": null
    }
}
1
2
3
4
5
import featureInternal from 'es-module-package/features/private-internal/m.js';
// Бросает: ERR_PACKAGE_PATH_NOT_EXPORTED

import featureX from 'es-module-package/features/x.js';
// Загружает ./node_modules/es-module-package/src/features/x.js

Условный экспорт

Условные экспорты обеспечивают возможность сопоставления с различными путями в зависимости от определенных условий. Они поддерживаются как для импорта модулей CommonJS, так и для импорта модулей ES.

Например, можно написать пакет, который хочет предоставить разные экспорты ES-модулей для require() и import:

1
2
3
4
5
6
7
8
// package.json
{
    "exports": {
        "import": "./index-module.js",
        "require": "./index-require.cjs"
    },
    "type": "module"
}

Node.js реализует следующие условия, перечисленные в порядке от наиболее специфичных к наименее специфичным, поскольку условия должны быть определены:

  • node-addons - аналогично node и подходит для любого окружения Node.js. Это условие может быть использовано для предоставления точки входа, которая использует родные C++ аддоны, в отличие от точки входа, которая является более универсальной и не полагается на родные аддоны. Это условие может быть отключено с помощью флага --no-addons.
  • node - соответствует любой среде Node.js. Может быть файлом модуля CommonJS или ES. В большинстве случаев явное указание платформы Node.js не требуется.
  • import - соответствует, когда пакет загружается через import или import(), или через любую операцию верхнего уровня import или resolve загрузчика модулей ECMAScript. Применяется независимо от формата модуля в целевом файле. *Всегда взаимоисключающие с require.
  • require - соответствует, когда пакет загружается через require(). Ссылаемый файл должен быть загружаемым с помощью require(), хотя условие выполняется независимо от формата модуля целевого файла. Ожидаемые форматы включают CommonJS, JSON и нативные аддоны, но не модули ES, поскольку require() их не поддерживает. *Всегда взаимоисключающий с import.
  • 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
{
    "exports": {
        ".": "./index.js",
        "./feature.js": {
            "node": "./feature-node.js",
            "default": "./feature.js"
        }
    }
}

Определяет пакет, в котором require('pkg/feature.js') и import 'pkg/feature.js' могут обеспечить различные реализации между Node.js и другими средами JS.

При использовании ветвей окружения всегда включайте условие "по умолчанию", где это возможно. Предоставление условия "по умолчанию" гарантирует, что любые неизвестные JS-окружения смогут использовать эту универсальную реализацию, что помогает избежать необходимости этим JS-окружениям притворяться существующими окружениями для поддержки пакетов с условным экспортом. По этой причине использование ветвей условий "узел" и "по умолчанию" обычно предпочтительнее, чем использование ветвей условий "узел" и "браузер".

Вложенные условия

В дополнение к прямым сопоставлениям, Node.js также поддерживает вложенные объекты условий.

Например, чтобы определить пакет, который имеет точки входа только в двойном режиме для использования в Node.js, но не в браузере:

1
2
3
4
5
6
7
8
9
{
    "exports": {
        "node": {
            "import": "./feature-node.mjs",
            "require": "./feature-node.cjs"
        },
        "default": "./feature.mjs"
    }
}

Условия продолжают сопоставляться в том же порядке, что и в плоских условиях. Если вложенное условие не имеет сопоставления, оно продолжает проверять оставшиеся условия родительского условия. Таким образом, вложенные условия ведут себя аналогично вложенным операторам JavaScript if.

Разрешение пользовательских условий

При работе Node.js пользовательские условия могут быть добавлены с помощью флага --conditions:

1
node --conditions=development index.js

что позволит разрешить условие development в импорте и экспорте пакетов, одновременно разрешая существующие условия node, node-addons, default, import и require.

Любое количество пользовательских условий может быть задано с помощью флагов повтора.

Определения условий сообщества

Строки условий, отличные от условий "import", "require", "node", "node-addons" и "default" реализованных в ядре Node.js, по умолчанию игнорируются.

Другие платформы могут реализовать другие условия, а пользовательские условия могут быть включены в Node.js с помощью флага --conditions / -C.

Поскольку пользовательские условия пакетов требуют четких определений для обеспечения правильного использования, ниже приведен список общих известных условий пакетов и их строгих определений для помощи в координации экосистемы.

  • types - может использоваться системами типизации для разрешения файла типизации для данного экспорта. Это условие всегда должно быть включено первым.
  • deno - указывает на вариацию для платформы Deno.
  • browser - любая среда веб-браузера.
  • react-native - будет соответствовать фреймворку React Native (все платформы). Чтобы нацелить React Native для Web, browser должен быть указан перед этим условием.
  • development - может использоваться для определения точки входа в среду только для разработки, например, для обеспечения дополнительного отладочного контекста, такого как лучшие сообщения об ошибках при запуске в режиме разработки. *Всегда должно быть взаимоисключающим с production.
  • production - может использоваться для определения точки входа в производственную среду. *Всегда должно быть взаимоисключающим с development.

Новые определения условий могут быть добавлены в этот список путем создания pull request в документации Node.js для этого раздела. Требования для включения нового определения условия в этот список следующие:

  • Определение должно быть ясным и недвусмысленным для всех исполнителей.
  • Должно быть четко обосновано, почему условие необходимо.
  • Должно существовать достаточное количество существующих реализаций.
  • Имя условия не должно конфликтовать с другим определением условия или условием в широком использовании.
  • Перечисление определения условия должно обеспечивать координационную выгоду для экосистемы, которая иначе была бы невозможна. Например, это не обязательно относится к условиям, специфичным для компании или приложения.

Приведенные выше определения могут быть со временем перенесены в специальный реестр условий.

Самостоятельная ссылка на пакет с помощью его имени

Внутри пакета на значения, определенные в поле package.json exports, можно ссылаться через имя пакета. Например, если package.json имеет вид:

1
2
3
4
5
6
7
8
// package.json
{
    "name": "a-package",
    "exports": {
        ".": "./index.mjs",
        "./foo.js": "./foo.js"
    }
}

Затем любой модуль в этом пакете может ссылаться на экспорт в самом пакете:

1
2
// ./a-module.mjs
import { something } from 'a-package'; // Импортирует "что-то" из ./index.mjs.

Самостоятельная ссылка доступна только если в package.json есть "exports", и позволит импортировать только то, что разрешено этим "exports"package.json). Таким образом, приведенный ниже код, учитывая предыдущий пакет, выдаст ошибку во время выполнения:

1
2
3
4
5
6
// ./another-module.mjs

// Импортирует "другой" из ./m.mjs. Fails because.
// поле "exports" "package.json"
// не содержит экспорта с именем "./m.mjs".
import { another } from 'a-package/m.mjs';

Самостоятельные ссылки также доступны при использовании require, как в модуле ES, так и в модуле CommonJS. Например, этот код также будет работать:

1
2
// ./a-module.js
const { something } = require('a-package/foo.js'); // Загружается из ./foo.js.

Наконец, самоссылка также работает с пакетами, имеющими скопированную структуру. Например, этот код также будет работать:

1
2
3
4
5
// package.json
{
    "name": "@my/package",
    "exports": "./index.js"
}
1
2
// ./index.js
module.exports = 42;
1
2
// ./other.js
console.log(require('@my/package'));
1
2
$ node other.js
42

Двойные пакеты модулей CommonJS/ES

До появления поддержки ES-модулей в Node.js для авторов пакетов было обычной практикой включать в пакет исходные тексты JavaScript как CommonJS, так и ES-модуля, причем package.json "main" указывал точку входа CommonJS, а package.json "module" указывал точку входа ES-модуля. Это позволяло Node.js запускать точку входа CommonJS, в то время как инструменты сборки, такие как бандлеры, использовали точку входа модуля ES, поскольку Node.js игнорировал (и все еще игнорирует) поле верхнего уровня "module".

Теперь Node.js может запускать точки входа ES-модулей, и пакет может содержать как точки входа CommonJS, так и ES-модулей (либо через отдельные спецификаторы, такие как 'pkg' и 'pkg/es-module', либо оба в одном спецификаторе через Conditional exports). В отличие от сценария, когда "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 работают в средах модулей all-CommonJS или all-ES, соответственно, и поэтому вызывает удивление у пользователей. Это также отличается от поведения, с которым пользователи знакомы при использовании транспиляции с помощью таких инструментов, как Babel или esm.

Написание двойных пакетов, избегая или минимизируя опасности

Во-первых, опасность, описанная в предыдущем разделе, возникает, когда пакет содержит исходные тексты модулей CommonJS и ES, и оба источника предоставляются для использования в Node.js либо через отдельные основные точки входа, либо через экспортируемые пути. Вместо этого можно написать пакет, в котором любая версия Node.js получает только источники CommonJS, а любые отдельные источники модуля ES, которые может содержать пакет, предназначены только для других сред, таких как браузеры. Такой пакет будет доступен для использования в любой версии Node.js, поскольку import может ссылаться на файлы CommonJS; но он не будет предоставлять никаких преимуществ использования синтаксиса ES-модулей.

Пакет также может перейти с синтаксиса CommonJS на синтаксис ES-модулей при breaking change изменении версии. Это имеет тот недостаток, что самая новая версия пакета может быть использована только в версиях Node.js, поддерживающих ES-модули.

Каждый паттерн имеет свои компромиссы, но есть два общих подхода, которые удовлетворяют следующим условиям:

  1. Пакет можно использовать как через require, так и через import.
  2. Пакет можно использовать как в текущей версии Node.js, так и в старых версиях Node.js, в которых отсутствует поддержка ES-модулей.
  3. Главная точка входа пакета, например, 'pkg', может использоваться как require для разрешения на файл CommonJS, так и import для разрешения на файл ES-модуля. (Аналогично для экспортируемых путей, например, 'pkg/feature').
  4. Пакет предоставляет именованные экспорты, например, import { name } from 'pkg', а не import pkg from 'pkg'; pkg.name.
  5. Пакет потенциально может быть использован в других средах ES-модулей, например, в браузерах.
  6. Опасности, описанные в предыдущем разделе, исключены или сведены к минимуму.

Подход #1: Использовать обертку ES-модуля

Напишите пакет в CommonJS или транспилируйте исходники ES-модуля в CommonJS и создайте файл-обертку ES-модуля, определяющий именованные экспорты. Используя Conditional exports, обертка модуля ES используется для import и точки входа CommonJS для require.

1
2
3
4
5
6
7
8
// ./node_modules/pkg/package.json
{
    "type": "module",
    "exports": {
        "import": "./wrapper.mjs",
        "require": "./index.cjs"
    }
}

В предыдущем примере явно используются расширения .mjs и .cjs. Если ваши файлы используют расширение .js, то "type": "module" приведет к тому, что такие файлы будут рассматриваться как модули ES, так же как "type": "commonjs" приведет к тому, что они будут рассматриваться как CommonJS. См. Enabling.

1
2
// ./node_modules/pkg/index.cjs
exports.name = 'value';
1
2
3
// ./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', то вместо этого обертку следует написать для экспорта по умолчанию опционально вместе с любыми именованными экспортами:

1
2
3
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-модуля (например, потому что пакет не имеет состояния):

1
2
3
4
5
6
7
8
// ./node_modules/pkg/package.json
{
    "type": "module",
    "exports": {
        ".": "./index.cjs",
        "./module": "./wrapper.mjs"
    }
}

Подход #2: Изолировать состояние

Файл package.json может напрямую определять отдельные точки входа модулей CommonJS и ES:

1
2
3
4
5
6
7
8
// ./node_modules/pkg/package.json
{
    "type": "module",
    "exports": {
        "import": "./index.mjs",
        "require": "./index.cjs"
    }
}

Это можно сделать, если обе версии пакета - CommonJS и ES-модуля - эквивалентны, например, потому что одна является транспилированным результатом другой; и управление состоянием пакета тщательно изолировано (или пакет не имеет состояния).

Причина, по которой состояние является проблемой, заключается в том, что обе версии пакета - CommonJS и ES-модуля - могут использоваться в приложении; например, пользовательский код приложения может "импортировать" версию ES-модуля, в то время как зависимость "требует" версию CommonJS. В этом случае в память будут загружены две копии пакета и, следовательно, будут присутствовать два разных состояния. Это, скорее всего, привело бы к трудноустранимым ошибкам.

Помимо написания пакета без состояния (если бы, например, JavaScript Math был пакетом, он был бы без состояния, так как все его методы статичны), есть несколько способов изолировать состояние, чтобы оно было общим для потенциально загруженных экземпляров пакета CommonJS и модуля ES:

  1. Если возможно, содержать все состояние внутри инстанцированного объекта. Например, объект JavaScript Date должен быть инстанцирован, чтобы содержать состояние; если бы это был пакет, он использовался бы следующим образом:
1
2
3
import Date from 'date';
const someDate = new Date();
// someDate содержит состояние; Date - нет

Ключевое слово new не обязательно; функция пакета может вернуть новый объект или модифицировать переданный объект, чтобы сохранить состояние, внешнее по отношению к пакету.

  1. Изолируйте состояние в одном или нескольких файлах CommonJS, которые являются общими для версий пакета CommonJS и ES-модуля. Например, если точками входа в CommonJS и ES-модуль являются index.cjs и index.mjs соответственно:
1
2
3
// ./node_modules/pkg/index.cjs
const state = require('./state.cjs');
module.exports.state = state;
1
2
3
// ./node_modules/pkg/index.mjs
import state from './state.cjs';
export { state };

Даже если pkg используется через require и import в приложении (например, через import в коде приложения и через require зависимостью), каждая ссылка pkg будет содержать одно и то же состояние; и изменение этого состояния из любой модульной системы будет применяться к обеим.

Любые плагины, которые присоединяются к синглтону пакета, должны будут отдельно присоединяться к синглтонам модулей CommonJS и ES.

Этот подход подходит для любого из следующих случаев использования:

  • Пакет в настоящее время написан в синтаксисе модуля ES, и автор пакета хочет, чтобы эта версия использовалась везде, где поддерживается такой синтаксис.
  • Пакет не имеет состояния или его состояние может быть изолировано без особых трудностей.

Определения полей Node.js package.json

В этом разделе описаны поля, используемые средой выполнения Node.js. Другие инструменты (например, npm) используют дополнительные поля, которые игнорируются Node.js и не документируются здесь.

В Node.js используются следующие поля в файлах package.json:

  • имя - Актуально при использовании именованных импортов внутри пакета. Также используется менеджерами пакетов в качестве имени пакета.
  • main - Модуль по умолчанию при загрузке пакета, если не указан exports, и в версиях Node.js до введения exports.
  • packageManager - Менеджер пакетов, рекомендуемый при внесении вклада в пакет. Используется шеймами Corepack.
  • type - Тип пакета, определяющий, загружать ли файлы .js как модули CommonJS или ES.
  • exports - Экспорт пакетов и условный экспорт. Когда присутствует, ограничивает, какие подмодули могут быть загружены из пакета.
  • imports - Импорт пакета, для использования модулями внутри самого пакета.

name

1
2
3
{
    "name": "package-name"
}

Поле имя определяет имя вашего пакета. Для публикации в реестре npm требуется имя, удовлетворяющее определенным требованиям.

Поле имя можно использовать в дополнение к полю экспорты для самоссылки пакета, используя его имя.

main

1
2
3
{
    "main": "./index.js"
}

Поле "main" определяет точку входа пакета при импорте по имени через поиск node_modules. Его значением является путь.

Если пакет имеет поле "exports", оно будет иметь приоритет над полем "main" при импорте пакета по имени.

Он также определяет сценарий, который используется, когда каталог пакета загружается через require().

1
2
// Это разрешается в ./path/to/directory/index.js.
require('./path/to/directory');

packageManager

Стабильность: 1 – Экспериментальная

Фича изменяется и не допускается флагом командной строки. Может быть изменена или удалена в последующих версиях.

1
2
3
{
    "packageManager": "<имя менеджера пакетов>@<версия>"
}

Поле packageManager определяет, какой менеджер пакетов будет использоваться при работе над текущим проектом. Оно может быть установлено в любой из поддерживаемых менеджеров пакетов и гарантирует, что ваши команды используют одинаковые версии менеджеров пакетов без необходимости устанавливать что-либо еще, кроме Node.js.

В настоящее время это поле является экспериментальным, и его необходимо активировать; ознакомьтесь со страницей Corepack для получения подробной информации о процедуре.

type

Поле "type" определяет формат модуля, который Node.js использует для всех файлов .js, ближайшим родителем которых является файл package.json.

Файлы, заканчивающиеся на .js, загружаются как модули ES, если ближайший родительский файл package.json содержит поле верхнего уровня "type" со значением "module".

Ближайший родительский package.json определяется как первый package.json, найденный при поиске в текущей папке, родительской папке этой папки и так далее, пока не будет достигнута папка node_modules или корень тома.

1
2
3
4
// package.json
{
    "type": "module"
}
1
2
# В той же папке, что и предыдущий package.json
node my-app.js # Запускается как ES модуль

Если в ближайшем родительском package.json отсутствует поле "type" или содержится "type": "commonjs", файлы .js рассматриваются как CommonJS. Если достигнут корень тома и не найден package.json, файлы .js рассматриваются как CommonJS.

Операторы импорта файлов .js рассматриваются как модули ES, если ближайший родительский package.json содержит "type": "module".

1
2
// my-app.js, часть того же примера, что и выше
import './startup.js'; // Загружается как ES-модуль из-за package.json

Независимо от значения поля тип, файлы .mjs всегда рассматриваются как модули ES, а файлы .cjs всегда рассматриваются как CommonJS.

exports

1
2
3
{
    "exports": "./index.js"
}

Поле "exports" позволяет определить точки входа пакета при импорте по имени, загруженному либо через поиск node_modules, либо самоссылкой на его собственное имя. Поддерживается в Node.js 12+ как альтернатива главному, который может поддерживать определение subpath exports и conditional exports при инкапсуляции внутренних неэкспортированных модулей.

Условный экспорт также может быть использован в "exports" для определения различных точек входа в пакет для каждого окружения, включая то, ссылается ли пакет через require или через import.

Все пути, определенные в "exports", должны быть относительными URL файлов, начинающимися с ./.

imports

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// package.json
{
    "imports": {
        "#dep": {
            "node": "dep-node-native",
            "default": "./dep-polyfill.js"
        }
    },
    "dependencies": {
        "dep-node-native": "^1.0.0"
    }
}

Записи в поле imports должны быть строками, начинающимися с #.

Импорт пакетов разрешает сопоставление с внешними пакетами.

Это поле определяет subpath imports для текущего пакета.

Комментарии