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

ECMAScript модули

v18.x.x

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

АПИ является удовлетворительным. Совместимость с NPM имеет высший приоритет и не будет нарушена кроме случаев явной необходимости.

Введение

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

Следующий пример модуля 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 использовать загрузчик модулей ECMAScript через расширение файла .mjs, поле package.json "type", или флаг --input-type. Вне этих случаев Node.js будет использовать загрузчик модулей CommonJS. Более подробную информацию смотрите в разделе Определение системы модулей.

Пакеты

Этот раздел был перемещен в Модули: Пакеты.

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

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

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

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

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

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

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

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

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

Подробнее об этих правилах разрешения пакетов, которые применяются к голым спецификаторам в разрешении модулей Node.js, смотрите документацию packages.

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

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

Это поведение соответствует тому, как import ведет себя в среде браузера, предполагая типично настроенный сервер.

URLs

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

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

file: URLs

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

1
2
import './foo.mjs?query=1'; // загружает ./foo.mjs с запросом "?query=1"
import './foo.mjs?query=2'; // загружает ./foo.mjs с запросом "?query=2"

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

data: imports

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!"' assert { type: "json" };

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

node: imports

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

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

Утверждения импорта

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

Экспериментальный

Предложение Import Assertions proposal добавляет встроенный синтаксис для операторов импорта модулей, чтобы передавать дополнительную информацию наряду со спецификатором модуля.

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


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

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

Assertion type Needed for
'json' JSON modules

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

Core modules предоставляют именованные экспорты своих публичных 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
 9
10
import fs, { readFileSync } from "node:fs";
import { syncBuiltinESMExports } из "node:module";
import { Buffer } from "node:buffer";


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


fs.readFileSync === readFileSync;

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

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

import.meta

Мета-свойство import.meta представляет собой объект, содержащий следующие свойства.

import.meta.url

  • <string> Абсолютный файл: URL модуля.

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

Это позволяет использовать такие полезные шаблоны, как относительная загрузка файлов:

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

import.meta.resolve(specifier[, parent])

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

Экспериментальная

Эта функция доступна только при включенном флаге команды --experimental-import-meta-resolve.

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

Предоставляет функцию разрешения относительно модуля, относящуюся к каждому модулю и возвращающую строку URL.

1
2
3
const dependencyAsset = await import.meta.resolve(
    'component-lib/asset.css'
);

import.meta.resolve также принимает второй аргумент, который является родительским модулем, из которого нужно выполнить resolve:

1
await import.meta.resolve('./dep', import.meta.url);

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

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

импортные утверждения

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

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

require

Модуль CommonJS require всегда рассматривает файлы, на которые он ссылается, как CommonJS.

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

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

Модули CommonJS состоят из объекта module.exports, который может быть любого типа.

При импорте модуля CommonJS он может быть надежно импортирован с помощью стандартного импорта модуля ES или соответствующего сахарного синтаксиса:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { default as cjs } from 'cjs';

// Следующий оператор импорта является "синтаксическим сахаром" (эквивалентным, но более сладким)
// для `{ default as cjsSugar }` в вышеприведенном утверждении импорта:
import cjsSugar from 'cjs';

console.log(cjs);
console.log(cjs === cjsSugar);
// Опечатки:
// <module.exports>
// true

Представление ECMAScript Module Namespace модуля CommonJS всегда является пространством имен с ключом экспорта default, указывающим на значение CommonJS module.exports.

Этот экзотический объект пространства имен модуля можно непосредственно наблюдать либо при использовании 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> }
// 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
import { name } from './cjs.cjs';
console.log(name);
// Печатает: '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' }, name: 'exported' }

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

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

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

Обнаружение именованного экспорта охватывает многие общие шаблоны экспорта, шаблоны реэкспорта, а также выходы инструментов сборки и транспилятора. Точная семантика реализована в cjs-module-lexer.

Различия между модулями ES и CommonJS

Нет require, exports или module.exports

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

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

Нет __filename или __dirname

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

Варианты использования __filename и __dirname могут быть воспроизведены через import.meta.url.

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

Addons в настоящее время не поддерживаются импортом модулей ES.

Вместо этого они могут быть загружены с помощью module.createRequire() или process.dlopen.

Нет require.resolve

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

