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

Модули: ECMAScript

Добавлено в: v8.5.0

Стабильность: 2 – Стабильная

Введение

Модули ECMAScript — официальный стандартный формат для упаковки кода JavaScript для повторного использования. Модули задаются с помощью различных операторов import и export.

Ниже пример ES-модуля, экспортирующего функцию:

1
2
3
4
5
6
// addTwo.mjs
function addTwo(num) {
    return num + 2;
}

export { addTwo };

Ниже пример ES-модуля, импортирующего функцию из addTwo.mjs:

1
2
3
4
5
// app.mjs
import { addTwo } from './addTwo.mjs';

// Prints: 6
console.log(addTwo(4));

Node.js полностью поддерживает модули ECMAScript в том виде, в каком они сейчас описаны в спецификации, и обеспечивает взаимодействие между ними и изначальным форматом модулей — CommonJS.

Включение

У Node.js две системы модулей: модули CommonJS и модули ECMAScript.

Авторы могут указать Node.js интерпретировать JavaScript как ES-модуль через расширение файла .mjs, поле package.json "type" со значением "module" или флаг --input-type со значением "module". Это явные признаки того, что код должен выполняться как ES-модуль.

Наоборот, авторы могут явно указать Node.js интерпретировать JavaScript как CommonJS через расширение .cjs, поле package.json "type" со значением "commonjs" или флаг --input-type со значением "commonjs".

Если в коде нет явных маркеров ни для одной из систем модулей, Node.js просматривает исходный текст модуля на наличие синтаксиса ES-модулей. Если такой синтаксис найден, код выполняется как ES-модуль; иначе модуль выполняется как CommonJS. Подробнее см. Определение системы модулей.

Пакеты

Этот раздел перенесён в Модули: пакеты.

Спецификаторы import

Терминология

Спецификатор оператора import — это строка после ключевого слова from, например 'node:path' в import { sep } from 'node:path'. Спецификаторы также используются в операторах export from и в качестве аргумента выражения import().

Существует три типа спецификаторов:

  • Относительные спецификаторы, например './startup.js' или '../config.mjs'. Они указывают путь относительно расположения импортирующего файла. Расширение файла для таких спецификаторов всегда обязательно.

  • Голые спецификаторы, например 'some-package' или 'some-package/shuffle'. Они могут ссылаться на основную точку входа пакета по имени или на конкретный функциональный модуль внутри пакета с префиксом имени пакета, как в примерах. Указывать расширение файла нужно только для пакетов без поля "exports".

  • Абсолютные спецификаторы, например 'file:///opt/nodejs/config.js'. Они прямо и однозначно ссылаются на полный путь.

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

Как и в CommonJS, файлы модулей внутри пакетов доступны добавлением пути к имени пакета, если только в package.json пакета нет поля "exports" — тогда файлы внутри пакета доступны только по путям, заданным в "exports".

Подробнее о правилах разрешения пакетов для голых спецификаторов см. в документации по пакетам.

Обязательные расширения файлов

При использовании ключевого слова import для разрешения относительных или абсолютных спецификаторов нужно указать расширение файла. Индексы каталогов (например './startup/index.js') также должны быть указаны полностью.

Такое поведение соответствует тому, как import ведёт себя в браузере при обычно настроенном сервере.

URL

ES-модули разрешаются и кэшируются как URL. Поэтому специальные символы нужно кодировать в процентах, например # как %23 и ? как %3F.

Поддерживаются схемы URL file:, node: и data:. Спецификатор вроде 'https://example.com/app.js' нативно в Node.js не поддерживается, если не используется пользовательский HTTPS-загрузчик.

URL file:

Модуль загружается несколько раз, если спецификатор import, которым он разрешается, имеет другой query или fragment.

1
2
import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"

Корень тома можно задать через /, // или file:///. Учитывая различия между URL и разрешением путей (в том числе детали процентного кодирования), при импорте пути рекомендуется использовать url.pathToFileURL.

Импорты data:

data: URLs поддерживаются для импорта со следующими MIME-типами:

  • text/javascript — для ES-модулей
  • application/json — для JSON
  • application/wasm — для Wasm
1
2
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' with { type: 'json' };

data: URL разрешают только голые спецификаторы для встроенных модулей и абсолютные спецификаторы. Разрешение относительных спецификаторов не работает, потому что data: не является специальной схемой. Например, попытка загрузить ./foo из data:text/javascript,import "./foo"; не разрешается, потому что для data: URL нет понятия относительного разрешения.

