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 运行时中,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()
你可以 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 文档 中正式记录;我们不会在此重新整理。简而言之:如果你从 "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 的一个重大分歧,Node.js 不支持任何形式的导入路径重新映射。
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // map specifier to file
"components/*": ["components/*"], // wildcard matching
}
}
}
如果你不是 TypeScript 用户,可以在项目根目录中创建一个 jsconfig.json
来实现相同的效果。
Bun 中 CommonJS 互操作的底层细节