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"; // 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
或 dynamic import()
。
匯入套件
Bun 實作了 Node.js 的模組解析演算法,因此您可以使用裸路徑 (bare specifier) 從 node_modules
匯入套件。
import { stuff } from "foo";
此演算法的完整規範已正式記錄在 Node.js 文件 中;我們在此不再贅述。簡而言之:如果您從 "foo"
匯入,Bun 會向上掃描檔案系統,尋找包含套件 foo
的 node_modules
目錄。
找到 foo
套件後,Bun 會讀取 package.json
以判斷套件應如何匯入。為了確定套件的進入點 (entrypoint),Bun 首先會讀取 exports
欄位並檢查以下條件。
{
"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"
。
{
"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"
}
自訂條件
--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
您也可以透過程式設計方式將 conditions
與 Bun.build
一起使用
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});
路徑重新對應
本著將 TypeScript 視為一等公民的精神,Bun 執行時環境將根據 tsconfig.json
中 compilerOptions.paths
欄位重新對應匯入路徑。這與 Node.js 截然不同,Node.js 不支援任何形式的匯入路徑重新對應。
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // map specifier to file
"components/*": ["components/*"], // wildcard matching
}
}
}
如果您不是 TypeScript 使用者,您可以在專案根目錄中建立 jsconfig.json
以達到相同的行為。
Bun 中 CommonJS 互操作的底層細節