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 は TypeScript の ES モジュールサポート と互換性を持つために、一致する *.ts{x} ファイルもチェックします。
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 エコシステムで広く使用されています。
Bun の JavaScript ランタイムでは、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() の使用
.ts や .mjs ファイルでも、ファイルまたはパッケージを require() できます。
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 を使用して値をエクスポートするモジュールです。通常、CommonJS モジュールをインポートするには require が使用されます。
const stuff = require("./stuff");
module.exports = { stuff };CommonJS と ES モジュールの最大の違いは、CommonJS モジュールは同期的であるのに対し、ES モジュールは非同期であることです。他にも違いがあります。
- ES モジュールはトップレベル
awaitをサポートしていますが、CommonJS モジュールはサポートしていません。 - ES モジュールは常に strict mode ですが、CommonJS モジュールはそうではありません。
- ブラウザは CommonJS モジュールのネイティブサポートを持っていませんが、
<script type="module">を介して ES モジュールのネイティブサポートを持っています。 - CommonJS モジュールは静的に分析できませんが、ES モジュールは静的なインポートとエクスポートのみを許可します。
CommonJS モジュール: これらは JavaScript で使用されるモジュールシステムの一種です。CommonJS モジュールの主な特徴の 1 つは、同期的にロードおよび実行されることです。つまり、CommonJS モジュールをインポートすると、そのモジュール内のコードは即座に実行され、プログラムは次のタスクに進む前にそれが完了するのを待ちます。これは、ページを飛ばずに最初から最後まで本を読むようなものです。
ES モジュール(ESM): これらは JavaScript で導入された別のモジュールシステムの一種です。これらは CommonJS とは少し異なる動作をします。ESM では、静的インポート(import 文を使用して行われるインポート)は CommonJS と同様に同期的です。つまり、通常の import 文を使用して ESM をインポートすると、そのモジュール内のコードは即座に実行され、プログラムは段階的に進行します。これは、ページごとに本を読むようなものです。
動的インポート: ここで、混乱する可能性がある部分があります。ES モジュールは、import() 関数を介してモジュールをその場でインポートすることもサポートしています。これは「動的インポート」と呼ばれ、非同期であるため、メインプログラムの実行をブロックしません。代わりに、プログラムが実行され続けている間に、バックグラウンドでモジュールを取得してロードします。モジュールの準備ができたら、使用できます。これは、本を読んでいる間に追加情報を取得するようなもので、読むのを一時停止する必要はありません。
要約:
- CommonJS モジュールと静的 ES モジュール(
import文)は、最初から最後まで本を読むような同期的な方法で動作します。 - ES モジュールは、
import()関数を使用してモジュールを非同期にインポートするオプションも提供します。これは、本を読んでいる途中で追加情報を調べるようなもので、停止する必要はありません。
import の使用
.cjs ファイルでも、ファイルまたはパッケージを import できます。
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");トップレベル await
このルールの唯一の例外はトップレベル await です。トップレベル await を使用するファイルを require() することはできません。require() 関数は本質的に同期的であるためです。
幸いなことに、トップレベル await を使用するライブラリはほとんどないため、これはめったに問題になりません。しかし、アプリケーションコードでトップレベル await を使用している場合、そのファイルがアプリケーションの他の場所から require() されていないことを確認してください。代わりに、import または 動的 import() を使用する必要があります。
パッケージのインポート
Bun は Node.js モジュール解決アルゴリズムを実装しているため、ベア指定子で node_modules からパッケージをインポートできます。
import { stuff } from "foo";このアルゴリズムの完全な仕様は、Node.js ドキュメント で公式に文書化されています。ここでは繰り返しません。簡単に言うと、from "foo" をインポートすると、Bun はファイルシステムを上にスキャンして、パッケージ foo を含む node_modules ディレクトリを探します。
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 # Windowsfoo パッケージを見つけると、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` に公開できます。`"bun"` 条件でパッケージの `*.ts` エントリーポイントを指定すると、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.jsBun.build で条件的に conditions を使用することもできます。
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});パスの再マッピング
Bun は、エディターと相性の良い tsconfig.json の TypeScript の compilerOptions.paths を介してインポートパスの再マッピングをサポートしています。TypeScript ユーザーでない場合、プロジェクトルートの jsconfig.json を使用して同じ動作を実現できます。
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // 指定子をファイルにマップ
"components/*": ["components/*"] // ワイルドカードマッチング
}
}
}Bun は package.json の Node.js スタイルのサブパスインポート もサポートしています。この場合、マップされたパスは # で始まる必要があります。このアプローチはエディターとはあまり相性が良くありませんが、両方のオプションを一緒に使用できます。
{
"imports": {
"#config": "./config.ts", // 指定子をファイルにマップ
"#components/*": "./components/*" // ワイルドカードマッチング
}
}Bun での CommonJS 相互運用の低レベルの詳細
Bun の JavaScript ランタイムは CommonJS のネイティブサポートを備えています。Bun の JavaScript トランスパイラーが module.exports の使用を検出すると、そのファイルを CommonJS として扱います。その後、モジュールローダーはトランスパイルされたモジュールを以下のような形状の関数でラップします。
(function (module, exports, require) {
// トランスパイルされたモジュール
})(module, exports, require);module、exports、および require は、Node.js の module、exports、および require と非常によく似ています。これらは C++ の with scope を介して割り当てられます。内部の Map は、モジュールが完全にロードされる前に循環 require 呼び出しを処理するために exports オブジェクトを保存します。
CommonJS モジュールが正常に評価されると、Synthetic Module Record が作成され、default ES モジュール エクスポートが 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; // このファイルが `bun run` によって直接実行される場合は `true`
// それ以外の場合は `false`
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| プロパティ | 説明 |
|---|---|
import.meta.dir | 現在のファイルを含むディレクトリへの絶対パス、例:/path/to/project。CommonJS モジュール(および Node.js)の __dirname と同等 |
import.meta.dirname | Node.js 互換性のために import.meta.dir のエイリアス |
import.meta.env | process.env のエイリアス |
import.meta.file | 現在のファイルの名前、例:index.tsx |
import.meta.path | 現在のファイルへの絶対パス、例:/path/to/project/index.ts。CommonJS モジュール(および Node.js)の __filename と同等 |
import.meta.filename | Node.js 互換性のために import.meta.path のエイリアス |
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 | 現在のファイルへの string URL、例:file:///path/to/project/index.ts。ブラウザの import.meta.url と同等 |