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

如果您从 "*.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.exportsmodule.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 文档中正式记录;我们在此不再赘述。简而言之:如果你从 "foo" 导入,Bun 会向上扫描文件系统以查找包含包 foonode_modules 目录。

NODE_PATH

Bun 支持 NODE_PATH 用于额外的模块解析目录

NODE_PATH=./packages bun run src/index.js
// packages/foo/index.js
export const hello = "world";

// src/index.js
import { hello } from "foo";

多个路径使用平台的定界符(Unix 上为 :,Windows 上为 ;

NODE_PATH=./packages:./lib bun run src/index.js  # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js  # Windows

一旦找到 foo 包,Bun 会读取 package.json 以确定应如何导入该包。为了确定包的入口点,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

你也可以通过 Bun.build 以编程方式使用 conditions

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

路径重映射

本着将 TypeScript 视为一等公民的精神,Bun 运行时将根据 tsconfig.json 中的compilerOptions.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 互操作的底层细节