Импорты node:

URL node: поддерживаются как альтернативный способ загрузки встроенных модулей Node.js. Эта схема URL позволяет ссылаться на встроенные модули допустимыми абсолютными строками URL.

1
import fs from 'node:fs/promises';

Атрибуты импорта

Атрибуты импорта — встроенный синтаксис для операторов импорта модулей, чтобы передавать дополнительную информацию вместе со спецификатором модуля.

1
2
3
4
import fooData from './foo.json' with { type: 'json' };

const { default: barData } =
  await import('./bar.json', { with: { type: 'json' } });

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

Атрибут type Назначение
'json' JSON-модули

Атрибут type: 'json' обязателен при импорте JSON-модулей.

Встроенные модули

Встроенные модули предоставляют именованные экспорты своего публичного API. Также есть экспорт по умолчанию — это значение экспорта CommonJS. Экспорт по умолчанию можно использовать, в том числе для изменения именованных экспортов. Именованные экспорты встроенных модулей обновляются только при вызове module.syncBuiltinESMExports().

1
2
import EventEmitter from 'node:events';
const e = new EventEmitter();

1
2
3
4
5
6
7
8
import { readFile } from 'node:fs';
readFile('./foo.txt', (err, source) => {
    if (err) {
        console.error(err);
    } else {
        console.log(source);
    }
});

1
2
3
4
5
6
7
8
import fs, { readFileSync } from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { Buffer } from 'node:buffer';

fs.readFileSync = () => Buffer.from('Hello, ESM');
syncBuiltinESMExports();

fs.readFileSync === readFileSync;

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

Выражения import()

Динамический import() даёт асинхронный способ импортировать модули. Он поддерживается и в CommonJS, и в ES-модулях и может загружать как CommonJS, так и ES-модули.

import.meta

Мета-свойство import.meta — это Object со следующими свойствами. Оно поддерживается только в ES-модулях.

import.meta.dirname

  • Тип: <string> Имя каталога текущего модуля.

Это то же самое, что path.dirname() от import.meta.filename.

Ограничение: свойство есть только у модулей с протоколом file:.

import.meta.filename

  • Тип: <string> Полный абсолютный путь и имя файла текущего модуля с разрешёнными символическими ссылками.

Это то же самое, что url.fileURLToPath() от import.meta.url.

Ограничение: это свойство поддерживают только локальные модули. У модулей без протокола file: его не будет.

import.meta.url

  • Тип: <string> Абсолютный file: URL модуля.

Определено так же, как в браузерах: URL текущего файла модуля.

Это позволяет удобно загружать файлы относительно модуля:

1
2
3
4
import { readFileSync } from 'node:fs';
const buffer = readFileSync(
    new URL('./data.proto', import.meta.url)
);

import.meta.main

Стабильность: 1.0 – ранняя разработка

  • Тип: <boolean> true, если текущий модуль — точка входа процесса; иначе false.

Эквивалентно require.main === module в CommonJS.

Аналогично __name__ == "__main__" в Python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export function foo() {
    return 'Hello, world';
}

function main() {
    const message = foo();
    console.log(message);
}

if (import.meta.main) main();
// `foo` can be imported from another module without possible side-effects from `main`

import.meta.resolve(specifier)

Стабильность: 1.2 – кандидат в релиз

  • specifier <string> Спецификатор модуля для разрешения относительно текущего модуля.
  • Возвращает: <string> Абсолютная строка URL, в которую разрешился бы спецификатор.

import.meta.resolve — функция разрешения относительно модуля с областью действия на каждый модуль; возвращает строку URL.

1
2
3
4
5
6
const dependencyAsset = import.meta.resolve(
    'component-lib/asset.css'
);
// file:///app/node_modules/component-lib/asset.css
import.meta.resolve('./dep.js');
// file:///app/dep.js

Поддерживаются все возможности разрешения модулей Node.js. Разрешение зависимостей ограничено допустимыми разрешениями exports внутри пакета.

Ограничения:

  • Могут выполняться синхронные операции с файловой системой, что сказывается на производительности аналогично require.resolve.
  • В пользовательских загрузчиках эта возможность недоступна (возник бы взаимный тупик).

Нестандартный API:

При использовании флага --experimental-import-meta-resolve функция принимает второй аргумент:

  • parent <string> | <URL> Необязательный абсолютный URL родительского модуля, от которого разрешать. По умолчанию: import.meta.url

