Разрешение модулей в JavaScript — это сложная тема.
Экосистема в настоящее время находится в процессе многолетнего перехода от модулей CommonJS к нативным ES-модулям. TypeScript применяет собственный набор правил для расширений импорта, которые не совместимы с ESM. Различные инструменты сборки поддерживают переназначение путей через разные несовместимые механизмы.
Bun стремится предоставить согласованную и предсказуемую систему разрешения модулей, которая просто работает. К сожалению, она всё ещё довольно сложна.
Синтаксис
Рассмотрим следующие файлы.
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}Когда мы запускаем index.ts, он выводит "Hello world!".
bun index.ts
Hello world!В этом случае мы импортируем из ./hello, относительного пути без расширения. Импорты с расширениями необязательны, но поддерживаются. Для разрешения этого импорта Bun проверит следующие файлы в порядке:
./hello.tsx./hello.jsx./hello.ts./hello.mjs./hello.js./hello.cjs./hello.json./hello/index.tsx./hello/index.jsx./hello/index.ts./hello/index.mjs./hello/index.js./hello/index.cjs./hello/index.json
Пути импорта могут включать расширения. Если расширение присутствует, Bun проверит только файл с этим точным расширением.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // это работаетЕсли вы импортируете from "*.js{x}", Bun дополнительно проверит соответствующий файл *.ts{x}, чтобы быть совместимым с поддержкой ES-модулей TypeScript.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // это работает
import { hello } from "./hello.js"; // это также работаетBun поддерживает как ES-модули (синтаксис import/export), так и модули CommonJS (require()/module.exports). Следующая версия CommonJS также будет работать в Bun.
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;Тем не менее, использование CommonJS не рекомендуется в новых проектах.
Системы модулей
Bun имеет нативную поддержку CommonJS и ES-модулей. ES-модули — рекомендуемый формат модулей для новых проектов, но модули CommonJS всё ещё широко используются в экосистеме Node.js.
В JavaScript-рантайме Bun require может использоваться как ES-модулями, так и модулями CommonJS. Если целевой модуль — ES-модуль, require возвращает объект пространства имён модуля (эквивалентно import * as). Если целевой модуль — модуль CommonJS, require возвращает объект module.exports (как в Node.js).
| Тип модуля | require() | import * as |
|---|---|---|
| ES-модуль | Пространство имён модуля | Пространство имён модуля |
| CommonJS | module.exports | default — это module.exports, ключи module.exports — именованные экспорты |
Использование require()
Вы можете require() любой файл или пакет, даже файлы .ts или .mjs.
const { foo } = require("./foo"); // расширения необязательны
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");Что такое модуль CommonJS?
В 2016 году ECMAScript добавил поддержку ES-модулей. ES-модули — это стандарт для модулей JavaScript. Однако миллионы npm-пакетов всё ещё используют модули CommonJS.
Модули CommonJS — это модули, которые используют module.exports для экспорта значений. Обычно require используется для импорта модулей CommonJS.
const stuff = require("./stuff");
module.exports = { stuff };Наибольшее различие между CommonJS и ES-модулями заключается в том, что модули CommonJS синхронны, а ES-модули асинхронны. Есть и другие различия.
- ES-модули поддерживают top-level
await, а модули CommonJS — нет. - ES-модули всегда находятся в strict mode, а модули CommonJS — нет.
- Браузеры не имеют нативной поддержки модулей CommonJS, но имеют нативную поддержку ES-модулей через
<script type="module">. - Модули CommonJS не поддаются статическому анализу, в то время как ES-модули позволяют только статические импорты и экспорты.
Модули CommonJS: Это тип системы модулей, используемой в JavaScript. Одной из ключевых особенностей модулей CommonJS является то, что они загружаются и выполняются синхронно. Это означает, что когда вы импортируете модуль CommonJS, код в этом модуле выполняется немедленно, и ваша программа ждёт его завершения, прежде чем перейти к следующей задаче. Это похоже на чтение книги от начала до конца без пропуска страниц.
ES-модули (ESM): Это другой тип системы модулей, введённый в JavaScript. Они имеют немного иное поведение по сравнению с CommonJS. В ESM статические импорты (импорты, сделанные с помощью операторов import) синхронны, как и CommonJS. Это означает, что когда вы импортируете ESM с помощью обычного оператора import, код в этом модуле выполняется немедленно, и ваша программа продолжает работу шаг за шагом. Думайте об этом как о чтении книги страница за страницей.
Динамические импорты: Теперь вот часть, которая может быть запутанной. ES-модули также поддерживают импорт модулей на лету через функцию import(). Это называется "динамический импорт", и он асинхронен, поэтому не блокирует выполнение основной программы. Вместо этого он получает и загружает модуль в фоновом режиме, пока ваша программа продолжает работать. Как только модуль готов, вы можете его использовать. Это похоже на получение дополнительной информации из книги, пока вы всё ещё читаете её, не останавливая чтение.
Вкратце:
- Модули CommonJS и статические ES-модули (операторы
import) работают схожим синхронным образом, как чтение книги от начала до конца. - ES-модули также предлагают возможность импортировать модули асинхронно с помощью функции
import(). Это похоже на поиск дополнительной информации в середине чтения книги без остановки.
Использование import
Вы можете import любой файл или пакет, даже файлы .cjs.
import { foo } from "./foo"; // расширения необязательны
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";Использование import и require() вместе
В Bun вы можете использовать import или require в одном файле — они оба работают всегда.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");Top-level await
Единственное исключение из этого правила — top-level await. Вы не можете require() файл, который использует top-level await, поскольку функция require() по своей природе синхронна.
К счастью, очень мало библиотек используют top-level await, поэтому это редко бывает проблемой. Но если вы используете top-level await в коде вашего приложения, убедитесь, что этот файл не require() из другого места в вашем приложении. Вместо этого вы должны использовать import или динамический import().
Импорт пакетов
Bun реализует алгоритм разрешения модулей Node.js, поэтому вы можете импортировать пакеты из node_modules с помощью bare specifier.
import { stuff } from "foo";Полная спецификация этого алгоритма официально задокументирована в документации Node.js; мы не будем здесь её повторять. Кратко: если вы импортируете from "foo", Bun сканирует файловую систему вверх в поисках директории node_modules, содержащей пакет foo.
NODE_PATH
Bun поддерживает NODE_PATH для дополнительных директорий разрешения модулей:
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";Несколько путей используют разделитель платформы (: на Unix, ; на Windows):
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # WindowsКак только он находит пакет foo, Bun читает package.json, чтобы определить, как пакет должен быть импортирован. Для определения точки входа пакета Bun сначала читает поле exports и проверяет следующие условия.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // если импортер — CommonJS
"import": "./index.mjs", // если импортер — ES-модуль
"default": "./index.js"
}
}То из этих условий, которое встречается первым в package.json, используется для определения точки входа пакета.
Bun уважает подпути "exports" и "imports".
{
"name": "foo",
"exports": {
".": "./index.js"
}
}Импорты подпутей и условные импорты работают совместно друг с другом.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}Как и в Node.js, указание любого подпути в карте "exports" предотвратит импорт других подпутей; вы можете импортировать только файлы, которые явно экспортированы. Учитывая package.json выше:
import stuff from "foo"; // это работает
import stuff from "foo/index.mjs"; // это не работаетNOTE
**Поставка TypeScript** — Обратите внимание, что Bun поддерживает специальное условие экспорта `"bun"`. Если ваша библиотека написана на TypeScript, вы можете публиковать ваши (не транспилированные!) файлы TypeScript в `npm` напрямую. Если вы укажете точку входа `*.ts` вашего пакета в условии `"bun"`, Bun будет напрямую импортировать и выполнять ваши исходные файлы TypeScript.Если exports не определён, Bun возвращается к "module" (только импорты ESM), затем к "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}Пользовательские условия
Флаг --conditions позволяет указать список условий для использования при разрешении пакетов из package.json "exports".
Этот флаг поддерживается как в bun build, так и в среде выполнения Bun.
# Используйте с bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# Используйте со средой выполнения Bun:
bun --conditions="react-server" ./app/foo/route.jsВы также можете использовать conditions программно с Bun.build:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});Переназначение путей
Bun поддерживает переназначение путей импорта через compilerOptions.paths TypeScript в tsconfig.json, что хорошо работает с редакторами. Если вы не пользователь TypeScript, вы можете достичь такого же поведения, используя jsconfig.json в корне вашего проекта.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // сопоставить спецификатор с файлом
"components/*": ["components/*"] // сопоставление с подстановочными знаками
}
}
}Bun также поддерживает импорты подпутей в стиле Node.js в package.json, где сопоставленные пути должны начинаться с #. Этот подход не так хорошо работает с редакторами, но оба варианта могут использоваться вместе.
{
"imports": {
"#config": "./config.ts", // сопоставить спецификатор с файлом
"#components/*": "./components/*" // сопоставление с подстановочными знаками
}
}Низкоуровневые детали взаимодействия CommonJS в Bun
JavaScript-рантайм Bun имеет нативную поддержку CommonJS. Когда JavaScript-транспайлер Bun обнаруживает использования module.exports, он обрабатывает файл как CommonJS. Загрузчик модулей затем обернёт транспилированный модуль в функцию следующей формы:
(function (module, exports, require) {
// транспилированный модуль
})(module, exports, require);module, exports и require очень похожи на module, exports и require в Node.js. Они присваиваются через with scope в C++. Внутренняя Map хранит объект exports для обработки циклических вызовов require до полной загрузки модуля.
Как только модуль CommonJS успешно оценён, создаётся Synthetic Module Record с ES-модулем default, экспорт установлен в module.exports, и ключи объекта module.exports переэкспортируются как именованные экспорты (если объект module.exports — объект).
При использовании сборщика Bun это работает иначе. Сборщик обернёт модуль CommonJS в функцию require_${moduleName}, которая возвращает объект module.exports.
import.meta
Объект import.meta — это способ для модуля получить информацию о себе. Это часть языка JavaScript, но его содержимое не стандартизировано. Каждый "хост" (браузер, среда выполнения и т.д.) может реализовать любые свойства, которые пожелает, на объекте import.meta.
Bun реализует следующие свойства.
import.meta.dir; // => "/path/to/project"
import.meta.file; // => "file.ts"
import.meta.path; // => "/path/to/project/file.ts"
import.meta.url; // => "file:///path/to/project/file.ts"
import.meta.main; // `true`, если этот файл выполняется напрямую через `bun run`
// `false` в противном случае
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| Свойство | Описание |
|---|---|
import.meta.dir | Абсолютный путь к директории, содержащей текущий файл, например /path/to/project. Эквивалентно __dirname в модулях CommonJS (и Node.js) |
import.meta.dirname | Псевдоним для import.meta.dir, для совместимости с Node.js |
import.meta.env | Псевдоним для process.env. |
import.meta.file | Имя текущего файла, например index.tsx |
import.meta.path | Абсолютный путь к текущему файлу, например /path/to/project/index.ts. Эквивалентно __filename в модулях CommonJS (и Node.js) |
import.meta.filename | Псевдоним для import.meta.path, для совместимости с Node.js |
import.meta.main | Указывает, является ли текущий файл точкой входа в текущий процесс bun. Выполняется ли файл напрямую через bun run или он импортируется? |
import.meta.resolve | Разрешить спецификатор модуля (например, "zod" или "./file.tsx") в url. Эквивалентно import.meta.resolve в браузерах. Пример: import.meta.resolve("zod") возвращает "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | Строковый url к текущему файлу, например file:///path/to/project/index.ts. Эквивалентно import.meta.url в браузерах |