Модули: 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';
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 существует экспериментальный флаг API import.meta.resolve.
В качестве альтернативы можно использовать 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<функция> - Возвращает: <объект>
-
url<строка>
-
Обработчик resolve возвращает разрешенный URL файла для заданного спецификатора модуля и родительского URL. Спецификатор модуля — это строка в выражении 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);
} getFormat(url, context, defaultGetFormat)
Примечание: API загрузчиков перерабатывается. Этот обработчик может исчезнуть или его сигнатура может измениться. Не полагайтесь на описание API ниже.
Обработчик getFormat предоставляет способ определения пользовательского метода интерпретации URL. Возвращаемый format также влияет на допустимые формы значений источника для модуля при парсинге. Это может быть одним из следующих:
format |
Описание | Допустимые типы для source , возвращаемые getSource или transformSource
|
|---|---|---|
'builtin' |
Загрузка встроенного модуля Node.js | Не применимо |
'commonjs' |
Загрузка модуля Node.js CommonJS | Не применимо |
'json' |
Загрузка JSON-файла | { string, ArrayBuffer, TypedArray } |
'module' |
Загрузка ES-модуля | { string, ArrayBuffer, TypedArray } |
'wasm' |
Загрузка модуля WebAssembly | { ArrayBuffer, TypedArray } |
Примечание: Эти типы соответствуют классам, определённым в ECMAScript.
- Конкретный объект
ArrayBufferявляетсяSharedArrayBuffer. - Конкретный объект
TypedArrayявляетсяUint8Array.
Примечание: Если значение источника текстового формата (то есть 'json', 'module') не является строкой, оно преобразуется в строку с помощью util.TextDecoder.
/**
* @param {string} url
* @param {Object} context (currently empty)
* @param {Function} defaultGetFormat
* @returns {Promise<{ format: string }>}
*/
export async function getFormat(url, context, defaultGetFormat) {
if (Math.random() > 0.5) { // Some condition.
// For some or all URLs, do some custom logic for determining format.
// Always return an object of the form {format: <string>}, where the
// format is one of the strings in the preceding table.
return {
format: 'module',
};
}
// Defer to Node.js for all other URLs.
return defaultGetFormat(url, context, defaultGetFormat);
} getSource(url, context, defaultGetSource)
Примечание: API загрузчиков перерабатывается. Этот обработчик может исчезнуть или его сигнатура может измениться. Не полагайтесь на описание API ниже.
-
url<строка> -
context<объект>-
format<строка>
-
-
defaultGetSource<функция> - Возвращает: <объект>
-
source<строка> | <SharedArrayBuffer> | <Uint8Array>
-
Обработчик getSource предоставляет способ определения пользовательского метода получения исходного кода спецификатора ES-модуля. Это позволит загрузчику потенциально избежать чтения файлов с диска.
/**
* @param {string} url
* @param {{ format: string }} context
* @param {Function} defaultGetSource
* @returns {Promise<{ source: !(string | SharedArrayBuffer | Uint8Array) }>}
*/
export async function getSource(url, context, defaultGetSource) {
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 {source: <string|buffer>}.
return {
source: '...',
};
}
// Defer to Node.js for all other URLs.
return defaultGetSource(url, context, defaultGetSource);
} transformSource(source, context, defaultTransformSource)
Примечание: API загрузчиков перерабатывается. Этот обработчик может исчезнуть или его сигнатура может измениться. Не полагайтесь на описание API ниже.
-
source<строка> | <SharedArrayBuffer> | <Uint8Array> -
context<объект> - Возвращает: <объект>
-
source<строка> | <SharedArrayBuffer> | <Uint8Array>
-
Обработчик transformSource предоставляет способ изменения исходного кода загруженного файла ES-модуля после загрузки исходной строки, но перед выполнением каких-либо действий Node.js.
Если этот обработчик используется для преобразования типов файлов, неизвестных Node.js, в исполняемый JavaScript, также необходим обработчик разрешения для регистрации любых неизвестных расширений файлов Node.js. См. пример загрузчика транспилятора ниже.
/**
* @param {!(string | SharedArrayBuffer | Uint8Array)} source
* @param {{
* format: string,
* url: string,
* }} context
* @param {Function} defaultTransformSource
* @returns {Promise<{ source: !(string | SharedArrayBuffer | Uint8Array) }>}
*/
export async function transformSource(source, context, defaultTransformSource) {
const { url, format } = context;
if (Math.random() > 0.5) { // Some condition.
// For some or all URLs, do some custom logic for modifying the source.
// Always return an object of the form {source: <string|buffer>}.
return {
source: '...',
};
}
// Defer to Node.js for all other sources.
return defaultTransformSource(source, context, defaultTransformSource);
} getGlobalPreloadCode()
Примечание: API загрузчиков перерабатывается. Этот обработчик может исчезнуть или его сигнатура может измениться. Не полагайтесь на описание API ниже.
- Возвращает: <строка>
Иногда может потребоваться выполнить код в той же глобальной области видимости, что и приложение. Этот хук позволяет вернуть строку, которая выполняется как скрипт в режиме sloppy на старте.
Аналогично тому, как работают обертки CommonJS, код выполняется в неявной области видимости функции. Единственным аргументом является функция типа require, которая может использоваться для загрузки встроенных функций, таких как "fs": getBuiltin(request: string).
Если код требует более продвинутых require функций, он должен создать свою require с помощью module.createRequire().
/**
* @returns {string} Code to run before application startup
*/
export function getGlobalPreloadCode() {
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');
const { createRequire } = getBuiltin('module');
const require = createRequire(process.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 getFormat(url, context, defaultGetFormat) {
// This loader assumes all network-provided JavaScript is ES module code.
if (url.startsWith('https://')) {
return {
format: 'module'
};
}
// Let Node.js handle all other URLs.
return defaultGetFormat(url, context, defaultGetFormat);
}
export function getSource(url, context, defaultGetSource) {
// 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({ source: data }));
}).on('error', (err) => reject(err));
});
}
// Let Node.js handle all other URLs.
return defaultGetSource(url, context, defaultGetSource);
} // 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 с помощью хука transformSource. Однако перед вызовом этого хука другие хуки должны указать Node.js не выдавать ошибку при обнаружении неизвестных типов файлов и сообщить Node.js, как загружать этот новый тип файлов.
Это менее эффективно, чем транспилирование исходных файлов до запуска Node.js; загрузчик транспайлера следует использовать только в целях разработки и тестирования.
// coffeescript-loader.mjs
import { URL, pathToFileURL } from 'url';
import CoffeeScript from 'coffeescript';
const baseURL = pathToFileURL(`${process.cwd()}/`).href;
// CoffeeScript files end in .coffee, .litcoffee or .coffee.md.
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
export 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 function getFormat(url, context, defaultGetFormat) {
// Now that we patched resolve to let CoffeeScript URLs through, we need to
// tell Node.js what format such URLs should be interpreted as. For the
// purposes of this loader, all CoffeeScript URLs are ES modules.
if (extensionsRegex.test(url)) {
return {
format: 'module'
};
}
// Let Node.js handle all other URLs.
return defaultGetFormat(url, context, defaultGetFormat);
}
export function transformSource(source, context, defaultTransformSource) {
const { url, format } = context;
if (extensionsRegex.test(url)) {
return {
source: CoffeeScript.compile(source, { bare: true })
};
}
// Let Node.js handle all other sources.
return defaultTransformSource(source, context, defaultTransformSource);
} # 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"].
Решатель может вызывать следующие ошибки:
- Invalid Module Specifier: Спецификатор модуля является недопустимым URL-адресом, именем пакета или спецификатором подпути пакета.
- Invalid Package Configuration: Конфигурация package.json неверна или содержит неверную конфигурацию.
- Invalid Package Target: Экспорт или импорт пакета определяют целевой модуль для пакета, который является недопустимым типом или строковым целевым объектом.
- Package Path Not Exported: Экспорт пакета не определяет или не разрешает целевой подпуть в пакете для данного модуля.
- Package Import Not Defined: Импорт пакета не определяет спецификатор.
- Module Not Found: Запрошенный пакет или модуль не существует.
- Unsupported Directory Import: Разрешенный путь соответствует каталогу, который не является поддерживаемой целью для импорта модулей.
Спецификация алгоритма решателя
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" соответственно), то
- Выбросьте ошибку Invalid Module Specifier.
- Если файл по resolved является каталогом, то
- Выбросьте ошибку Unsupported Directory Import.
- Если файл по resolved не существует, то
- Выбросьте ошибку Module Not Found.
- Установите resolved в реальный путь resolved.
- Пусть format будет результатом ESM_FORMAT(resolved).
- Загрузите resolved как формат модуля, format.
- Верните resolved.
PACKAGE_RESOLVE(packageSpecifier, parentURL)
- Пусть packageName будет undefined.
- Если packageSpecifier является пустой строкой, то
- Выбросьте ошибку Invalid Module Specifier.
- Если packageSpecifier не начинается с "@", то
- Установите packageName в подстроку packageSpecifier до первого разделителя "/" или конца строки.
- В противном случае,
- Если packageSpecifier не содержит разделителя "/", то
- Выбросьте ошибку Invalid Module Specifier.
- Установите packageName в подстроку packageSpecifier до второго разделителя "/" или конца строки.
- Если packageName начинается с "." или содержит "\" или "%", то
- Выбросьте ошибку Invalid Module Specifier.
- Пусть 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, выбросив ошибку Module Not Found при отсутствии разрешения.
- В противном случае,
- Верните результат разрешения URL packageSubpath в packageURL.
- Выбросьте ошибку Module Not Found.
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, subpath, pjson.exports, defaultConditions).
- В противном случае, верните undefined.
PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
- Если exports — это объект, содержащий как ключ, начинающийся с ".", так и ключ, не начинающийся с ".", выбросьте ошибку Invalid Package Configuration.
- Если 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 Path Not Exported.
PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
- Утверждение: specifier начинается с "#".
- Если specifier точно равен "#" или начинается с "#/", то
- Выбросьте ошибку Invalid Module Specifier.
- Пусть packageURL будет результатом READ_PACKAGE_SCOPE(parentURL).
- Если packageURL не null, то
- Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
- Если pjson.imports — это ненулевой объект, то
- Пусть resolvedMatch будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE(specifier, pjson.imports, packageURL, true, conditions).
- Если resolvedMatch.resolve не null или undefined, то
- Верните resolvedMatch.
- Выбросьте ошибку Package Import Not Defined.
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, заканчивающихся на "/" или "*", отсортированным по длине в порядке убывания.
- Для каждого ключа expansionKey в expansionKeys выполнить
- Если expansionKey заканчивается на "*" и matchKey начинается с подстроки expansionKey, исключая последний символ "*", но не равно ей, то
- Пусть target будет значением matchObj[expansionKey].
- Пусть subpath будет подстрокой matchKey, начинающейся с индекса, равного длине expansionKey минус один.
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, true, isImports, conditions).
- Вернуть объект { resolved, exact: true }.
- Если 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 }.
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.
- Вернуть или выбросить последнюю ошибку fallback resolution null return или ошибку.
- В противном случае, если 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. Одно из отличий заключается в автоматическом разрешении расширений файлов и возможности импортировать директории с файлом индекса.
Флаг --experimental-specifier-resolution=[mode] можно использовать для настройки алгоритма разрешения расширений. По умолчанию используется режим explicit, который требует полного пути к модулю для загрузчика. Для включения автоматического разрешения расширений и импорта из директорий, содержащих файл индекса, используйте режим 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-v14.x/docs/api/esm.html