Взаимодействие с CommonJS

Операторы import

Оператор import может ссылаться на ES-модуль или модуль CommonJS. Операторы import допустимы только в ES-модулях, но динамические выражения import() поддерживаются в CommonJS для загрузки ES-модулей.

При импорте модулей CommonJS объект module.exports предоставляется как экспорт по умолчанию. Именованные экспорты могут быть доступны благодаря статическому анализу как удобство для совместимости с экосистемой.

require

В CommonJS require сейчас поддерживает загрузку только синхронных ES-модулей (то есть ES-модулей без top-level await).

Подробнее см. Загрузка ECMAScript-модулей через require().

Пространства имён CommonJS

Модули CommonJS состоят из объекта module.exports произвольного типа.

Чтобы это поддержать, при импорте CommonJS из ES-модуля строится обёртка пространства имён для модуля CommonJS: у неё всегда есть ключ экспорта default, указывающий на значение module.exports в CommonJS.

Дополнительно к исходному тексту модуля CommonJS применяется эвристический статический анализ, чтобы составить по возможности статический список экспортов для пространства имён из значений на module.exports. Это нужно, потому что пространства имён должны быть построены до выполнения модуля CJS.

Объекты пространства имён CommonJS также предоставляют экспорт default как именованный экспорт 'module.exports', чтобы однозначно показать, что в CommonJS используется именно это значение, а не объект пространства имён. Это согласуется с семантикой обработки имени экспорта 'module.exports' в поддержке взаимодействия require(esm).

Импортируя модуль CommonJS, к нему можно надёжно обратиться через экспорт по умолчанию ES-модуля или эквивалентный «синтаксический сахар»:

1
2
3
4
5
6
7
8
9
import { default as cjs } from 'cjs';
// Identical to the above
import cjsSugar from 'cjs';

console.log(cjs);
console.log(cjs === cjsSugar);
// Prints:
//   <module.exports>
//   true

Этот экзотический объект пространства имён модуля можно увидеть напрямую при import * as m from 'cjs' или динамическом импорте:

1
2
3
4
5
6
import * as m from 'cjs';
console.log(m);
console.log(m === (await import('cjs')));
// Prints:
//   [Module] { default: <module.exports>, 'module.exports': <module.exports> }
//   true

Для лучшей совместимости с существующими практиками в экосистеме JS Node.js дополнительно пытается определить именованные экспорты CommonJS для каждого импортируемого модуля CommonJS и предоставить их как отдельные экспорты ES-модуля через статический анализ.

Например, модуль CommonJS:

1
2
// cjs.cjs
exports.name = 'exported';

Поддерживает именованные импорты в ES-модулях:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { name } from './cjs.cjs';
console.log(name);
// Prints: 'exported'

import cjs from './cjs.cjs';
console.log(cjs);
// Prints: { name: 'exported' }

import * as m from './cjs.cjs';
console.log(m);
// Prints:
//   [Module] {
//     default: { name: 'exported' },
//     'module.exports': { name: 'exported' },
//     name: 'exported'
//   }

Как видно из последнего примера с логированием экзотического объекта пространства имён, экспорт name копируется с объекта module.exports и задаётся напрямую в пространстве имён ES-модуля при импорте.

Обновления «живых» привязок или новые экспорты, добавленные в module.exports, для таких именованных экспортов не отслеживаются.

Определение именованных экспортов опирается на типичные синтаксические шаблоны, но не всегда даёт верный результат. В таких случаях лучше подойдёт импорт по умолчанию, описанный выше.

Охват обнаружения именованных экспортов включает многие распространённые шаблоны экспорта, реэкспорта и выходы сборщиков и транспайлеров. Точные правила см. в merve.

Отличия ES-модулей от CommonJS

Нет require, exports и module.exports

В большинстве случаев для загрузки модулей CommonJS можно использовать ES-import.

При необходимости функцию require можно создать внутри ES-модуля через module.createRequire().

Нет __filename и __dirname

Эти переменные CommonJS в ES-модулях недоступны.

Сценарии с __filename и __dirname можно воспроизвести через import.meta.filename и import.meta.dirname.

Нет загрузки аддонов

Импорты ES-модулей аддоны сейчас не поддерживают.

Их можно загрузить через module.createRequire() или process.dlopen.

Нет require.main

