Spec-Zone.ru › Node.js 12 LTS

Модули: ECMAScript-модули

История
Версия Изменения
v12.22.0

Стабилизация реализации модулей.

v12.20.0

Поддержка распознавания именованных экспортов CommonJS.

v12.20.0

Удаление предупреждений об экспериментальных модулях.

v12.17.0

Загрузка ECMAScript-модулей больше не требует командной строки.

v12.0.0

Добавлена поддержка ES-модулей с использованием расширения .js файла через поле package.json "type".

v8.5.0

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

Устойчивость: 2 - Стабильно

Введение

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().

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

  • Спецификаторы без имени, такие как 'some-package'. Они ссылаются на точку входа пакета по имени пакета.

  • Спецификаторы глубокого импорта, такие как 'some-package/lib/shuffle.mjs'. Они ссылаются на путь внутри пакета, префикс которого — имя пакета.

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

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

Спецификаторы без имени и часть спецификатора глубокого импорта без имени — строки; все остальное в спецификаторе — URL.

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

Спецификаторы не могут начинаться с / или //. Они зарезервированы для потенциального использования в будущем. Корень текущего объёма может быть сослаться посредством file:///.

node: Импорты

Добавлена в: v12.20.0

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

import fs from 'node:fs/promises';

data: Импорты

Добавлена в: v12.10.0

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!"';

import.meta

  • <Объект>

Метасвойство import.meta — это Object, содержащее следующее свойство:

  • url <строка> Абсолютный file: URL модуля.

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

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

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

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

Отсутствует NODE_PATH

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

Отсутствуют require, exports, module.exports, __filename, __dirname

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

require можно импортировать в ES-модуль с помощью module.createRequire().

Аналоги __filename и __dirname можно создать внутри каждого файла с помощью import.meta.url.

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Отсутствует require.resolve

Предыдущие случаи использования, полагающиеся на require.resolve для определения разрешенного пути модуля, могут быть поддержаны с помощью import.meta.resolve, что является экспериментальным и поддерживается с флагом --experimental-import-meta-resolve:

(async () => {
  const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
})();

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

(async () => {
  // Equivalent to import.meta.resolve('./dep')
  await import.meta.resolve('./dep', import.meta.url);
})();

Эта функция асинхронна, так как система разрешения ES-модулей в Node.js асинхронна. Введение Top-Level Await упростит эти случаи использования, так как им не потребуется оболочка асинхронной функции.

Отсутствует require.extensions

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

Отсутствует require.cache

require.cache не используется import. Имеет отдельный кэш.

Пути, основанные на URL

ES-модули разрешаются и кэшируются на основе семантики 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:.

Взаимозаменяемость с CommonJS

require

require всегда обрабатывает файлы, на которые ссылается, как CommonJS. Это относится как к традиционному использованию require в среде CommonJS, так и к использованию в среде ES-модулей с помощью module.createRequire().

Для включения ES-модуля в CommonJS используйте import().

import операторы

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

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

Доступны дополнительные экспериментальные флаги для импорта Wasm-модулей или JSON-модулей. Для импорта модулей native или JSON-модулей без флагов см. module.createRequire().

Спецификатор оператора import (строка после ключевого слова from) может быть URL-подобным относительным путем, например, './file.mjs', или именем пакета, например, 'fs'.

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

import { sin, cos } from 'geometry/trigonometry-functions.mjs';

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

Динамические import() поддерживаются как в CommonJS, так и в ES-модулях. Их можно использовать для включения файлов 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

Для лучшей совместимости с существующим использованием в экосистеме JavaScript, 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' }

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

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

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

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

Модули по умолчанию

Ядерные модули предоставляют именованные экспорты своей публичной 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;

Модули CommonJS, JSON и нативные модули

Модули CommonJS, JSON и нативные модули могут быть использованы с module.createRequire().

// cjs.cjs
module.exports = 'cjs';

// esm.mjs
import { createRequire } from 'module';

const require = createRequire(import.meta.url);

const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true

Экспериментальные JSON-модули

В настоящее время импорт JSON-модулей поддерживается только в режиме commonjs и загружается с помощью загрузчика CJS. Спецификация WHATWG JSON-модулей https://html.spec.whatwg.org/#creating-a-json-module-script все еще находится в стадии стандартизации и экспериментально поддерживается путем включения дополнительного флага --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 на https://github.com/webassembly/esm-integration.

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

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

выполняется в контексте:

node --experimental-wasm-modules index.mjs

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

Экспериментальные загрузчики

Примечание: Этот 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, описанный ниже.

  • url <строка>
  • context <объект>
  • defaultGetFormat <функция>
  • Возвращает: <объект>
    • format <строка>

Крючок getFormat предоставляет способ определения настраиваемого метода интерпретации URL. Возвращаемый format также влияет на допустимые формы исходных значений модуля при парсинге. Он может быть одним из следующих:

format Описание Допустимые типы для source , возвращаемые getSource или transformSource
'builtin' Загрузка встроенного модуля Node.js Не применимо
'dynamic' Использование крючка динамической инициализации dynamic instantiate hook Не применимо
'commonjs' Загрузка модуля CommonJS Node.js Не применимо
'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)

NODE_OPTIONS='--experimental-loader ./custom-loader.mjs' node x.js

Примечание: API загрузчиков перерабатывается. Эта фиксация может исчезнуть или её сигнатура может измениться. Не полагайтесь на описанный ниже API.

  • source <строка> | <SharedArrayBuffer> | <Uint8Array>
  • context <объект>
    • format <строка>
    • url <строка>
  • Возвращает: <объект>
    • 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.

  • Возвращает: <строка>

