* Restructure * Update nav * Reorg * Reshuffle ecosystem pages * Split up runtime/runtime * Back to runtime/index * Fix issue * Split up runtime/index * Add Writing Tests page * Prettier matcher table * More updates
5.1 KiB
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.
{% codetabs %}
import { hello } from "./hello";
hello();
export function hello() {
console.log("Hello world!");
}
{% /codetabs %}
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. To resolve this import, Bun will check for the following files in order:
./hello.ts./hello.tsx./hello.js./hello.mjs./hello.cjs./hello/index.ts./hello/index.js./hello/index.json./hello/index.mjs
Import paths are case-insensitive.
import { hello } from "./hello";
import { hello } from "./HELLO";
import { hello } from "./hElLo";
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
There is one exception: 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.
{% codetabs %}
const { hello } = require("./hello");
hello();
function hello() {
console.log("Hello world!");
}
exports.hello = hello;
{% /codetabs %}
That said, using CommonJS is discouraged in new projects.
Resolution
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. Unless "type": "module" is specified, Bun assumes the package is using CommonJS and transpiles into a synchronous ES module internally. To determine the package's entrypoint, Bun first reads the exports field in and checks the following conditions in order:
{
"name": "foo",
"exports": {
"bun": "./index.js", // highest priority
"worker": "./index.js",
"module": "./index.js",
"node": "./index.js",
"browser": "./index.js",
"default": "./index.js" // lowest priority
}
}
Bun respects subpath "exports" and "imports". Specifying any subpath in the "exports" map will prevent other subpaths from being importable.
{
"name": "foo",
"exports": {
".": "./index.js",
"./package.json": "./package.json" # subpath
}
}
{% callout %}
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.
{% /callout %}
If exports is not defined, Bun falls back to "module" (ESM imports only) then "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.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.