Вместо require.main === module есть API import.meta.main.

Нет require.resolve

Относительное разрешение — через new URL('./local', import.meta.url).

Полная замена require.resolve — API import.meta.resolve.

Либо можно использовать module.createRequire().

Нет NODE_PATH

NODE_PATH не участвует в разрешении спецификаторов import. Если нужно подобное поведение, используйте символические ссылки.

Нет require.extensions

import не использует require.extensions. Замену можно обеспечить хуками настройки модулей.

Нет require.cache

import не использует require.cache — у загрузчика ES-модулей свой отдельный кэш.

JSON-модули

На JSON-файлы можно ссылаться через import:

1
import packageConfig from './package.json' with { type: 'json' };

Синтаксис with { type: 'json' } обязателен; см. атрибуты импорта.

Импортируемый JSON даёт только экспорт default. Именованных экспортов нет. В кэше CommonJS создаётся запись, чтобы избежать дублирования. Тот же объект возвращается в CommonJS, если JSON-модуль уже был импортирован с того же пути.

Wasm-модули

Поддерживается импорт как экземпляров модулей WebAssembly, так и импортов в фазе исходника (source phase).

Оба варианта согласованы с предложении интеграции ES-модулей для WebAssembly.

Импорты Wasm в фазе исходника

Стабильность: 1.2 – кандидат в релиз

Предложение импортов фазы исходника позволяет сочетанию ключевых слов import source импортировать объект WebAssembly.Module напрямую, вместо получения уже инстанцированного экземпляра модуля с зависимостями.

Это полезно, когда нужны кастомные инстанции Wasm, но разрешение и загрузка по-прежнему через интеграцию ES-модулей.

Например, чтобы создать несколько экземпляров модуля или передать пользовательские импорты в новый экземпляр library.wasm:

1
2
3
4
5
import source libraryModule from './library.wasm';

const instance1 = await WebAssembly.instantiate(libraryModule, importObject1);

const instance2 = await WebAssembly.instantiate(libraryModule, importObject2);

Помимо статической фазы исходника есть динамический вариант фазы исходника через синтаксис динамического импорта фазы import.source:

1
2
3
4
5
6
7
8
const dynamicLibrary = await import.source(
    './library.wasm'
);

const instance = await WebAssembly.instantiate(
    dynamicLibrary,
    importObject
);

Встроенные строковые функции JavaScript

Стабильность: 1.2 – кандидат в релиз

При импорте модулей WebAssembly предложение предложение WebAssembly JS String Builtins автоматически включается через интеграцию ESM. Это позволяет модулям WebAssembly напрямую использовать эффективные встроенные строковые функции времени компиляции из пространства имён wasm:js-string.

Например, следующий Wasm-модуль экспортирует строковую функцию getLength с использованием встроенной length из wasm:js-string:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(module
  ;; Compile-time import of the string length builtin.
  (import "wasm:js-string" "length" (func $string_length (param externref) (result i32)))

  ;; Define getLength, taking a JS value parameter assumed to be a string,
  ;; calling string length on it and returning the result.
  (func $getLength (param $str externref) (result i32)
    local.get $str
    call $string_length
  )

  ;; Export the getLength function.
  (export "getLength" (func $get_length))
)

1
2
import { getLength } from './string-len.wasm';
getLength('foo'); // Returns 3.

Встроенные функции Wasm — это импорты времени компиляции, связываемые при компиляции модуля, а не при инстанцировании. Они ведут себя не как обычные импорты графа модулей и их нельзя просмотреть через WebAssembly.Module.imports(mod) или виртуализовать без перекомпиляции модуля через прямой API WebAssembly.compile с отключёнными строковыми встроенными функциями.

Строковые константы также можно импортировать из встроенного URL импорта wasm:js/string-constants, чтобы задать статические глобальные строки JS:

1
2
3
(module
  (import "wasm:js/string-constants" "hello" (global $hello externref))
)

Импорт модуля в фазе исходника до инстанцирования тоже автоматически использует встроенные функции времени компиляции:

1
2
3
import source mod from './string-len.wasm';
const { exports: { getLength } } = await WebAssembly.instantiate(mod, {});
getLength('foo'); // Also returns 3.

Импорты фазы экземпляра Wasm

Стабильность: 1.1 – активная разработка

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

Например, index.js с содержимым:

1
2
import * as M from './library.wasm';
console.log(M);

при запуске:

1
node index.mjs