Для полной замены require.resolve существует экспериментальный API import.meta.resolve.

В качестве альтернативы можно использовать module.createRequire().

Нет NODE_PATH

NODE_PATH не является частью разрешения спецификаторов импорта. Пожалуйста, используйте симлинки, если такое поведение желательно.

Нет require.extensions

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

Нет require.cache

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

Модули JSON

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

Экспериментальный

Файлы JSON могут быть упомянуты в import:

1
import packageConfig from "./package.json" assert { type: "json" };

Синтаксис assert { type: 'json' } является обязательным; смотрите Import Assertions.

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

Модули Wasm.

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

Экспериментальный

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

Эта интеграция соответствует ES Module Integration Proposal for WebAssembly.

Например, index.mjs, содержащий:

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

выполненные под:

1
node --experimental-wasm-modules index.mjs

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

Верхний уровень 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); // Логирует `5`
1
node b.mjs # works

Если выражение верхнего уровня await никогда не разрешится, процесс node завершится с 13 кодом состояния.

 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); // Запись в журнал `13`
});

Импорт HTTPS и HTTP

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

Экспериментальный

Импорт сетевых модулей с использованием https: и http: поддерживается под флагом --experimental-network-imports. Это позволяет импортировать модули, подобные веб-браузеру, в Node.js с некоторыми отличиями, связанными со стабильностью приложения и проблемами безопасности, которые отличаются при работе в привилегированной среде, а не в песочнице браузера.

Импорт ограничен HTTP/1

Автоматическое согласование протоколов для HTTP/2 и HTTP/3 пока не поддерживается.

HTTP ограничен адресами loopback

http: уязвим для атак типа "человек посередине" и не может использоваться для адресов за пределами IPv4-адреса 127.0.0.0/8 (127.0.0.1 - 127.255.255.255) и IPv6-адреса ::1. Поддержка http: предназначена для использования в локальных разработках.

Аутентификация никогда не отправляется на сервер назначения.

Заголовки Authorization, Cookie и Proxy-Authorization не отправляются на сервер. Избегайте включения информации о пользователе в части импортируемых URL. В настоящее время разрабатывается модель безопасности для безопасного использования этих заголовков на сервере.

CORS никогда не проверяется на сервере назначения

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

Невозможно загрузить несетевые зависимости.

Эти модули не могут получить доступ к другим модулям, которые не работают через http: или https:. Чтобы получить доступ к локальным модулям и избежать проблем с безопасностью, передавайте ссылки на локальные зависимости:

1
2
3
4
5
6
7
// file.mjs
import worker_threads from 'node:worker_threads';
import {
    configure,
    resize,
} from 'https://example.com/imagelib.mjs';
configure({ worker_threads });
1
2
3
4
5
6
7
8
// https://example.com/imagelib.mjs
let worker_threads;
export function configure(opts) {
    worker_threads = opts.worker_threads;
}
export function resize(img, size) {
    // Выполняем изменение размера в потоке worker_thread, чтобы избежать блокировки основного потока
}

Загрузка по сети не включена по умолчанию.

Пока что флаг --experimental-network-imports необходим для включения загрузки ресурсов через http: или https:. В будущем для обеспечения этого будет использоваться другой механизм. Opt-in требуется для предотвращения случайного использования переходных зависимостей с потенциально изменяемым состоянием, что может повлиять на надежность приложений Node.js.

Грузчики

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

Экспериментальный

В настоящее время этот API находится в стадии разработки и будет меняться.

Чтобы настроить разрешение модулей по умолчанию, можно опционально предоставить крючки загрузчика через аргумент --experimental-loader ./loader-name.mjs в Node.js.

При использовании хуков они применяются к каждому последующему загрузчику, точке входа и всем вызовам import. Они не применяются к вызовам require; они по-прежнему следуют правилам CommonJS.

Загрузчики следуют шаблону --require:

1
2
3
4
node \
  --experimental-loader unpkg \
  --experimental-loader http-to-https \
  --experimental-loader cache-buster

Они вызываются в следующей последовательности: cache-buster вызывает http-to-https, который вызывает unpkg.

Крючки

