Module resolution in JavaScript is a complex topic.
The ecosystem is currently in the midst of a years-long transition from CommonJS modules to native ES modules. TypeScript enforces its own set of rules around import extensions that aren't compatible with ESM. Different build tools support path re-mapping via disparate non-compatible mechanisms.
Bun aims to provide a consistent and predictable module resolution system that just works. Unfortunately it's still quite complex.
Syntax
Consider the following files.
import { hello } from "./hello";
hello();
export function hello() {
console.log("Hello world!");
}
When we run index.ts
, it prints "Hello world!".
bun index.ts
Hello world!
In this case, we are importing from ./hello
, a relative path with no extension. Extensioned imports are optional but supported. To resolve this import, Bun will check for the following files in order:
./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 paths can optionally include extensions. If an extension is present, Bun will only check for a file with that exact extension.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // this works
If you import from "*.js{x}"
, Bun will additionally check for a matching *.ts{x}
file, to be compatible with TypeScript's ES module support.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // this works
import { hello } from "./hello.js"; // this also works
Bun supports both ES modules (import
/export
syntax) and CommonJS modules (require()
/module.exports
). The following CommonJS version would also work in Bun.
const { hello } = require("./hello");
hello();
function hello() {
console.log("Hello world!");
}
exports.hello = hello;
That said, using CommonJS is discouraged in new projects.
Module systems
Bun has native support for CommonJS and ES modules. ES Modules are the recommended module format for new projects, but CommonJS modules are still widely used in the Node.js ecosystem.
In Bun's JavaScript runtime, require
can be used by both ES Modules and CommonJS modules. If the target module is an ES Module, require
returns the module namespace object (equivalent to import * as
). If the target module is a CommonJS module, require
returns the module.exports
object (as in Node.js).
Module Type | require() | import * as |
---|---|---|
ES Module | Module Namespace | Module Namespace |
CommonJS | module.exports | default is module.exports , keys of module.exports are named exports |
Using require()
You can require()
any file or package, even .ts
or .mjs
files.
const { foo } = require("./foo"); // extensions are optional
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");
What is a CommonJS module?
Using import
You can import
any file or package, even .cjs
files.
import { foo } from "./foo"; // extensions are optional
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";
Using import
and require()
together
In Bun, you can use import
or require
in the same file—they both work, all the time.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");
Top level await
The only exception to this rule is top-level await. You can't require()
a file that uses top-level await, since the require()
function is inherently synchronous.
Fortunately, very few libraries use top-level await, so this is rarely a problem. But if you're using top-level await in your application code, make sure that file isn't being require()
from elsewhere in your application. Instead, you should use import
or dynamic import()
.
Importing packages
Bun implements the Node.js module resolution algorithm, so you can import packages from node_modules
with a bare specifier.
import { stuff } from "foo";
The full specification of this algorithm are officially documented in the Node.js documentation; we won't rehash it here. Briefly: if you import from "foo"
, Bun scans up the file system for a node_modules
directory containing the package foo
.
Once it finds the foo
package, Bun reads the package.json
to determine how the package should be imported. To determine the package's entrypoint, Bun first reads the exports
field and checks for the following conditions.
{
"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",
}
}
Whichever one of these conditions occurs first in the package.json
is used to determine the package's entrypoint.
Bun respects subpath "exports"
and "imports"
.
{
"name": "foo",
"exports": {
".": "./index.js"
}
}
Subpath imports and conditional imports work in conjunction with each other.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}
As in Node.js, Specifying any subpath in the "exports"
map will prevent other subpaths from being importable; you can only import files that are explicitly exported. Given the package.json
above:
import stuff from "foo"; // this works
import stuff from "foo/index.mjs"; // this doesn't
Shipping TypeScript — Note that Bun supports the special "bun"
export condition. If your library is written in TypeScript, you can publish your (un-transpiled!) TypeScript files to npm
directly. If you specify your package's *.ts
entrypoint in the "bun"
condition, Bun will directly import and execute your TypeScript source files.
If exports
is not defined, Bun falls back to "module"
(ESM imports only) then "main"
.
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}
Custom conditions
The --conditions
flag allows you to specify a list of conditions to use when resolving packages from package.json "exports"
.
This flag is supported in both bun build
and Bun's runtime.
# 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
You can also use conditions
programmatically with Bun.build
:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});
Path re-mapping
In the spirit of treating TypeScript as a first-class citizen, the Bun runtime will re-map import paths according to the compilerOptions.paths
field in tsconfig.json
. This is a major divergence from Node.js, which doesn't support any form of import path re-mapping.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // map specifier to file
"components/*": ["components/*"], // wildcard matching
}
}
}
If you aren't a TypeScript user, you can create a jsconfig.json
in your project root to achieve the same behavior.
Low-level details of CommonJS interop in Bun