даст интерфейс экспортов для инстанцирования library.wasm.

Зарезервированные пространства имён Wasm

При импорте экземпляров модулей WebAssembly нельзя использовать имена импортируемых модулей или имена импорта/экспорта с зарезервированными префиксами:

  • wasm-js: — зарезервировано для всех имён импорта модулей, имён модулей и имён экспорта.
  • wasm: — зарезервировано для имён импорта модулей и имён экспорта (имена импортируемых модулей разрешены для поддержки будущих полифиллов встроенных функций).

Импорт модуля с перечисленными зарезервированными именами вызовет WebAssembly.LinkError.

await верхнего уровня

Ключевое слово await можно использовать в теле модуля ECMAScript на верхнем уровне.

Пусть в a.mjs

1
export const five = await Promise.resolve(5);

а в b.mjs

1
2
3
import { five } from './a.mjs';

console.log(five); // Logs `5`

1
node b.mjs # works

Если выражение top-level await никогда не завершится, процесс node завершится с кодом 13 (status code).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { spawn } from 'node:child_process';
import { execPath } from 'node:process';

spawn(execPath, [
    '--input-type=module',
    '--eval',
    // Never-resolving Promise:
    'await new Promise(() => {})',
]).once('exit', (code) => {
    console.log(code); // Logs `13`
});

Загрузчики

Документация по загрузчикам перенесена в Модули: хуки настройки.

Алгоритм разрешения и загрузки

Возможности

У стандартного резолвера такие свойства:

  • разрешение на основе FileURL, как у ES-модулей;
  • относительное и абсолютное разрешение URL;
  • нет расширений по умолчанию;
  • нет «главных» файлов каталогов;
  • разрешение голых спецификаторов пакетов через поиск в node_modules;
  • не падает на неизвестных расширениях или протоколах;
  • при необходимости может передать в фазу загрузки подсказку о формате.

У стандартного загрузчика такие свойства:

  • загрузка встроенных модулей через URL node:;
  • «встроенная» загрузка модулей через URL data:;
  • загрузка модулей file:;
  • ошибка на любой другой протокол URL;
  • ошибка на неизвестных расширениях при загрузке file: (поддерживаются только .cjs, .js и .mjs).

Алгоритм разрешения

Алгоритм загрузки спецификатора ES-модуля задаётся методом ESM_RESOLVE ниже. Он возвращает разрешённый URL для спецификатора модуля относительно parentURL.

Алгоритм разрешения определяет полный разрешённый URL для загрузки модуля вместе с предполагаемым форматом модуля. Сам алгоритм разрешения не решает, можно ли загрузить протокол разрешённого URL и допустимы ли расширения файлов — эти проверки выполняет Node.js на фазе загрузки (например, если запрошен URL с протоколом, отличным от file:, data: или node:).

Алгоритм также пытается определить формат файла по расширению (см. алгоритм ESM_FILE_FORMAT ниже). Если расширение не распознано (например, это не .mjs, .cjs или .json), возвращается формат undefined, что приведёт к ошибке на фазе загрузки.

Формат модуля для разрешённого URL задаётся ESM_FILE_FORMAT — он возвращает однозначный формат для любого файла. Формат «module» соответствует ECMAScript-модулю, а «commonjs» — загрузке через устаревший загрузчик CommonJS. Дополнительные форматы, например «addon», могут появиться в будущих версиях.

В приведённых ниже алгоритмах ошибки подпрограмм пробрасываются как ошибки верхнего уровня, если явно не сказано иное.

defaultConditions — массив имён условной среды, ["node", "import"].

Резолвер может выбросить такие ошибки:

  • Недопустимый спецификатор модуля: спецификатор модуля — недопустимый URL, имя пакета или подпуть пакета.
  • Недопустимая конфигурация пакета: конфигурация package.json недопустима или содержит недопустимые данные.
  • Недопустимая цель пакета: в exports или imports пакета целевой модуль имеет недопустимый тип или строку.
  • Путь пакета не экспортирован: exports пакета не задаёт и не разрешает целевой подпуть для данного модуля.
  • Импорт пакета не определён: в imports пакета нет такого спецификатора.
  • Модуль не найден: запрошенный пакет или модуль не существует.
  • Импорт каталога не поддерживается: разрешённый путь указывает на каталог, импорт модулей в каталог не поддерживается.

Спецификация алгоритма разрешения

