Модули: ECMAScript-модули
Введение
ECMAScript-модули являются официальным стандартным форматом для упаковки JavaScript-кода для повторного использования. Модули определяются с помощью различных import и export операторов.
Следующий пример ES-модуля экспортирует функцию:
// addTwo.mjs
function addTwo(num) {
return num + 2;
}
export { addTwo }; Следующий пример ES-модуля импортирует функцию из addTwo.mjs:
// app.mjs
import { addTwo } from './addTwo.mjs';
// Prints: 6
console.log(addTwo(4)); Node.js полностью поддерживает ECMAScript-модули в их текущей спецификации и обеспечивает межсовместимость между ними и его исходным форматом модулей, CommonJS.
Включение
Node.js по умолчанию обрабатывает JavaScript-код как CommonJS-модули. Авторы могут указать Node.js обработать JavaScript-код как ECMAScript-модули через .mjs расширение файла, package.json "type" поле или --input-type флаг. Более подробную информацию см. в разделе Модули: Пакеты.
Пакеты
Этот раздел перемещён в Модули: Пакеты.
import Спецификаторы
Терминология
Спецификатор оператора import — это строка после ключевого слова from, например 'path' в import { sep } from '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".
Подробную информацию об этих правилах разрешения пакетов, которые применяются к голым спецификаторам в алгоритме разрешения модулей Node.js, см. в документации по пакетам.
Обязательные расширения файлов
Расширение файла должно быть указано при использовании ключевого слова import для разрешения относительных или абсолютных спецификаторов. Индексы каталогов (например, './startup/index.js') также должны быть полностью указаны.
Это поведение соответствует тому, как import ведет себя в браузерной среде при условии типично настроенного сервера.
URL
ES-модули разрешаются и кэшируются как URL. Это означает, что файлы, содержащие специальные символы, такие как # и ? должны быть закодированы.
file:, node:, и data: URL-схемы поддерживаются. Спецификатор, такой как 'https://example.com/app.js' не поддерживается в Node.js напрямую, если не используется настраиваемый HTTPS-загрузчик.
file: URL
Модули загружаются несколько раз, если спецификатор import используемый для их разрешения, имеет разные параметры запроса или фрагмент.
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: URL поддерживаются для импорта со следующими типами MIME:
-
text/javascriptдля ES-модулей -
application/jsonдля JSON -
application/wasmдля Wasm
data: URL разрешают только голые спецификаторы для встроенных модулей и абсолютные спецификаторы. Разрешение относительных спецификаторов не работает, так как data: не является специальной схемой. Например, попытка загрузить ./foo из data:text/javascript,import "./foo"; не удается, так как для data: URL нет понятия относительного разрешения. Пример использования data: URL:
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"';
node: Импорты
node: URL поддерживаются как альтернативный способ загрузки встроенных модулей Node.js. Эта URL-схема позволяет ссылаться на встроенные модули с помощью допустимых абсолютных строковых URL.
import fs from 'node:fs/promises';
Встроенные модули
Ядерные модули предоставляют именованные экспорты своей публичной API. Также предоставляется экспорт по умолчанию, который является значением CommonJS-экспортов. Экспорт по умолчанию может использоваться, среди прочего, для изменения именованных экспортов. Именованные экспорты встроенных модулей обновляются только вызовом module.syncBuiltinESMExports().
import EventEmitter from 'events'; const e = new EventEmitter();
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
}); import fs, { readFileSync } from 'fs';
import { syncBuiltinESMExports } from 'module';
import { Buffer } from 'buffer';
fs.readFileSync = () => Buffer.from('Hello, ESM');
syncBuiltinESMExports();
fs.readFileSync === readFileSync;
import() выражения
Динамические import() поддерживаются как в CommonJS, так и в ES-модулях. В CommonJS-модулях он может использоваться для загрузки ES-модулей.
import.meta
Свойство мета import.meta является Object, которое содержит следующие свойства.
import.meta.url
-
<строка> Абсолютный
file:URL модуля.
Определяется точно так же, как в браузерах, предоставляя URL текущего файла модуля.
Это позволяет использовать полезные шаблоны, такие как загрузка относительных файлов:
import { readFileSync } from 'fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
import.meta.resolve(specifier[, parent])
Эта функция доступна только при включенном флаге командной строки --experimental-import-meta-resolve.
-
specifier<строка> Спецификатор модуля для разрешения относительноparent. -
parent<строка> | <URL> Абсолютный URL родительского модуля, относительно которого разрешить. Если не указан, используется значениеimport.meta.urlпо умолчанию. - Возвращает: <Promise>
Предоставляет функцию разрешения, относящуюся к модулю, и ограниченную каждым модулем, возвращающую строку URL.
const dependencyAsset = await import.meta.resolve('component-lib/asset.css'); import.meta.resolve также принимает второй аргумент, который является родительским модулем, относительно которого следует разрешить:
await import.meta.resolve('./dep', import.meta.url); Эта функция асинхронна, поскольку разрешитель ES-модулей в Node.js может быть асинхронным.
Взаимодействие с CommonJS
import операторы
Оператор import может ссылаться на модуль ES или CommonJS. import операторы разрешены только в модулях ES, но динамические import() выражения поддерживаются в CommonJS для загрузки модулей ES.
При импорте модулей CommonJS объект module.exports предоставляется в качестве значения по умолчанию. Имена экспортов могут быть доступны, предоставляемые статическим анализом для лучшей совместимости экосистемы.
require
Модуль CommonJS require всегда обрабатывает файлы, на которые он ссылается, как CommonJS.
Использование require для загрузки модуля ES не поддерживается, так как модули ES выполняют асинхронную обработку. Вместо этого используйте import() для загрузки модуля ES из модуля CommonJS.
Пространства имён CommonJS
Модули CommonJS состоят из объекта module.exports, который может быть любого типа.
При импорте модуля CommonJS его можно надёжно импортировать, используя импорт по умолчанию модуля ES или соответствующий синтаксический сахар:
import { default as cjs } from 'cjs';
// The following import statement is "syntax sugar" (equivalent but sweeter)
// for `{ default as cjsSugar }` in the above import statement:
import cjsSugar from 'cjs';
console.log(cjs);
console.log(cjs === cjsSugar);
// Prints:
// <module.exports>
// true Представление пространства имён модуля ECMAScript для модуля CommonJS всегда является пространством имён с ключом экспорта default, указывающим на значение CommonJS module.exports.
Этот экзотический объект пространства имён модуля можно напрямую наблюдать, используя import * as m from 'cjs' или динамический импорт:
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, написанный:
// cjs.cjs exports.name = 'exported';
Предыдущий модуль поддерживает именованные импорты в модулях ES:
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' }, name: 'exported' } Как видно из последнего примера вывода экзотического объекта пространства имён модуля, экспорт name копируется из объекта module.exports и устанавливается непосредственно в пространстве имён модуля ES при импорте модуля.
Обновления привязки в реальном времени или новые экспорты, добавленные к module.exports , для этих именованных экспортов не обнаруживаются.
Обнаружение именованных экспортов основано на распространённых шаблонах синтаксиса, но не всегда правильно обнаруживает именованные экспорты. В таких случаях использование формы импорта по умолчанию, описанной выше, может быть лучшим вариантом.
Обнаружение именованных экспортов охватывает многие распространённые шаблоны экспорта, шаблоны переэкспорта и результаты работы инструментов сборки и транспилятора. См. cjs-module-lexer для точных реализованных семантик.
Различия между модулями ES и CommonJS
Отсутствие require, exports или module.exports
В большинстве случаев, модуль ES import может быть использован для загрузки модулей CommonJS.
Если необходимо, функция require может быть создана внутри модуля ES с помощью module.createRequire().
Отсутствие __filename или __dirname
Эти переменные CommonJS недоступны в модулях ES.
__filename и __dirname случаи использования могут быть воспроизведены с помощью import.meta.url.
Отсутствие загрузки модулей JSON
Импорт JSON модулей всё ещё экспериментален и поддерживается только со флагом --experimental-json-modules.
Локальные файлы JSON могут быть загружены относительно import.meta.url с помощью fs напрямую:
import { readFile } from 'fs/promises';
const json = JSON.parse(await readFile(new URL('./dat.json', import.meta.url))); В качестве альтернативы можно использовать module.createRequire().
Отсутствие загрузки нативных модулей
Нативные модули в настоящее время не поддерживаются с импортом модулей ES.
Их можно загрузить с помощью module.createRequire() или process.dlopen.
Отсутствие require.resolve
Обработка относительного разрешения может быть выполнена с помощью new URL('./local', import.meta.url).
Для полного require.resolve замещения есть экспериментальный флаг import.meta.resolve API.
В качестве альтернативы можно использовать module.createRequire().
Отсутствие NODE_PATH
NODE_PATH не участвует в разрешении import спецификаторов. Для достижения такого поведения используйте символические ссылки.
Отсутствие require.extensions
require.extensions не используется import. Ожидается, что в будущем обработчики загрузчиков смогут предоставить эту работу.
Отсутствие require.cache
require.cache не используется import, так как у загрузчика модулей ES есть собственный кэш.
Модули JSON
В настоящее время импорт модулей JSON поддерживается только в режиме commonjs и загружается с помощью загрузчика CJS. Спецификация WHATWG модулей JSON всё ещё стандартизируется и экспериментально поддерживается путём включения дополнительного флага --experimental-json-modules при запуске Node.js.
При включении флага --experimental-json-modules, как режим commonjs, так и режим module используют новый экспериментальный загрузчик JSON. Импортированный JSON экспонирует только значение default. Поддержка именованных экспортов отсутствует. В кэше CommonJS создаётся запись для избежания дублирования. Один и тот же объект возвращается в CommonJS, если модуль JSON уже был импортирован из того же пути.
Предположим index.mjs с
import packageConfig from './package.json';
Флаг --experimental-json-modules необходим для работы модуля.
node index.mjs # fails node --experimental-json-modules index.mjs # works
Модули Wasm
Импорт модулей Web Assembly поддерживается с флагом --experimental-wasm-modules, позволяя импортировать любые файлы .wasm как обычные модули, а также поддерживать их импортируемые модули.
Эта интеграция соответствует предложению по интеграции модулей ES для Web Assembly.
Например, index.mjs содержащий:
import * as M from './module.wasm'; console.log(M);
выполняется под:
node --experimental-wasm-modules index.mjs
предоставит интерфейс экспорта для инициализации module.wasm.
Оператор await в верхнем уровне
Ключевое слово await может использоваться в верхнем уровне (вне асинхронных функций) внутри модулей в соответствии с предложением ECMAScript Top-Level await.
Предположим a.mjs с
export const five = await Promise.resolve(5);
И b.mjs с
import { five } from './a.mjs';
console.log(five); // Logs `5` node b.mjs # works
Загрузчики
Примечание: Данный API в настоящее время перерабатывается и может быть изменён.
Для настройки стандартной разрешения модулей, плагины загрузчиков могут быть необязательно предоставлены через аргумент --experimental-loader ./loader-name.mjs в Node.js.
При использовании плагинов, они применяются только к загрузке ES модулей, а не к загрузке модулей CommonJS.
Плагины
resolve(specifier, context, defaultResolve)
Примечание: API загрузчиков перерабатывается. Данный плагин может быть удалён или изменён в структуре. Не полагайтесь на описанный ниже API.
-
specifier<строка> -
context<объект>-
conditions<массив строк> -
parentURL<строка> | <неопределено>
-
-
defaultResolve<функция> Стандартный разрешитель Node.js. - Возвращает: <объект>
-
format<строка> | <null> | <неопределено>'builtin' | 'commonjs' | 'json' | 'module' | 'wasm' -
url<строка> Абсолютный URL целевого импорта (например,file://…)
-
Плагин resolve возвращает разрешённый URL файла для заданного спецификатора модуля и родительского URL, и, необязательно, его формат (например, 'module') в качестве подсказки для плагина load. Если формат указан, то плагин load в конечном итоге отвечает за предоставление окончательного значения format (и может игнорировать подсказку от плагина resolve); если resolve предоставляет format, необходим пользовательский плагин load даже если только для передачи значения в стандартный плагин Node.js load.
Спецификатор модуля — это строка в операторе import или выражении import(), а родительский URL — URL модуля, который импортировал этот модуль, или undefined, если это точка входа в приложение.
Свойство conditions в context — массив условий экспорта пакетов для условия экспорта пакетов, которые применяются к этому запросу на разрешение. Они могут использоваться для поиска условных сопоставлений в другом месте или для изменения списка при вызове стандартной логики разрешения.
Текущие условия экспорта пакетов всегда находятся в массиве context.conditions, передаваемом в плагин. Для гарантии стандартного поведения разрешения спецификатора модуля Node.js при вызове defaultResolve, массив context.conditions обязательно должен включать все элементы массива context.conditions изначально переданного в плагин resolve.
/**
* @param {string} specifier
* @param {{
* conditions: !Array<string>,
* parentURL: !(string | undefined),
* }} context
* @param {Function} defaultResolve
* @returns {Promise<{ url: string }>}
*/
export async function resolve(specifier, context, defaultResolve) {
const { parentURL = null } = context;
if (Math.random() > 0.5) { // Some condition.
// For some or all specifiers, do some custom logic for resolving.
// Always return an object of the form {url: <string>}.
return {
url: parentURL ?
new URL(specifier, parentURL).href :
new URL(specifier).href,
};
}
if (Math.random() < 0.5) { // Another condition.
// When calling `defaultResolve`, the arguments can be modified. In this
// case it's adding another value for matching conditional exports.
return defaultResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
// Defer to Node.js for all other specifiers.
return defaultResolve(specifier, context, defaultResolve);
}
load(url, context, defaultLoad)
Примечание: API загрузчиков перерабатывается. Данный плагин может быть удалён или изменён в структуре. Не полагайтесь на описанный ниже API.
Примечание: В предыдущей версии API данный плагин был разделён на 3 отдельных, теперь устаревших, плагина (
getFormat,getSource, иtransformSource).
-
url<строка> -
context<объект>-
format<строка> | <null> | <неопределено> Формат, необязательно предоставленный плагиномresolve.
-
-
defaultLoad<функция> - Возвращает: <объект>
-
format<строка> -
source<строка> | <ArrayBuffer> | <TypedArray>
-
Плагин load предоставляет способ определения пользовательского метода определения, как интерпретировать, получать и анализировать URL.
Конечное значение format должно быть одним из следующих:
format |
Описание | Допустимые типы для source возвращаемые load
|
|---|---|---|
'builtin' |
Загрузка встроенного модуля Node.js | Не применимо |
'commonjs' |
Загрузка модуля Node.js CommonJS | Не применимо |
'json' |
Загрузка JSON файла | { string, ArrayBuffer, TypedArray } |
'module' |
Загрузка ES модуля | { string, ArrayBuffer, TypedArray } |
'wasm' |
Загрузка модуля WebAssembly | { ArrayBuffer, TypedArray } |
Значение source игнорируется для типа 'builtin', потому что в настоящее время невозможно заменить значение встроенного (ядерного) модуля Node.js. Значение source игнорируется для типа 'commonjs', потому что загрузчик модулей CommonJS не предоставляет механизма для переопределения значения возврата модуля CommonJS загрузчиком ES модулей. Это ограничение может быть устранено в будущем.
Предостережение: Плагин ESM
loadи пространства имён из модулей CommonJS несовместимы. Попытка их совместного использования приведёт к пустому объекту при импорте. Это может быть исправлено в будущем.
Примечание: Все эти типы соответствуют классам, определённым в ECMAScript.
- Указанный объект
ArrayBuffer— этоSharedArrayBuffer. - Указанный объект
TypedArray— этоUint8Array.
Если исходное значение текстового формата (то есть 'json', 'module') не является строкой, оно преобразуется в строку с помощью util.TextDecoder.
Плагин load предоставляет способ определения пользовательского метода получения исходного кода спецификатора ES модуля. Это позволило бы загрузчику потенциально избегать чтения файлов с диска. Также это можно использовать для сопоставления нераспознанного формата с поддерживаемым, например, yaml с module.
/**
* @param {string} url
* @param {{
format: string,
}} context If resolve settled with a `format`, that value is included here.
* @param {Function} defaultLoad
* @returns {Promise<{
format: !string,
source: !(string | ArrayBuffer | SharedArrayBuffer | Uint8Array),
}>}
*/
export async function load(url, context, defaultLoad) {
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,
source: '...',
};
}
// Defer to Node.js for all other URLs.
return defaultLoad(url, context, defaultLoad);
} В более сложной ситуации это также можно использовать для преобразования неподдерживаемого исходника в поддерживаемый (см. Примеры ниже).
globalPreload()
Примечание: API загрузчиков перерабатывается. Данный плагин может быть удалён или изменён в структуре. Не полагайтесь на описанный ниже API.
Примечание: В предыдущей версии API этот плагин назывался
getGlobalPreloadCode.
- Возвращает: <строка>
Иногда может потребоваться выполнить код в том же глобальном пространстве имён, в котором выполняется приложение. Этот плагин позволяет вернуть строку, которая выполняется как скрипт режима sloppy при запуске.
Подобно тому, как работают оболочки CommonJS, код выполняется в неявной области видимости функции. Единственный аргумент — функция типа require, которая может использоваться для загрузки встроенных модулей, например, "fs": getBuiltin(request: string).
Если коду требуются более продвинутые require функции, ему необходимо создать собственный require с помощью module.createRequire().
/**
* @returns {string} Code to run before application startup
*/
export function globalPreload() {
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');
const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');
const require = createRequire(cwd() + '/<preload>');
// [...]
`;
} Примеры
Различные хуки загрузчика могут быть объединены для достижения широкого спектра кастомизаций поведения загрузки и оценки кода Node.js.
Загрузчик HTTPS
В текущем Node.js спецификаторы, начинающиеся с https:// , не поддерживаются. Загрузчик ниже регистрирует хуки для обеспечения ограниченной поддержки таких спецификаторов. Хотя это может показаться значительным улучшением основной функциональности Node.js, существуют существенные недостатки при фактическом использовании этого загрузчика: производительность значительно медленнее, чем загрузка файлов с диска, отсутствует кэширование и безопасность.
// https-loader.mjs
import { get } from 'https';
export function resolve(specifier, context, defaultResolve) {
const { parentURL = null } = context;
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
if (specifier.startsWith('https://')) {
return {
url: specifier
};
} else if (parentURL && parentURL.startsWith('https://')) {
return {
url: new URL(specifier, parentURL).href
};
}
// Let Node.js handle all other specifiers.
return defaultResolve(specifier, context, defaultResolve);
}
export function load(url, context, defaultLoad) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
// This example assumes all network-provided JavaScript is ES module
// code.
format: 'module',
source: data,
}));
}).on('error', (err) => reject(err));
});
}
// Let Node.js handle all other URLs.
return defaultLoad(url, context, defaultLoad);
} // 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.
Загрузчик транспилятора
Источники в форматах, которые Node.js не понимает, могут быть преобразованы в JavaScript с использованием хука load. Однако, перед вызовом этого хука, хук resolve должен сообщить Node.js, что не следует выбрасывать ошибку при обнаружении неизвестных типов файлов.
Это менее эффективно, чем транспиляция файлов исходного кода перед запуском Node.js; загрузчик транспилятора должен использоваться только в целях разработки и тестирования.
// 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, defaultResolve) {
const { parentURL = baseURL } = context;
// Node.js normally errors on unknown file extensions, so return a URL for
// specifiers ending in the CoffeeScript file extensions.
if (extensionsRegex.test(specifier)) {
return {
url: new URL(specifier, parentURL).href
};
}
// Let Node.js handle all other specifiers.
return defaultResolve(specifier, context, defaultResolve);
}
export async function load(url, context, defaultLoad) {
// 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'.
if (extensionsRegex.test(url)) {
// 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 be 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 };
}
const { source: rawSource } = await defaultLoad(url, { format });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = CoffeeScript.compile(rawSource.toString(), {
bare: true,
filename: url,
});
return {
format,
source: transformedSource,
};
}
// Let Node.js handle all other URLs.
return defaultLoad(url, context, defaultLoad);
}
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
// extension-less 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, '..'));
} # main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'
import { version } from 'process'
console.log "Brought to you by Node.js version #{version}" # 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-адресов
- Отсутствие расширений по умолчанию
- Отсутствие главных папок
- Поиск разрешения пакета с голым спецификатором через node_modules
Алгоритм решателя
Алгоритм загрузки спецификатора ES-модуля задается с помощью метода ESM_RESOLVE ниже. Он возвращает разрешенный URL для спецификатора модуля относительно parentURL.
Алгоритм определения формата модуля разрешенного URL-адреса предоставляется ESM_FORMAT, который возвращает уникальный формат модуля для любого файла. Формат "module" возвращается для модуля ECMAScript, а формат "commonjs" используется для обозначения загрузки через устаревший загрузчик CommonJS. Дополнительные форматы, такие как "addon", могут быть расширены в будущих обновлениях.
В следующих алгоритмах все ошибки подпрограмм распространяются как ошибки этих главных процедур, если не указано иное.
defaultConditions — это массив имен условной среды, ["node", "import"].
Решатель может вызывать следующие ошибки:
- Неверный спецификатор модуля: Спецификатор модуля является недопустимым URL, именем пакета или спецификатором подпути пакета.
- Неверная конфигурация пакета: Конфигурация package.json неверна или содержит неверную конфигурацию.
- Неверная цель пакета: Экспорт или импорт пакета определяют целевой модуль для пакета, который является неверным типом или строковым целевым объектом.
- Путь пакета не экспортируется: Экспорт пакета не определяет или не разрешает целевой подпуть в пакете для данного модуля.
- Импорт пакета не определен: Импорт пакета не определяет спецификатор.
- Модуль не найден: Запрошенный пакет или модуль не существует.
- Неподдерживаемый импорт каталога: Разрешенный путь соответствует каталогу, который не является поддерживаемой целью для импорта модулей.
Спецификация алгоритма решателя
ESM_RESOLVE(specifier, parentURL)
- Пусть resolved будет undefined.
- Если specifier является допустимым URL, то
- Установите resolved в результат разбора и повторной сериализации specifier как URL.
- В противном случае, если specifier начинается с "/", "./" или "../", то
- Установите resolved в разрешение URL specifier относительно parentURL.
- В противном случае, если specifier начинается с "#", то
- Установите resolved в деструктурированное значение результата PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
- В противном случае,
- Примечание: specifier теперь является голым спецификатором.
- Установите resolved в результат PACKAGE_RESOLVE(specifier, parentURL).
- Если resolved содержит какие-либо процентные кодировки "/" или "\" ("%2f" и "%5C" соответственно), то
- Выбросьте ошибку Неверный спецификатор модуля.
- Если файл по адресу resolved является каталогом, то
- Выбросьте ошибку Неподдерживаемый импорт каталога.
- Если файл по адресу resolved не существует, то
- Выбросьте ошибку Модуль не найден.
- Установите resolved в реальный путь resolved.
- Пусть format будет результатом ESM_FORMAT(resolved).
- Загрузите resolved как формат модуля, format.
- Верните resolved.
PACKAGE_RESOLVE(packageSpecifier, parentURL)
- Пусть packageName будет undefined.
- Если packageSpecifier является пустой строкой, то
- Выбросьте ошибку Неверный спецификатор модуля.
- Если packageSpecifier не начинается с "@", то
- Установите packageName в подстроку packageSpecifier до первого разделителя "/" или конца строки.
- В противном случае,
- Если packageSpecifier не содержит разделителя "/", то
- Выбросьте ошибку Неверный спецификатор модуля.
- Установите packageName в подстроку packageSpecifier до второго разделителя "/" или конца строки.
- Если packageName начинается с "." или содержит "\" или "%", то
- Выбросьте ошибку Неверный спецификатор модуля.
- Пусть packageSubpath будет ".", объединенной с подстрокой packageSpecifier с позиции по длине packageName.
- Пусть selfUrl будет результатом PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
- Если selfUrl не undefined, верните selfUrl.
- Если packageSubpath равно "." и packageName является встроенным модулем Node.js, то
- Верните строку "node:", объединенную с packageSpecifier.
- Пока parentURL не является корнем файловой системы,
- Пусть packageURL будет разрешением URL "node_modules/", объединенного с packageSpecifier, относительно parentURL.
- Установите parentURL в родительский URL каталога parentURL.
- Если папка по адресу packageURL не существует, то
- Установите parentURL в родительский URL-путь parentURL.
- Продолжите следующую итерацию цикла.
- Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
- Если pjson не null и pjson.exports не null или undefined, то
- Пусть exports будет pjson.exports.
- Верните деструктурированное значение resolved результата PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- В противном случае, если packageSubpath равно ".", то
- Верните результат, применяя устаревший решатель CommonJS LOAD_AS_DIRECTORY к packageURL, выбросив ошибку Модуль не найден при отсутствии разрешения.
- В противном случае,
- Верните разрешение URL packageSubpath в packageURL.
- Выбросьте ошибку Модуль не найден.
PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)
- Пусть packageURL будет результатом READ_PACKAGE_SCOPE(parentURL).
- Если packageURL равно null, то
- Верните undefined.
- Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
- Если pjson равно null или если pjson.exports равно null или undefined, то
- Верните undefined.
- Если pjson.name равно packageName, то
- Верните деструктурированное значение resolved результата PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- В противном случае, верните undefined.
PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
- Если exports является объектом, имеющим как ключ, начинающийся с ".", так и ключ, не начинающийся с ".", выбросьте ошибку Неверная конфигурация пакета.
- Если subpath равно ".", то
- Пусть mainExport будет undefined.
- Если exports является строкой или массивом, или объектом, не содержащим ключей, начинающихся с ".", то
- Установите mainExport в exports.
- В противном случае, если exports является объектом, содержащим свойство ".", то
- Установите mainExport в exports["."].
- Если mainExport не undefined, то
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, mainExport, "", false, false, conditions).
- Если resolved не null или undefined, то
- Верните resolved.
- В противном случае, если exports является объектом и все ключи exports начинаются с ".", то
- Пусть matchKey будет строкой "./", объединенной с subpath.
- Пусть resolvedMatch будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
- Если resolvedMatch.resolve не null или undefined, то
- Верните resolvedMatch.
- Выбросьте ошибку Путь пакета не экспортируется.
PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
- Утверждение: specifier начинается с "#".
- Если specifier точно равен "#" или начинается с "#/", то
- Выбросьте ошибку Неверный спецификатор модуля.
- Пусть packageURL будет результатом READ_PACKAGE_SCOPE(parentURL).
- Если packageURL не null, то
- Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
- Если pjson.imports является объектом, отличным от null, то
- Пусть resolvedMatch будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE(specifier, pjson.imports, packageURL, true, conditions).
- Если resolvedMatch.resolve не null или undefined, то
- Верните resolvedMatch.
- Выбросьте ошибку Импорт пакета не определен.
PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)
- Если matchKey является ключом matchObj и не оканчивается на "/" и не содержит "*", то
- Пусть target будет значением matchObj[matchKey].
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, "", false, isImports, conditions).
- Вернуть объект { resolved, exact: true }.
- Пусть expansionKeys будет списком ключей matchObj, оканчивающихся на "/" или содержащих только один "*", отсортированным функцией сортировки PATTERN_KEY_COMPARE, которая упорядочивает их по убыванию специфичности.
- Для каждого ключа expansionKey в expansionKeys выполнить
- Пусть patternBase будет null.
- Если expansionKey содержит "*", установить patternBase на подстроку expansionKey до, но не включая, первый символ "*".
- Если patternBase не null и matchKey начинается с patternBase, но не равно ему, то
- Если matchKey оканчивается на "/", выбросить ошибку Invalid Module Specifier.
- Пусть patternTrailer будет подстрокой expansionKey с индекса, следующего за первым символом "*".
- Если patternTrailer имеет нулевую длину или если matchKey оканчивается на patternTrailer, и длина matchKey больше или равна длине expansionKey, то
- Пусть target будет значением matchObj[expansionKey].
- Пусть subpath будет подстрокой matchKey, начинающейся с индекса длины patternBase до длины matchKey минус длина patternTrailer.
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, true, isImports, conditions).
- Вернуть объект { resolved, exact: true }.
- В противном случае, если patternBase равно null и matchKey начинается с expansionKey, то
- Пусть target будет значением matchObj[expansionKey].
- Пусть subpath будет подстрокой matchKey, начинающейся с индекса длины expansionKey.
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, false, isImports, conditions).
- Вернуть объект { resolved, exact: false }.
- Вернуть объект { resolved: null, exact: true }.
PATTERN_KEY_COMPARE(keyA, keyB)
- Утверждение: keyA оканчивается на "/" или содержит только один "*".
- Утверждение: keyB оканчивается на "/" или содержит только один "*".
- Пусть baseLengthA будет индексом "*" в keyA плюс один, если keyA содержит "*", или длиной keyA в противном случае.
- Пусть baseLengthB будет индексом "*" в keyB плюс один, если keyB содержит "*", или длиной keyB в противном случае.
- Если baseLengthA больше baseLengthB, вернуть -1.
- Если baseLengthB больше baseLengthA, вернуть 1.
- Если keyA не содержит "*", вернуть 1.
- Если keyB не содержит "*", вернуть -1.
- Если длина keyA больше длины keyB, вернуть -1.
- Если длина keyB больше длины keyA, вернуть 1.
- Вернуть 0.
PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions)
- Если target является строкой, то
- Если pattern равно false, subpath имеет ненулевую длину и target не оканчивается на "/", выбросить ошибку Invalid Module Specifier.
- Если target не начинается с "./", то
- Если internal равно true и target не начинается с "../" или "/" и не является допустимым URL, то
- Если pattern равно true, то
- Вернуть PACKAGE_RESOLVE(target с заменой каждого "*" на subpath, packageURL + "/").
- Вернуть PACKAGE_RESOLVE(target + subpath, packageURL + "/").
- В противном случае, выбросить ошибку Invalid Package Target.
- Если target, разделенный на "/" или "\", содержит ".", ".." или "node_modules" сегменты после первого сегмента, выбросить ошибку Invalid Package Target.
- Пусть resolvedTarget будет результатом разрешения URL конкатенации packageURL и target.
- Утверждение: resolvedTarget содержится в packageURL.
- Если subpath, разделенный на "/" или "\", содержит ".", ".." или "node_modules" сегменты, выбросить ошибку Invalid Module Specifier.
- Если pattern равно true, то
- Вернуть результат разрешения URL resolvedTarget с заменой каждого "*" на subpath.
- В противном случае,
- Вернуть результат разрешения URL конкатенации subpath и resolvedTarget.
- В противном случае, если target является не-нулевым объектом, то
- Если exports содержит ключи свойств-индексов, как определено в ECMA-262 6.1.7 Array Index, выбросить ошибку Invalid Package Configuration.
- Для каждого свойства p объекта target, в порядке вставки,
- Если p равно "default" или conditions содержит запись для p, то
- Пусть targetValue будет значением свойства p в target.
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, targetValue, subpath, pattern, internal, conditions).
- Если resolved равно undefined, продолжить цикл.
- Вернуть resolved.
- Вернуть undefined.
- В противном случае, если target является массивом, то
- Если target.length равно нулю, вернуть null.
- Для каждого элемента targetValue в target выполнить
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, targetValue, subpath, pattern, internal, conditions), продолжая цикл при ошибке Invalid Package Target.
- Если resolved равно undefined, продолжить цикл.
- Вернуть resolved.
- Вернуть или выбросить последнюю ошибку или результат с возвращаемым значением null.
- В противном случае, если target равно null, вернуть null.
- В противном случае, выбросить ошибку Invalid Package Target.
ESM_FORMAT(url)
- Утверждение: url соответствует существующему файлу.
- Пусть pjson будет результатом READ_PACKAGE_SCOPE(url).
- Если url оканчивается на ".mjs", то
- Вернуть "module".
- Если url оканчивается на ".cjs", то
- Вернуть "commonjs".
- Если pjson?.type существует и равно "module", то
- Если url оканчивается на ".js", то
- Вернуть "module".
- Выбросить ошибку Unsupported File Extension.
- В противном случае,
- Выбросить ошибку Unsupported File Extension.
READ_PACKAGE_SCOPE(url)
- Пусть scopeURL будет url.
- Пока scopeURL не является корнем файловой системы,
- Установить scopeURL на родительский URL scopeURL.
- Если scopeURL оканчивается на сегмент пути "node_modules", вернуть null.
- Пусть pjson будет результатом READ_PACKAGE_JSON(scopeURL).
- Если pjson не null, то
- Вернуть pjson.
- Вернуть null.
READ_PACKAGE_JSON(packageURL)
- Пусть pjsonURL будет результатом разрешения "package.json" в packageURL.
- Если файл по адресу pjsonURL не существует, то
- Вернуть null.
- Если файл по адресу packageURL не может быть проанализирован как корректный JSON, то
- Выбросить ошибку Invalid Package Configuration.
- Вернуть разобранный JSON-источник файла по адресу pjsonURL.
Настройка алгоритма разрешения спецификаторов ESM
Текущее разрешение спецификаторов не поддерживает все стандартные возможности загрузчика CommonJS. Одно из различий в поведении заключается в автоматическом разрешении расширений файлов и возможности импорта каталогов, содержащих файл index.
Флаг --experimental-specifier-resolution=[mode] может быть использован для настройки алгоритма разрешения расширений. По умолчанию используется режим explicit, который требует полного пути к модулю для загрузчика. Для включения автоматического разрешения расширений и импорта из каталогов, содержащих файл index, используйте режим node.
$ node index.mjs success! $ node index # Failure! Error: Cannot find module $ node --experimental-specifier-resolution=node index success!
© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v16.x/docs/api/esm.html