Хуки являются частью цепочки, даже если эта цепочка состоит только из одного пользовательского (user-provided) хука и хука по умолчанию, который присутствует всегда. Функции хуков вложены друг в друга: каждая из них всегда должна возвращать простой объект, а цепочка происходит в результате вызова каждой функцией функции next<hookName>(), которая является ссылкой на последующий хук загрузчика.

Хук, возвращающий значение, в котором отсутствует необходимое свойство, вызывает исключение. Хук, который возвращается без вызова next<hookName>() и без возврата shortCircuit: true, также вызывает исключение. Эти ошибки призваны помочь предотвратить непреднамеренный разрыв цепи.

resolve(specifier, context, nextResolve)

В настоящее время API загрузчиков перерабатывается. Этот хук может исчезнуть или его сигнатура может измениться. Не полагайтесь на API, описанный ниже.

  • specifier <string>
  • контекст <Object>
    • условия <string[]> Условия экспорта соответствующего package.json.
    • importAssertions <Object> Объект, пары ключ-значение которого представляют утверждения для импортируемого модуля
    • parentURL {string|undefined} Модуль, импортирующий данный модуль, или undefined, если это точка входа Node.js
  • nextResolve <Function> Следующий хук resolve в цепочке, или хук resolve по умолчанию Node.js после последнего пользовательского хука resolve.
  • Возвращает: <Object>
    • формат {string|null|undefined} Подсказка для крючка загрузки (может быть проигнорирована) 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'.
    • importAssertions {Object|undefined} Утверждения импорта для использования при кэшировании модуля (необязательно; если исключить, то будут использоваться входные данные)
    • shortCircuit {undefined|boolean} Сигнал о том, что этот хук намерен прервать цепочку хуков resolve. По умолчанию: false.
    • url <string> Абсолютный URL, к которому разрешается данный вход.

Цепочка хуков resolve отвечает за указание Node.js, где найти и как кэшировать заданный оператор import или выражение. По желанию она может возвращать его формат (например, 'module') в качестве подсказки для хука load. Если формат не указан, крючок load в конечном итоге отвечает за предоставление окончательного значения формата (и он может игнорировать подсказку, предоставленную resolve); если resolve предоставляет формат, требуется пользовательский крючок load, даже если только для передачи значения крючку Node.js по умолчанию load.

Утверждения типов импорта являются частью ключа кэша для сохранения загруженных модулей во внутреннем кэше модулей. Хук resolve отвечает за возврат объекта importAssertions, если модуль должен быть кэширован с утверждениями, отличными от тех, что присутствуют в исходном коде.

Свойство conditions в context - это массив условий package exports conditions, которые применяются к данному запросу разрешения. Их можно использовать для поиска условных сопоставлений в других местах или для изменения списка при вызове логики разрешения по умолчанию.

Текущие условия package exports conditions всегда находятся в массиве context.conditions, передаваемом в хук. Чтобы гарантировать дефолтное поведение разрешения спецификатора модуля Node.js при вызове defaultResolve, массив context.conditions, переданный ему, должен включать все элементы массива context.conditions, изначально переданного в хук resolve.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export async function resolve(
    specifier,
    context,
    nextResolve
) {
    const { parentURL = null } = context;

    if (Math.random() > 0.5) {
        // Некоторое условие.
        // Для некоторых или всех спецификаторов сделайте некоторую пользовательскую логику для разрешения.
        // Всегда возвращайте объект вида {url: <строка>}.
        return {
            shortCircuit: true,
            url: parentURL
                ? new URL(specifier, parentURL).href
                : new URL(specifier).href,
        };
    }

    if (Math.random() < 0.5) {
        // Еще одно условие.
        // При вызове `defaultResolve` аргументы могут быть изменены. В данном
        // случае это добавление еще одного значения для соответствия условному экспорту.
        return nextResolve(specifier, {
            контекст,
            условия: [
                ...context.conditions,
                'another-condition',
            ],
        });
    }

    // Откладываем до следующего хука в цепочке, которым будет resolve по умолчанию в Node.
    // Node.js resolve по умолчанию, если это последний указанный пользователем загрузчик.
    return nextResolve(specifier);
}

load(url, context, nextLoad)

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