ESM_RESOLVE(specifier, parentURL)

  1. Пусть resolved будет undefined.
  2. Если specifier является корректным URL, тогда
    1. Установить resolved равным результату разбора и повторной сериализации specifier как URL.
  3. Иначе, если specifier начинается с "/", "./" или "../", тогда
    1. Установить resolved равным результату разрешения URL для specifier относительно parentURL.
  4. Иначе, если specifier начинается с "#", тогда
    1. Установить resolved равным результату PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
  5. Иначе,
    1. Примечание: теперь specifier является голым спецификатором.
    2. Установить resolved равным результату PACKAGE_RESOLVE(specifier, parentURL).
  6. Пусть format будет undefined.
  7. Если resolved является URL вида "file:", тогда
    1. Если resolved содержит процентные кодировки "/" или "\" ("%2F" и "%5C" соответственно), тогда
      1. Выбросить ошибку Недопустимый спецификатор модуля.
    2. Если файл по адресу resolved является каталогом, тогда
      1. Выбросить ошибку Импорт каталога не поддерживается.
    3. Если файл по адресу resolved не существует, тогда
      1. Выбросить ошибку Модуль не найден.
    4. Установить resolved в реальный путь resolved, сохранив те же компоненты строки запроса URL и фрагмента.
    5. Установить format равным результату ESM_FILE_FORMAT(resolved).
  8. Иначе,
    1. Установить format в формат модуля для типа содержимого, связанного с URL resolved.
  9. Вернуть format и resolved на этап загрузки.

PACKAGE_RESOLVE(packageSpecifier, parentURL)

  1. Пусть packageName будет undefined.
  2. Если packageSpecifier является пустой строкой, тогда
    1. Выбросить ошибку Недопустимый спецификатор модуля.
  3. Если packageSpecifier является именем встроенного модуля Node.js, тогда
    1. Вернуть строку "node:", объединённую с packageSpecifier.
  4. Если packageSpecifier не начинается с "@", тогда
    1. Установить packageName равным подстроке packageSpecifier до первого разделителя "/" или до конца строки.
  5. Иначе,
    1. Если packageSpecifier не содержит разделитель "/", тогда
      1. Выбросить ошибку Недопустимый спецификатор модуля.
    2. Установить packageName равным подстроке packageSpecifier до второго разделителя "/" или до конца строки.
  6. Если packageName начинается с "." или содержит "\" либо "%", тогда
    1. Выбросить ошибку Недопустимый спецификатор модуля.
  7. Пусть packageSubpath будет ".", объединённая с подстрокой packageSpecifier от позиции длины packageName.
  8. Пусть selfUrl будет результатом PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
  9. Если selfUrl не равен undefined, вернуть selfUrl.
  10. Пока parentURL не является корнем файловой системы,
    1. Пусть packageURL будет результатом разрешения URL "node_modules/", объединённого с packageName, относительно parentURL.
    2. Установить parentURL в URL родительской папки для parentURL.
    3. Если каталог по адресу packageURL не существует, тогда
      1. Продолжить следующую итерацию цикла.
    4. Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
    5. Если pjson не равен null и pjson.exports не равен null или undefined, тогда
      1. Вернуть результат PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
    6. Иначе, если packageSubpath равен ".", тогда
      1. Если pjson.main является строкой, тогда
        1. Вернуть разрешение URL main в packageURL.
    7. Иначе,
      1. Вернуть разрешение URL packageSubpath в packageURL.
  11. Выбросить ошибку Модуль не найден.

PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)

  1. Пусть packageURL будет результатом LOOKUP_PACKAGE_SCOPE(parentURL).
  2. Если packageURL равно null, то
    1. Вернуть undefined.
  3. Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
  4. Если pjson равно null либо pjson.exports равно null или undefined, то
    1. Вернуть undefined.
  5. Если pjson.name равно packageName, то
    1. Вернуть результат PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
  6. Иначе вернуть undefined.

PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)

