Spec-Zone.ru › TypeScript 5.1

Модули ECMAScript в Node.js

В последние несколько лет Node.js работает над поддержкой выполнения модулей ECMAScript (ESM). Это была очень сложная функция для поддержки, так как основа экосистемы Node.js построена на другой системе модулей под названием CommonJS (CJS).

Взаимодействие между двумя системами модулей создаёт большие сложности, с множеством новых функций, которые нужно учитывать; однако, поддержка ESM в Node.js теперь реализована, и пыль начала оседать.

Вот почему TypeScript предоставляет две новые module и moduleResolution настройки: Node16 и NodeNext.

{
    "compilerOptions": {
        "module": "NodeNext",
    }
}

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

type в package.json и новые расширения

Node.js поддерживает новую настройку в package.json под названием type. "type" можно установить на "module" или "commonjs".

{
    "name": "my-package",
    "type": "module",

    "//": "...",
    "dependencies": {
    }
}

Эта настройка определяет, будут ли файлы .js и .d.ts интерпретироваться как модули ES или CommonJS, и по умолчанию устанавливается как CommonJS, если не задано. Когда файл считается модулем ES, несколько правил действуют по-другому по сравнению с CommonJS:

  • import/export инструкции и верхнеуровневые await могут использоваться
  • абсолютные пути к импорту требуют полных расширений (например, нам нужно написать import "./foo.js" вместо import "./foo")
  • импорты могут разрешаться по-разному от зависимостей в node_modules
  • некоторые глобальные значения, такие как require() и __dirname, нельзя использовать напрямую
  • модули CommonJS импортируются по определенным специальным правилам

Мы вернемся к некоторым из них.

Чтобы наложить способ работы TypeScript в этой системе, файлы .ts и .tsx теперь работают одинаково. Когда TypeScript находит файл .ts, .tsx, .js, или .jsx, он будет подниматься вверх, чтобы найти package.json, чтобы узнать, является ли этот файл модулем ES, и использовать это для определения:

  • как найти другие модули, которые импортирует этот файл
  • и как преобразовать этот файл при создании выходных данных

Когда файл .ts компилируется как модуль ES, синтаксис ECMAScript import/export остается неизменным в выходных данных .js; когда он компилируется как модуль CommonJS, он выведет те же выходные данные, которые вы получаете сегодня по module: commonjs.

Это также означает, что пути разрешаются по-разному для файлов .ts которые являются модулями ES и модулями CJS. Например, предположим, что у вас есть следующий код сегодня:

// ./foo.ts
export function helper() {
    // ...
}

// ./bar.ts
import { helper } from "./foo"; // only works in CJS

helper();

Этот код работает в модулях CommonJS, но потерпит неудачу в модулях ES, потому что относительные пути импорта должны использовать расширения. В результате его необходимо переписать, используя расширение выхода foo.ts - так что bar.ts вместо этого должен импортировать из ./foo.js.

// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS

helper();

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

Ещё один момент, который следует отметить, заключается в том, что это относится и к файлам .d.ts. Когда TypeScript находит файл .d.ts в пакете, то определяется ли он как файл ESM или CommonJS, основано на содержащем пакете.

Новые расширения файлов

Поле type в package.json удобно, поскольку позволяет продолжать использовать расширения файлов .ts и .js, что может быть удобно; однако, вам иногда потребуется написать файл, отличающийся от того, что указывает type. Вы также можете просто всегда быть явным.

Node.js поддерживает два расширения, которые помогут в этом: .mjs и .cjs. Файлы .mjs всегда являются модулями ES, а файлы .cjs всегда являются модулями CommonJS, и переопределить их нельзя.

В свою очередь, TypeScript поддерживает два новых расширения файлов исходного кода: .mts и .cts. Когда TypeScript выводит их в файлы JavaScript, он выведет их в .mjs и .cjs соответственно.

Кроме того, TypeScript также поддерживает два новых расширения файлов объявлений: .d.mts и .d.cts. Когда TypeScript генерирует файлы объявлений для .mts и .cts, соответствующие расширения будут .d.mts и .d.cts.

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

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

Node.js позволяет модулям ES импортировать модули CommonJS, как если бы они были модулями ES с экспортом по умолчанию.

// @filename: helper.cts
export function helper() {
    console.log("hello world!");
}
 
// @filename: index.mts
import foo from "./helper.cjs";
 
// prints "hello world!"
foo.helper();

В некоторых случаях Node.js также синтезирует именованные экспорты из модулей CommonJS, что может быть удобнее. В этих случаях модули ES могут использовать импорт в стиле «пространства имён» (т. е. import * as foo from "...") или именованные импорты (т. е. import { helper } from "...").