Иногда может потребоваться выполнить код в том же глобальном контексте, в котором работает приложение. Эта фиксация позволяет вернуть строку, которая выполняется как скрипт в режиме «без строгости» при запуске.

Подобно тому, как работают обёртки 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>');
// [...]
`;
}

dynamicInstantiate фиксация

Примечание: API загрузчиков перерабатывается. Эта фиксация может исчезнуть или её сигнатура может измениться. Не полагайтесь на описанный ниже API.

Для создания пользовательского динамического модуля, не соответствующего одному из существующих format интерпретаций, можно использовать фиксацию dynamicInstantiate. Эта фиксация вызывается только для модулей, которые возвращают format: 'dynamic' из фиксации getFormat.

/**
 * @param {string} url
 * @returns {object} response
 * @returns {array} response.exports
 * @returns {function} response.execute
 */
export async function dynamicInstantiate(url) {
  return {
    exports: ['customExportName'],
    execute: (exports) => {
      // Get and set functions provided for pre-allocated export names
      exports.customExportName.set('value');
    }
  };
}

С предварительно предоставленным списком экспортов модуля, функция execute будет затем вызвана в точности в момент оценки модуля в порядке импорта для этого модуля в дереве импорта.

Примеры

Различные фиксации загрузчика могут использоваться вместе для достижения широкого спектра кастомизации поведения загрузки и оценки кода 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
  • Отсутствие расширений по умолчанию
  • Отсутствие главных папок
  • Поиск разрешения спецификатора без префикса (bare specifier) через node_modules

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

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

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

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

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

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

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

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

ESM_RESOLVE(specifier, parentURL)

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

PACKAGE_RESOLVE(packageSpecifier, parentURL)

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

PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)

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

PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)

  1. Если exports — объект, содержащий ключ, начинающийся с ".", и ключ, не начинающийся с ".", выбросить ошибку Invalid Package Configuration.
  2. Если subpath равно ".", то
    1. Пусть mainExport будет undefined.
    2. Если exports — строка, массив или объект без ключей, начинающихся с ".", то
      1. Установить mainExport в exports.
    3. В противном случае, если exports — объект, содержащий свойство ".", то
      1. Установить mainExport в exports["."].
    4. Если mainExport не равно undefined, то
      1. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, mainExport, "", false, false, conditions).
      2. Если resolved не равно null или undefined, то
        1. Вернуть resolved.
  3. В противном случае, если exports — объект и все ключи exports начинаются с ".", то
    1. Пусть matchKey будет строкой "./", конкатенированной с subpath.
    2. Пусть resolvedMatch будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
    3. Если resolvedMatch.resolve не равно null или undefined, то
      1. Вернуть resolvedMatch.
  4. Выбросить ошибку Package Path Not Exported.

PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)

  1. Утверждение: specifier начинается с "#".
  2. Если specifier точно равно "#" или начинается с "#/", то
    1. Выбросить ошибку Invalid Module Specifier.
  3. Пусть packageURL будет результатом READ_PACKAGE_SCOPE(parentURL).
  4. Если packageURL не равно null, то
    1. Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
    2. Если pjson.imports — непустой объект, то
      1. Пусть resolvedMatch будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE(specifier, pjson.imports, packageURL, true, conditions).
      2. Если resolvedMatch.resolve не равно null или undefined, то
        1. Вернуть resolvedMatch.
  5. Выбросить ошибку Package Import Not Defined.

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

  1. Если matchKey — ключ matchObj и не заканчивается на "*", то
    1. Пусть target будет значением matchObj[matchKey].
    2. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, "", false, isImports, conditions).
    3. Вернуть объект { resolved, exact: true }.
  2. Пусть expansionKeys — список ключей matchObj, оканчивающихся на "/" или "*", отсортированный по длине в убывающем порядке.
  3. Для каждого ключа expansionKey в expansionKeys, выполните
    1. Если expansionKey оканчивается на "*" и matchKey начинается с, но не равен подстроке expansionKey без последнего символа "*", то
      1. Пусть target будет значением matchObj[expansionKey].
      2. Пусть subpath будет подстрокой matchKey, начиная с индекса длины expansionKey минус один.
      3. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, true, isImports, conditions).
      4. Вернуть объект { resolved, exact: true }.
    2. Если matchKey начинается с expansionKey, то
      1. Пусть target будет значением matchObj[expansionKey].
      2. Пусть subpath будет подстрокой matchKey, начиная с индекса длины expansionKey.
      3. Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, false, isImports, conditions).
      4. Вернуть объект { resolved, exact: false }.
  4. Вернуть объект { resolved: null, exact: true }.

PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions)

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

ESM_FORMAT(url)

  1. Утверждение: url соответствует существующему файлу.
  2. Пусть pjson будет результатом READ_PACKAGE_SCOPE(url).
  3. Если url заканчивается на ".mjs", то
    1. Вернуть "module".
  4. Если url заканчивается на ".cjs", то
    1. Вернуть "commonjs".
  5. Если pjson?.type существует и равно "module", то
    1. Если url заканчивается на ".js", то
      1. Вернуть "module".
    2. Выбросить ошибку Неподдерживаемое расширение файла.
  6. В противном случае,
    1. Выбросить ошибку Неподдерживаемое расширение файла.

READ_PACKAGE_SCOPE(url)

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

READ_PACKAGE_JSON(packageURL)

  1. Пусть pjsonURL будет результатом разрешения "package.json" в рамках packageURL.
  2. Если файл по адресу pjsonURL не существует, то
    1. Вернуть null.
  3. Если файл по адресу packageURL не может быть проанализирован как допустимый JSON, то
    1. Выбросить ошибку Неверная конфигурация пакета.
  4. Вернуть разобранный 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-v12.x/docs/api/esm.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API