В предыдущей версии этого API эта функция была разделена на 3 отдельных, ныне устаревших хука (getFormat, getSource и transformSource).

  • url <string> URL, возвращаемый цепочкой resolve.
  • context <Object>
    • conditions <string[]> Условия экспорта соответствующего package.json.
    • формат {string|null|undefined} Формат, опционально предоставляемый цепочкой хуков resolve.
    • importAssertions <Object>
  • nextLoad <Function> Следующий load хук в цепочке, или load хук по умолчанию Node.js после последнего пользовательского load хука.
  • Возвращает: <Object>
    • формат <string>
    • shortCircuit {undefined|boolean} Сигнал о том, что этот хук намерен прервать цепочку хуков resolve. По умолчанию: false.
    • source {string|ArrayBuffer|TypedArray} Источник для оценки Node.js

Хук load предоставляет возможность определить пользовательский метод определения того, как URL должен быть интерпретирован, получен и разобран. Он также отвечает за проверку утверждения об импорте.

Конечное значение format должно быть одним из следующих:

format Description Acceptable types for source returned by load
'builtin' Load a Node.js builtin module Not applicable
'commonjs' Load a Node.js CommonJS module Not applicable
'json' Load a JSON file { string, ArrayBuffer, TypedArray }
'module' Load an ES module { string, ArrayBuffer, TypedArray }
'wasm' Load a WebAssembly module { ArrayBuffer, TypedArray }

Значение source игнорируется для типа 'builtin', потому что в настоящее время невозможно заменить значение встроенного (core) модуля Node.js. Значение source игнорируется для типа 'commonjs', потому что загрузчик модуля CommonJS не предоставляет механизм для загрузчика модуля ES для переопределения возвращаемого значения модуля CommonJS. Это ограничение может быть преодолено в будущем.

Кавэат: ESM load hook и namespaced exports из модулей CommonJS несовместимы. Попытка использовать их вместе приведет к получению пустого объекта при импорте. Эта проблема может быть решена в будущем.

Все эти типы соответствуют классам, определенным в ECMAScript.

Если исходное значение текстового формата (например, 'json', 'module') не является строкой, оно преобразуется в строку с помощью util.TextDecoder.

Хук load предоставляет возможность определить пользовательский метод для получения исходного кода спецификатора модуля ES. Это позволит загрузчику потенциально избежать чтения файлов с диска. Его также можно использовать для сопоставления нераспознанного формата с поддерживаемым, например, yaml с module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export async function load(url, context, nextLoad) {
    const { format } = context;

    if (Math.random() > 0.5) {
        // Some condition
        /*
      For some or all URLs, do some custom logic for retrieving the source.
      Always return an object of the form {
        format: <string>,
        source: <string|buffer>,
      }.
    */
        return {
            format,
            shortCircuit: true,
            source: '...',
        };
    }

    // Defer to the next hook in the chain.
    return nextLoad(url);
}

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

globalPreload()

В настоящее время API загрузчиков перерабатывается. Этот хук может исчезнуть или его сигнатура может измениться. Не полагайтесь на API, описанный ниже.

В предыдущей версии этого API этот хук назывался getGlobalPreloadCode.

  • context <Object> Информация для помощи коду предварительной загрузки
    • port {MessagePort}
  • Возвращает: <string> Код для запуска перед стартом приложения

Иногда может потребоваться запустить некоторый код внутри той же глобальной области видимости, в которой запускается приложение. Этот хук позволяет вернуть строку, которая будет запущена как скрипт в небрежном режиме при запуске приложения.

Подобно тому, как работают обертки CommonJS, код запускается в неявной области видимости функции. Единственным аргументом является require-подобная функция, которая может быть использована для загрузки встроенных модулей, таких как "fs": getBuiltin(request: string).

Если коду нужны более продвинутые функции require, он должен создать свой собственный require, используя module.createRequire().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export function globalPreload(context) {
    return `\
globalThis.someInjectedProperty = 42;
console.log('Я только что установил некоторые глобальные свойства!');


const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');


const require = createRequire(cwd() + '/<preload>');
// [...]
`;
}

Для того чтобы обеспечить связь между приложением и загрузчиком, коду предварительной загрузки предоставляется еще один аргумент: port. Он доступен в качестве параметра хука загрузчика и внутри исходного текста, возвращаемого хуком. Необходимо соблюдать некоторую осторожность, чтобы правильно вызвать port.ref() и port.unref(), чтобы предотвратить нахождение процесса в состоянии, в котором он не сможет нормально закрыться.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * В этом примере контекст приложения посылает сообщение загрузчику.
 * и отправляет сообщение обратно в контекст приложения.
 */
