Bun

模組解析

JavaScript 中的模組解析是一個複雜的主題。

生態系統目前正處於從 CommonJS 模組轉換到原生 ES 模組長達數年的過渡期。TypeScript 強制執行其自己的一組與 ESM 不相容的匯入擴充規則。不同的建置工具支援透過不同的不相容機制重新對應路徑。

Bun 旨在提供一個一致且可預測的模組解析系統,它能正常運作。不幸的是,它仍然相當複雜。

語法

考慮以下檔案。

index.ts
hello.ts
index.ts
import { hello } from "./hello";

hello();
hello.ts
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

匯入路徑不區分大小寫,這表示以下都是有效的匯入

index.ts
import { hello } from "./hello";
import { hello } from "./HELLO";
import { hello } from "./hElLo";

匯入路徑可以選擇包含擴充名。如果存在擴充名,Bun 將只檢查具有該確切擴充名的檔案。

index.ts
import { hello } from "./hello";
import { hello } from "./hello.ts"; // this works

如果你匯入 `from "*.js{x}"`,Bun 還會檢查匹配的 `*.ts{x}` 檔案,以相容於 TypeScript 的 ES 模組支援

index.ts
import { hello } from "./hello";
import { hello } from "./hello.ts"; // this works
import { hello } from "./hello.js"; // this also works

Bun 同時支援 ES 模組(`import` / `export` 語法)和 CommonJS 模組(`require()` / `module.exports`)。以下 CommonJS 版本也適用於 Bun。

index.js
hello.js
index.js
const { hello } = require("./hello");

hello();
hello.js
function hello() {
  console.log("Hello world!");
}

exports.hello = hello;

儘管如此,不建議在新專案中使用 CommonJS。

模組系統

Bun 原生支援 CommonJS 和 ES 模組。ES 模組是新專案建議的模組格式,但 CommonJS 模組仍廣泛用於 Node.js 生態系統中。

在 Bun 的 JavaScript 執行時間中,ES 模組和 CommonJS 模組都可以使用 `require`。如果目標模組是 ES 模組,`require` 會傳回模組命名空間物件(等同於 `import * as`)。如果目標模組是 CommonJS 模組,`require` 會傳回 `module.exports` 物件(如同在 Node.js 中)。

模組類型require()import * as
ES 模組模組命名空間模組命名空間
CommonJSmodule.exportsdefaultmodule.exports,module.exports 的鍵為命名匯出

使用 require()

你可以 require() 任何檔案或套件,甚至 .ts.mjs 檔案。

const { foo } = require("./foo"); // extensions are optional
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");

什麼是 CommonJS 模組?

使用 import

你可以 import 任何檔案或套件,甚至是 .cjs 檔案。

import { foo } from "./foo"; // extensions are optional
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";

同時使用 importrequire()

在 Bun 中,你可以在同一個檔案中使用 importrequire,它們隨時都能正常運作。

import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");

頂層 await

這個規則唯一的例外是頂層 await。你無法 require() 使用頂層 await 的檔案,因為 require() 函數本質上是同步的。

幸運的是,很少有函式庫使用頂層 await,因此這很少會造成問題。但是,如果你在應用程式碼中使用頂層 await,請確保該檔案不會在應用程式的其他地方被 require()。相反,你應該使用 import動態 import()

匯入套件

Bun 實作 Node.js 模組解析演算法,因此你可以使用裸指定符從 node_modules 匯入套件。

import { stuff } from "foo";

此演算法的完整規格已正式記載於 Node.js 文件 中;我們在此不會重新整理。簡而言之:如果你匯入 from "foo",Bun 會掃描檔案系統,尋找包含套件 foonode_modules 目錄。

一旦找到 foo 套件,Bun 會讀取 package.json 以確定如何匯入套件。為了確定套件的進入點,Bun 首先會讀取 exports 欄位,並檢查以下條件。

package.json
{
  "name": "foo",
  "exports": {
    "bun": "./index.js",
    "worker": "./index.js",
    "node": "./index.js",
    "require": "./index.js", // if importer is CommonJS
    "import": "./index.mjs", // if importer is ES module
    "default": "./index.js",
  }
}

這些條件中,哪一個最先出現在 package.json 中,就會用來確定套件的進入點。

Bun 尊重子路徑 "exports""imports"

package.json
{
  "name": "foo",
  "exports": {
    ".": "./index.js"
  }
}

子路徑匯入和條件匯入會相互配合。

{
  "name": "foo",
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.js"
    }
  }
}

如同 Node.js,在 "exports" 地圖中指定任何子路徑會防止匯入其他子路徑;你只能匯入明確匯出的檔案。根據上述的 package.json

import stuff from "foo"; // this works
import stuff from "foo/index.mjs"; // this doesn't

運送 TypeScript — 請注意,Bun 支援特殊的 "bun" 匯出條件。如果你的程式庫是用 TypeScript 編寫的,你可以直接將你的(未轉譯!)TypeScript 檔案發布到 npm。如果你在 "bun" 條件中指定套件的 *.ts 進入點,Bun 會直接匯入並執行你的 TypeScript 原始碼檔案。

如果未定義 exports,Bun 會回退到 "module"(僅 ESM 匯入),然後 "main"

package.json
{
  "name": "foo",
  "module": "./index.js",
  "main": "./index.js"
}

路徑重新對應

為了將 TypeScript 視為一級公民,Bun 執行時期會根據 tsconfig.json 中的 compilerOptions.paths 欄位重新對應匯入路徑。這與不支援任何形式的匯入路徑重新對應的 Node.js 有很大的不同。

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "config": ["./config.ts"],         // map specifier to file
      "components/*": ["components/*"],  // wildcard matching
    }
  }
}

如果你不是 TypeScript 使用者,可以在專案根目錄中建立一個 jsconfig.json 來達成相同的行為。

Bun 中 CommonJS 互通的低階細節