Примечание: эта функция вызывается напрямую алгоритмом разрешения CommonJS.

  1. Если exports является объектом, у которого есть и ключ, начинающийся с ".", и ключ, не начинающийся с ".", выбросить ошибку Недопустимая конфигурация пакета.
  2. Если subpath равен ".", то
    1. Пусть mainExport будет undefined.
    2. Если exports является строкой или массивом либо объектом, не содержащим ключей, начинающихся с ".", то
      1. Установить mainExport в exports.
    3. Иначе, если exports является объектом и содержит свойство ".", то
      1. Установить mainExport в exports["."].
    4. Если mainExport не равен undefined, то
      1. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions).
      2. Если resolved не равен null и не равен undefined, вернуть resolved.
  3. Иначе, если exports является объектом и все его ключи начинаются с ".", то
    1. Утверждение: subpath начинается с "./".
    2. Пусть resolved будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE( subpath, exports, packageURL, false, conditions).
    3. Если resolved не равен null и не равен undefined, вернуть resolved.
  4. Выбросить ошибку Путь пакета не экспортируется.

PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)

Примечание: эта функция вызывается напрямую алгоритмом разрешения CommonJS.

  1. Утверждение: specifier начинается с "#".
  2. Если specifier в точности равен "#", то
    1. Выбросить ошибку Недопустимый спецификатор модуля.
  3. Пусть packageURL будет результатом LOOKUP_PACKAGE_SCOPE(parentURL).
  4. Если packageURL не равен null, то
    1. Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
    2. Если pjson.imports является ненулевым объектом, то
      1. Пусть resolved будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions).
      2. Если resolved не равен null и не равен undefined, вернуть resolved.
  5. Выбросить ошибку Импорт пакета не определён.

PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)

  1. Если matchKey оканчивается на "/", то
    1. Выбросить ошибку Недопустимый спецификатор модуля.
  2. Если matchKey является ключом в matchObj и не содержит "*", то
    1. Пусть target будет значением matchObj[matchKey].
    2. Вернуть результат PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
  3. Пусть expansionKeys будет списком ключей matchObj, содержащих только один "*", отсортированных функцией PATTERN_KEY_COMPARE, которая располагает их по убыванию специфичности.
  4. Для каждого ключа expansionKey в expansionKeys выполнить
    1. Пусть patternBase будет подстрокой expansionKey до первого символа "*", не включая его.
    2. Если matchKey начинается с patternBase, но не равен ему, то
      1. Пусть patternTrailer будет подстрокой expansionKey, начиная с позиции после первого символа "*".
      2. Если patternTrailer имеет нулевую длину либо matchKey оканчивается на patternTrailer и длина matchKey больше либо равна длине expansionKey, то
        1. Пусть target будет значением matchObj[expansionKey].
        2. Пусть patternMatch будет подстрокой matchKey, начиная с позиции, равной длине patternBase, и до длины matchKey минус длина patternTrailer.
        3. Вернуть результат PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions).
  5. Вернуть null.

PATTERN_KEY_COMPARE(keyA, keyB)

  1. Утверждение: keyA содержит только один "*".
  2. Утверждение: keyB содержит только один "*".
  3. Пусть baseLengthA будет индексом "*" в keyA.
  4. Пусть baseLengthB будет индексом "*" в keyB.
  5. Если baseLengthA больше baseLengthB, вернуть -1.
  6. Если baseLengthB больше baseLengthA, вернуть 1.
  7. Если длина keyA больше длины keyB, вернуть -1.
  8. Если длина keyB больше длины keyA, вернуть 1.
  9. Вернуть 0.

PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions)

  1. Если target является строкой, то
    1. Если target не начинается с "./", то
      1. Если isImports равно false, либо target начинается с "../" или "/", либо target является допустимым URL, то
        1. Выбросить ошибку Недопустимая цель пакета.
      2. Если patternMatch является строкой, то
        1. Вернуть PACKAGE_RESOLVE(target, где каждое вхождение "*" заменено на patternMatch, packageURL + "/").
      3. Вернуть PACKAGE_RESOLVE(target, packageURL + "/").
    2. Если target, разбитый по "/" или "\", содержит после первого сегмента "." любой из сегментов "", ".", ".." или "node_modules" без учёта регистра и с учётом percent-encoding, выбросить ошибку Недопустимая цель пакета.
    3. Пусть resolvedTarget будет URL-результатом разрешения конкатенации packageURL и target.
    4. Утверждение: packageURL содержится в resolvedTarget.
    5. Если patternMatch равен null, то
      1. Вернуть resolvedTarget.
    6. Если patternMatch, разбитый по "/" или "\", содержит любой из сегментов "", ".", "..", или "node_modules" без учёта регистра и с учётом percent-encoding, выбросить ошибку Недопустимый спецификатор модуля.
    7. Вернуть результат разрешения URL для resolvedTarget, где каждое вхождение "*" заменено на patternMatch.
  2. Иначе, если target является ненулевым объектом, то
    1. Если target содержит какие-либо индексные ключи свойств, как определено в ECMA-262 6.1.7 Индекс массива, выбросить ошибку Недопустимая конфигурация пакета.
    2. Для каждого свойства p объекта target в порядке вставки выполнить
      1. Если p равно "default" либо conditions содержит запись для p, то
        1. Пусть targetValue будет значением свойства p в target.
        2. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
        3. Если resolved равен undefined, продолжить цикл.
        4. Вернуть resolved.
    3. Вернуть undefined.
  3. Иначе, если target является массивом, то
    1. Если _target.length равно нулю, вернуть null.
    2. Для каждого элемента targetValue в target выполнить
      1. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions); при ошибке Недопустимая цель пакета продолжить цикл.
      2. Если resolved равен undefined, продолжить цикл.
      3. Вернуть resolved.
    3. Вернуть null последнего резервного разрешения либо выбросить его ошибку.
  4. Иначе, если target равен null, вернуть null.
  5. Иначе выбросить ошибку Недопустимая цель пакета.