export function globalPreload({ port }) {
    port.onmessage = (evt) => {
        port.postMessage(evt.data);
    };
    return `\
    port.postMessage('console.log("Я сходил в Loader и обратно");');
    port.onmessage = (evt) => {
      eval(evt.data);
    };
  `;
}

Примеры

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

HTTPS-загрузчик

В текущем Node.js спецификаторы, начинающиеся с https://, являются экспериментальными (см. HTTPS и HTTP imports).

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// https-loader.mjs
import { get } from 'node:https';

export function resolve(specifier, context, nextResolve) {
    const { parentURL = null } = context;

    // Обычно Node.js ошибается на спецификаторах, начинающихся с 'https://', поэтому
    // этот хук перехватывает их и преобразует в абсолютные URL-адреса, которые будут
    // передаются последующим хукам ниже.
    if (specifier.startsWith('https://')) {
        return {
            shortCircuit: true,
            url: specifier,
        };
    } else if (
        parentURL &&
        parentURL.startsWith('https://')
    ) {
        return {
            shortCircuit: true,
            url: new URL(specifier, parentURL).href,
        };
    }

    // Пусть Node.js обрабатывает все остальные спецификаторы.
    return nextResolve(specifier);
}

export function load(url, context, nextLoad) {
    // Чтобы JavaScript загружался по сети, нам нужно получить и
    // вернуть его.
    if (url.startsWith('https://')) {
        return new Promise((resolve, reject) => {
            get(url, (res) => {
                let data = '';
                res.on('data', (chunk) => (data += chunk));
                res.on('end', () =>
                    resolve({
                        // В этом примере предполагается, что весь JavaScript, предоставляемый сетью, является модулем ES.
                        // код.
                        format: 'module',
                        shortCircuit: true,
                        source: data,
                    })
                );
            }).on('error', (err) => reject(err));
        });
    }

    // Пусть Node.js обрабатывает все остальные URL.
    return nextLoad(url);
}
1
2
3
4
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';

console.log(VERSION);

С предыдущим загрузчиком выполнение node --experimental-loader ./https-loader.mjs ./main.mjs выводит текущую версию CoffeeScript для модуля по URL в main.mjs.

Transpiler loader

Исходные тексты в форматах, которые Node.js не понимает, могут быть преобразованы в JavaScript с помощью хука load. Однако прежде чем этот хук будет вызван, хук resolve должен сказать Node.js, чтобы он не выдавал ошибку при неизвестных типах файлов.

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// coffeescript-loader.mjs
import { readFile } from 'node:fs/promises';
import {
    dirname,
    extname,
    resolve as resolvePath,
} from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import CoffeeScript from 'coffeescript';

const baseURL = pathToFileURL(`${cwd()}/`).href;

// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;

export async function resolve(
    specifier,
    context,
    nextResolve
) {
    if (extensionsRegex.test(specifier)) {
        const { parentURL = baseURL } = context;

        // Node.js normally errors on unknown file extensions, so return a URL for
        // specifiers ending in the CoffeeScript file extensions.
        return {
            shortCircuit: true,
            url: new URL(specifier, parentURL).href,
        };
    }

    // Let Node.js handle all other specifiers.
    return nextResolve(specifier);
}

