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
匯入路徑不區分大小寫,這表示以下都是有效的匯入
import { hello } from "./hello";
import { hello } from "./HELLO";
import { hello } from "./hElLo";
匯入路徑可以選擇包含擴充名。如果存在擴充名,Bun 將只檢查具有該確切擴充名的檔案。
import { hello } from "./hello";
import { hello } from "./hello.ts"; // this works
如果你匯入 `from "*.js{x}"`,Bun 還會檢查匹配的 `*.ts{x}` 檔案,以相容於 TypeScript 的 ES 模組支援。
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。
const { hello } = require("./hello");
hello();
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 模組 | 模組命名空間 | 模組命名空間 |
CommonJS | module.exports | default 為 module.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";
同時使用 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。你無法 require()
使用頂層 await 的檔案,因為 require()
函數本質上是同步的。
幸運的是,很少有函式庫使用頂層 await,因此這很少會造成問題。但是,如果你在應用程式碼中使用頂層 await,請確保該檔案不會在應用程式的其他地方被 require()
。相反,你應該使用 import
或 動態 import()
。
匯入套件
Bun 實作 Node.js 模組解析演算法,因此你可以使用裸指定符從 node_modules
匯入套件。
import { stuff } from "foo";
此演算法的完整規格已正式記載於 Node.js 文件 中;我們在此不會重新整理。簡而言之:如果你匯入 from "foo"
,Bun 會掃描檔案系統,尋找包含套件 foo
的 node_modules
目錄。
一旦找到 foo
套件,Bun 會讀取 package.json
以確定如何匯入套件。為了確定套件的進入點,Bun 首先會讀取 exports
欄位,並檢查以下條件。
{
"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"
。
{
"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"
。
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}
路徑重新對應
為了將 TypeScript 視為一級公民,Bun 執行時期會根據 tsconfig.json
中的 compilerOptions.paths
欄位重新對應匯入路徑。這與不支援任何形式的匯入路徑重新對應的 Node.js 有很大的不同。
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // map specifier to file
"components/*": ["components/*"], // wildcard matching
}
}
}
如果你不是 TypeScript 使用者,可以在專案根目錄中建立一個 jsconfig.json
來達成相同的行為。
Bun 中 CommonJS 互通的低階細節