ESM_FILE_FORMAT(url)

  1. Утверждение: url соответствует существующему файлу.
  2. Если url оканчивается на ".mjs", то
    1. Вернуть "module".
  3. Если url оканчивается на ".cjs", то
    1. Вернуть "commonjs".
  4. Если url оканчивается на ".json", то
    1. Вернуть "json".
  5. Если url оканчивается на ".wasm", то
    1. Вернуть "wasm".
  6. Если включён --experimental-addon-modules и url оканчивается на ".node", то
    1. Вернуть "addon".
  7. Пусть packageURL будет результатом LOOKUP_PACKAGE_SCOPE(url).
  8. Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
  9. Пусть packageType будет null.
  10. Если pjson?.type равно "module" или "commonjs", то
    1. Установить packageType в pjson.type.
  11. Если url оканчивается на ".js", то
    1. Если packageType не равно null, то
      1. Вернуть packageType.
    2. Если результат DETECT_MODULE_SYNTAX(source) равен true, то
      1. Вернуть "module".
    3. Вернуть "commonjs".
  12. Если у url нет расширения, то
    1. Если packageType равно "module" и файл по адресу url содержит заголовок типа содержимого "application/wasm" для модуля WebAssembly, то
      1. Вернуть "wasm".
    2. Если packageType не равно null, то
      1. Вернуть packageType.
    3. Если результат DETECT_MODULE_SYNTAX(source) равен true, то
      1. Вернуть "module".
    4. Вернуть "commonjs".
  13. Вернуть undefined (ошибка будет выброшена на этапе загрузки).

LOOKUP_PACKAGE_SCOPE(url)

  1. Пусть scopeURL будет url.
  2. Пока scopeURL не является корнем файловой системы,
    1. Установить scopeURL в родительский URL для scopeURL.
    2. Если scopeURL оканчивается сегментом пути "node_modules", вернуть null.
    3. Пусть pjsonURL будет результатом разрешения "package.json" внутри scopeURL.
    4. Если файл по адресу pjsonURL существует, то
      1. Вернуть scopeURL.
  3. Вернуть null.

READ_PACKAGE_JSON(packageURL)

  1. Пусть pjsonURL будет результатом разрешения "package.json" внутри packageURL.
  2. Если файл по адресу pjsonURL не существует, то
    1. Вернуть null.
  3. Если файл по адресу packageURL не удаётся разобрать как корректный JSON, то
    1. Выбросить ошибку Недопустимая конфигурация пакета.
  4. Вернуть разобранный JSON-источник файла по адресу pjsonURL.

DETECT_MODULE_SYNTAX(source)

  1. Разобрать source как ECMAScript-модуль.
  2. Если разбор успешен, то
    1. Если source содержит await верхнего уровня, статические операторы import или export, либо import.meta, вернуть true.
    2. Если source содержит лексическое объявление верхнего уровня (const, let или class) для любой из переменных-обёрток CommonJS (require, exports, module, __filename или __dirname), вернуть true.
  3. Вернуть false.

Настройка алгоритма разрешения спецификаторов ESM

модули: хуки настройки дают способ изменить алгоритм разрешения спецификаторов ESM. Пример с разрешением в стиле CommonJS для спецификаторов ESM — commonjs-extension-resolution-loader.

Комментарии