export async function load(url, context, nextLoad) {
    if (extensionsRegex.test(url)) {
        // Now that we patched resolve to let CoffeeScript URLs through, we need to
        // tell Node.js what format such URLs should be interpreted as. Because
        // CoffeeScript transpiles into JavaScript, it should be one of the two
        // JavaScript formats: 'commonjs' or 'module'.

        // CoffeeScript files can be either CommonJS or ES modules, so we want any
        // CoffeeScript file to be treated by Node.js the same as a .js file at the
        // same location. To determine how Node.js would interpret an arbitrary .js
        // file, search up the file system for the nearest parent package.json file
        // and read its "type" field.
        const format = await getPackageType(url);
        // When a hook returns a format of 'commonjs', `source` is ignored.
        // To handle CommonJS files, a handler needs to be registered with
        // `require.extensions` in order to process the files with the CommonJS
        // loader. Avoiding the need for a separate CommonJS handler is a future
        // enhancement planned for ES module loaders.
        if (format === 'commonjs') {
            return {
                format,
                shortCircuit: true,
            };
        }

        const { source: rawSource } = await nextLoad(url, {
            ...context,
            format,
        });
        // This hook converts CoffeeScript source code into JavaScript source code
        // for all imported CoffeeScript files.
        const transformedSource = coffeeCompile(
            rawSource.toString(),
            url
        );

        return {
            format,
            shortCircuit: true,
            source: transformedSource,
        };
    }

    // Let Node.js handle all other URLs.
    return nextLoad(url);
}

async function getPackageType(url) {
    // `url` is only a file path during the first iteration when passed the
    // resolved url from the load() hook
    // an actual file path from load() will contain a file extension as it's
    // required by the spec
    // this simple truthy check for whether `url` contains a file extension will
    // work for most projects but does not cover some edge-cases (such as
    // extensionless files or a url ending in a trailing space)
    const isFilePath = !!extname(url);
    // If it is a file path, get the directory it's in
    const dir = isFilePath
        ? dirname(fileURLToPath(url))
        : url;
    // Compose a file path to a package.json in the same directory,
    // which may or may not exist
    const packagePath = resolvePath(dir, 'package.json');
    // Try to read the possibly nonexistent package.json
    const type = await readFile(packagePath, {
        encoding: 'utf8',
    })
        .then((filestring) => JSON.parse(filestring).type)
        .catch((err) => {
            if (err?.code !== 'ENOENT') console.error(err);
        });
    // Ff package.json existed and contained a `type` field with a value, voila
    if (type) return type;
    // Otherwise, (if not at the root) continue checking the next directory up
    // If at the root, stop and return false
    return (
        dir.length > 1 &&
        getPackageType(resolvePath(dir, '..'))
    );
}
1
2
3
4
5
6
7
# main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'


import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}"
1
2
# scream.coffee
export scream = (str) -> str.toUpperCase()

С предыдущим загрузчиком выполнение node --experimental-loader ./coffeescript-loader.mjs main.coffee приводит к тому, что main.coffee превращается в JavaScript после загрузки его исходного кода с диска, но до того, как Node.js выполнит его; и так далее для любых файлов .coffee, .litcoffee или .coffee.md, на которые ссылаются операторы import любого загруженного файла.

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

Особенности

Резольвер обладает следующими свойствами:

  • Разрешение на основе FileURL, как это используется в модулях ES
  • Поддержка загрузки встроенных модулей
  • Относительное и абсолютное разрешение URL
  • Отсутствие расширений по умолчанию
  • Отсутствие папки mains
  • Поиск разрешения пакетов с голыми спецификаторами через node_modules

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

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

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

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

defaultConditions - это массив имен условного окружения, ["node", "import"].

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

  • Invalid Module Specifier: Спецификатор модуля является недопустимым URL, именем пакета или спецификатором подпути пакета.
  • Invalid Package Configuration: конфигурация package.json недопустима или содержит недопустимую конфигурацию.
  • Неверная цель пакета: Экспорт или импорт пакета определяет целевой модуль для пакета, который является недопустимым типом или строковым целевым модулем.
  • Путь пакета не экспортирован: Экспорт пакетов не определяет или не разрешает целевой подпуть в пакете для данного модуля.
  • Импорт пакета не определен: Импорт пакета не определяет спецификатор.
  • Module Not Found: Запрашиваемый пакет или модуль не существует.
  • Unsupported Directory Import: Разрешенный путь соответствует каталогу, который не является поддерживаемой целью для импорта модулей.

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