// @filename: helper.cts
export function helper() {
    console.log("hello world!");
}
 
// @filename: index.mts
import { helper } from "./helper.cjs";
 
// prints "hello world!"
helper();

Не всегда есть способ, чтобы TypeScript знал, будут ли эти именованные импорты синтезированы, но TypeScript будет допускать их и использовать некоторые эвристики при импорте из файла, который определенно является модулем CommonJS.

Один конкретный для TypeScript комментарий относительно взаимодействия - следующий синтаксис:

import foo = require("foo");

В модуле CommonJS это просто сводится к вызову require(), а в модуле ES это импортирует createRequire для достижения того же результата. Это сделает код менее переносимым в средах выполнения, таких как браузеры (которые не поддерживают require()), но часто будет полезным для взаимодействия. В свою очередь, вы можете написать приведенный выше пример с использованием этого синтаксиса следующим образом:

// @filename: foo.cts
export function helper() {
    console.log("hello world!");
}
 
// @filename: index.mts
import foo = require("./foo.cjs");
 
foo.helper()

Наконец, стоит отметить, что единственный способ импортировать файлы ESM из модуля CJS - использование динамических вызовов import(). Это может создать проблемы, но это поведение в Node.js на сегодня.

Вы можете подробнее узнать о взаимодействии ESM/CommonJS в Node.js здесь.

package.json Экспорт, импорт и самоссылка

Node.js поддерживает новое поле для определения точек входа в package.json под названием "exports". Это поле - более мощная альтернатива определению "main" в package.json, и может контролировать какие части вашего пакета будут доступны потребителям.

Вот пример package.json, который поддерживает отдельные точки входа для CommonJS и ESM:

// package.json
{
    "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for `import "my-package"` in ESM
            "import": "./esm/index.js",

            // Entry-point for `require("my-package") in CJS
            "require": "./commonjs/index.cjs",
        },
    },

    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs",
}

Эта функция многогранна, о чем вы можете узнать больше в документации Node.js. Здесь мы постараемся сфокусироваться на том, как TypeScript её поддерживает.

С оригинальной поддержкой Node в TypeScript, он искал бы поле "main", а затем искал бы файлы объявлений, которые соответствовали этому входу. Например, если "main" указывал на ./lib/index.js, TypeScript искал бы файл с именем ./lib/index.d.ts. Автор пакета может переопределить это, указав отдельное поле под названием "types" (например, "types": "./types/index.d.ts").

Новая поддержка работает аналогично условиям импорта. По умолчанию TypeScript накладывает те же правила на условия импорта - если вы пишете import из модуля ES, он будет искать поле import, а из модуля CommonJS, он будет смотреть на поле require. Если он их найдёт, он будет искать сопутствующий файл объявления. Если вам нужно указать другое местоположение для файлов описания типов, вы можете добавить условие импорта "types".

// package.json
{
    "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for `import "my-package"` in ESM
            "import": {
                // Where TypeScript will look.
                "types": "./types/esm/index.d.ts",

                // Where Node.js will look.
                "default": "./esm/index.js"
            },
            // Entry-point for `require("my-package")` in CJS
            "require": {
                // Where TypeScript will look.
                "types": "./types/commonjs/index.d.cts",

                // Where Node.js will look.
                "default": "./commonjs/index.cjs"
            },
        }
    },

    // Fall-back for older versions of TypeScript
    "types": "./types/index.d.ts",

    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs"
}

Условие "types" должно всегда стоять первым в "exports".

Важно отметить, что каждой точке входа CommonJS и модуля ES нужен свой собственный файл объявления, даже если содержимое одинаково между ними. Каждый файл объявления интерпретируется либо как модуль CommonJS, либо как модуль ES, на основании его расширения файла и поля "type" пакета package.json, и этот обнаруженный тип модуля должен совпадать с типом модуля, который Node обнаружит для соответствующего файла JavaScript, чтобы проверка типов была корректной. Попытка использовать один файл .d.ts для типизации как точки входа модуля ES, так и точки входа модуля CommonJS, заставит TypeScript думать, что существует только одна из этих точек входа, что приведёт к ошибкам компилятора для пользователей пакета.

TypeScript также поддерживает поле "imports" пакета package.json аналогичным образом (поиск файлов объявления рядом с соответствующими файлами) и поддерживает самоссылку пакетов. Эти функции обычно не столь сложны, но поддерживаются.

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/esm-node.html

Spec-Zone.ru

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