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

匯入路徑可以選擇性地包含擴展名。如果存在擴展名,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() 引入。相反地,您應該使用 importdynamic import()

匯入套件

Bun 實作了 Node.js 的模組解析演算法,因此您可以使用裸路徑 (bare specifier) 從 node_modules 匯入套件。

import { stuff } from "foo";

此演算法的完整規範已正式記錄在 Node.js 文件 中;我們在此不再贅述。簡而言之:如果您從 "foo" 匯入,Bun 會向上掃描檔案系統,尋找包含套件 foonode_modules 目錄。

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

package.json
{
  "name": "foo",
  "exports": {
    "bun": "./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"
}

自訂條件

--conditions 標記允許您指定條件列表,以便在從 package.json "exports" 解析套件時使用。

bun build 和 Bun 的執行時環境都支援此標記。

# Use it with bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js

# Use it with bun's runtime:
bun --conditions="react-server" ./app/foo/route.js

您也可以透過程式設計方式將 conditionsBun.build 一起使用

await Bun.build({
  conditions: ["react-server"],
  target: "bun",
  entryPoints: ["./app/foo/route.js"],
});

路徑重新對應

本著將 TypeScript 視為一等公民的精神,Bun 執行時環境將根據 tsconfig.jsoncompilerOptions.paths 欄位重新對應匯入路徑。這與 Node.js 截然不同,Node.js 不支援任何形式的匯入路徑重新對應。

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

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

Bun 中 CommonJS 互操作的底層細節