ESM_RESOLVE(specifier, parentURL)

  1. Let resolved be undefined.
  2. If specifier is a valid URL, then
    1. Set resolved to the result of parsing and reserializing specifier as a URL.
  3. Otherwise, if specifier starts with “/”, “./”, or “../”, then
    1. Set resolved to the URL resolution of specifier relative to parentURL.
  4. Otherwise, if specifier starts with “#”, then
    1. Set resolved to the result of PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
  5. Otherwise,
    1. Note: specifier is now a bare specifier.
    2. Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
  6. Let format be undefined.
  7. If resolved is a “file:” URL, then
    1. If resolved contains any percent encodings of “/” or “\” (“%2F” and “%5C” respectively), then
      1. Throw an Invalid Module Specifier error.
    2. If the file at resolved is a directory, then
      1. Throw an Unsupported Directory Import error.
    3. If the file at resolved does not exist, then
      1. Throw a Module Not Found error.
    4. Set resolved to the real path of resolved, maintaining the same URL querystring and fragment components.
    5. Set format to the result of ESM_FILE_FORMAT(resolved).
  8. Otherwise,
    1. Set format the module format of the content type associated with the URL resolved.
  9. Load resolved as module format, format.

PACKAGE_RESOLVE(packageSpecifier, parentURL)

  1. Let packageName be undefined.
  2. If packageSpecifier is an empty string, then
    1. Throw an Invalid Module Specifier error.
  3. If packageSpecifier is a Node.js builtin module name, then
    1. Return the string “node:” concatenated with packageSpecifier.
  4. If packageSpecifier does not start with “@”, then
    1. Set packageName to the substring of packageSpecifier until the first “/” separator or the end of the string.
  5. Otherwise,
    1. If packageSpecifier does not contain a “/” separator, then
      1. Throw an Invalid Module Specifier error.
    2. Set packageName to the substring of packageSpecifier until the second “/” separator or the end of the string.
  6. If packageName starts with “.” or contains “\” or “%”, then
    1. Throw an Invalid Module Specifier error.
  7. Let packageSubpath be “.” concatenated with the substring of packageSpecifier from the position at the length of packageName.
  8. If packageSubpath ends in “/”, then
    1. Throw an Invalid Module Specifier error.
  9. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
  10. If selfUrl is not undefined, return selfUrl.
  11. While parentURL is not the file system root,
    1. Let packageURL be the URL resolution of “node_modules/” concatenated with packageSpecifier, relative to parentURL.
    2. Set parentURL to the parent folder URL of parentURL.
    3. If the folder at packageURL does not exist, then
      1. Continue the next loop iteration.
    4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
    5. If pjson is not null and pjson._exports_ is not null or undefined, then
      1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
    6. Otherwise, if packageSubpath is equal to “.”, then
      1. If pjson.main is a string, then
        1. Return the URL resolution of main in packageURL.
    7. Otherwise,
      1. Return the URL resolution of packageSubpath in packageURL.
  12. Throw a Module Not Found error.

PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)

  1. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
  2. If packageURL is null, then
    1. Return undefined.
  3. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
  4. If pjson is null or if pjson._exports_ is null or undefined, then
    1. Return undefined.
  5. If pjson.name is equal to packageName, then
    1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
  6. Otherwise, return undefined.

PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)

  1. If exports is an Object with both a key starting with “.” and a key not starting with “.”, throw an Invalid Package Configuration error.
  2. If subpath is equal to “.”, then
    1. Let mainExport be undefined.
    2. If exports is a String or Array, or an Object containing no keys starting with “.”, then
      1. Set mainExport to exports.
    3. Otherwise if exports is an Object containing a “.” property, then
      1. Set mainExport to exports[“.”].
    4. If mainExport is not undefined, then
      1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions).
      2. If resolved is not null or undefined, return resolved.
  3. Otherwise, if exports is an Object and all keys of exports start with “.”, then
    1. Let matchKey be the string “./” concatenated with subpath.
    2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
    3. If resolved is not null or undefined, return resolved.
  4. Throw a Package Path Not Exported error.

PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)

  1. Assert: specifier begins with “#”.
  2. If specifier is exactly equal to “#” or starts with “#/”, then
    1. Throw an Invalid Module Specifier error.
  3. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
  4. If packageURL is not null, then
    1. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
    2. If pjson.imports is a non-null Object, then
      1. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions).
      2. If resolved is not null or undefined, return resolved.
  5. Throw a Package Import Not Defined error.

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

  1. If matchKey is a key of matchObj and does not contain “*”, then
    1. Let target be the value of matchObj[matchKey].
    2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
  2. Let expansionKeys be the list of keys of matchObj containing only a single “*”, sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
  3. For each key expansionKey in expansionKeys, do
    1. Let patternBase be the substring of expansionKey up to but excluding the first “*” character.
    2. If matchKey starts with but is not equal to patternBase, then
      1. Let patternTrailer be the substring of expansionKey from the index after the first “*” character.
      2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then
        1. Let target be the value of matchObj[expansionKey].
        2. Let patternMatch be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer.
        3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions).
  4. Return null.

PATTERN_KEY_COMPARE(keyA, keyB)

  1. Assert: keyA ends with “/” or contains only a single “*”.
  2. Assert: keyB ends with “/” or contains only a single “*”.
  3. Let baseLengthA be the index of “*” in keyA plus one, if keyA contains “*”, or the length of keyA otherwise.
  4. Let baseLengthB be the index of “*” in keyB plus one, if keyB contains “*”, or the length of keyB otherwise.
  5. If baseLengthA is greater than baseLengthB, return -1.
  6. If baseLengthB is greater than baseLengthA, return 1.
  7. If keyA does not contain “*”, return 1.
  8. If keyB does not contain “*”, return -1.
  9. If the length of keyA is greater than the length of keyB, return -1.
  10. If the length of keyB is greater than the length of keyA, return 1.
  11. Return 0.

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

  1. If target is a String, then
    1. If target does not start with “./”, then
      1. If isImports is false, or if target starts with “../” or “/”, or if target is a valid URL, then
        1. Throw an Invalid Package Target error.
      2. If patternMatch is a String, then
        1. Return PACKAGE_RESOLVE(target with every instance of “*” replaced by patternMatch, packageURL + “/”).
      3. Return PACKAGE_RESOLVE(target, packageURL + “/”).
    2. If target split on “/” or “\” contains any "“, ”.“, ”..“, or ”node_modules" segments after the first “.” segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
    3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
    4. Assert: resolvedTarget is contained in packageURL.
    5. If patternMatch is null, then
      1. Return resolvedTarget.
    6. If patternMatch split on “/” or “\” contains any "“, ”.“, ”..“, or ”node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error.
    7. Return the URL resolution of resolvedTarget with every instance of “*” replaced with patternMatch.
  2. Otherwise, if target is a non-null Object, then
    1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
    2. For each property p of target, in object insertion order as,
      1. If p equals “default” or conditions contains an entry for p, then
        1. Let targetValue be the value of the p property in target.
        2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
        3. If resolved is equal to undefined, continue the loop.
        4. Return resolved.
    3. Return undefined.
  3. Otherwise, if target is an Array, then
    1. If _target.length is zero, return null.
    2. For each item targetValue in target, do
      1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error.
      2. If resolved is undefined, continue the loop.
      3. Return resolved.
    3. Return or throw the last fallback resolution null return or error.
  4. Otherwise, if target is null, return null.
  5. Otherwise throw an Invalid Package Target error.

ESM_FILE_FORMAT(url)

  1. Assert: url corresponds to an existing file.
  2. If url ends in “.mjs”, then
    1. Return “module”.
  3. If url ends in “.cjs”, then
    1. Return “commonjs”.
  4. If url ends in “.json”, then
    1. Return “json”.
  5. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url).
  6. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
  7. If pjson?.type exists and is “module”, then
    1. If url ends in “.js”, then
      1. Return “module”.
    2. Throw an Unsupported File Extension error.
  8. Otherwise,
    1. Throw an Unsupported File Extension error.

LOOKUP_PACKAGE_SCOPE(url)

  1. Let scopeURL be url.
  2. While scopeURL is not the file system root,
    1. Set scopeURL to the parent URL of scopeURL.
    2. If scopeURL ends in a “node_modules” path segment, return null.
    3. Let pjsonURL be the resolution of “package.json” within scopeURL.
    4. if the file at pjsonURL exists, then
      1. Return scopeURL.
  3. Return null.

READ_PACKAGE_JSON(packageURL)

  1. Let pjsonURL be the resolution of “package.json” within packageURL.
  2. If the file at pjsonURL does not exist, then
    1. Return null.
  3. If the file at packageURL does not parse as valid JSON, then
    1. Throw an Invalid Package Configuration error.
  4. Return the parsed JSON source of the file at pjsonURL.

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

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

Комментарии