Skip to main content
Bun implements a sophisticated module resolution system compatible with Node.js, enhanced with additional features for modern JavaScript development.

Module Systems

Bun supports both ES Modules (ESM) and CommonJS (CJS):

ES Modules

ES Modules
// Importing
import { readFile } from "node:fs/promises";
import React from "react";
import { add } from "./math.js";

// Exporting
export function greet(name) {
  return `Hello, ${name}!`;
}

export default App;
Features:
  • Static imports and exports
  • Top-level await support
  • Tree-shaking friendly
  • Strict mode by default

CommonJS

CommonJS
// Importing
const fs = require("node:fs");
const React = require("react");
const { add } = require("./math.js");

// Exporting
module.exports = {
  greet(name) {
    return `Hello, ${name}!`;
  },
};

module.exports.default = App;
Features:
  • Dynamic require() calls
  • Synchronous loading
  • Circular dependency handling
  • exports shorthand

Interoperability

Bun seamlessly bridges ESM and CommonJS:
ESM → CommonJS
// Import CommonJS from ESM
import pkg from "./commonjs-module.js"; // module.exports
import { named } from "./commonjs-module.js"; // module.exports.named
CommonJS → ESM
// Require ESM from CommonJS (async)
const esm = await import("./esm-module.js");
Implementation: src/runtime.zig:167 - CommonJS named exports feature

Resolution Algorithm

Bun’s module resolution follows the Node.js algorithm with enhancements:

Resolution Steps

  1. Built-in modules - Check for node:* and bun:* modules
  2. Relative/absolute paths - Resolve file paths
  3. Package imports - Search node_modules
  4. Extensions - Try .ts, .tsx, .js, .jsx, .mjs, .cjs
  5. Index files - Check index.* files
  6. package.json - Resolve via exports, main, module

Relative Imports

Relative imports
import { util } from "./utils.js"; // Same directory
import { config } from "../config.js"; // Parent directory
import { helper } from "./lib/helper.js"; // Subdirectory

Absolute Imports

Absolute imports
import { db } from "/src/database.js"; // From root
import data from "file:///home/user/data.json"; // file:// URL

Package Imports

Package imports
import React from "react"; // node_modules/react
import { parse } from "@babel/parser"; // Scoped package
import "./styles.css"; // Side-effect import

Extension Resolution

Bun tries extensions in this order:
  1. Exact match (if extension provided)
  2. .ts
  3. .tsx
  4. .js
  5. .jsx
  6. .mjs
  7. .cjs
  8. .json
Extension resolution
// All resolve to math.ts if it exists:
import { add } from "./math.ts"; // Explicit
import { add } from "./math"; // Implicit
Explicit extensions recommended for clarity and performance.

package.json Fields

Bun respects multiple package.json fields:

exports Field

Modern package entry points:
package.json
{
  "name": "my-package",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}
Import usage:
import pkg from "my-package"; // Uses exports["."]
import { util } from "my-package/utils"; // Uses exports["./utils"]

Conditional Exports

Conditional exports
{
  "exports": {
    ".": {
      "bun": "./dist/bun.js",
      "node": "./dist/node.js",
      "browser": "./dist/browser.js",
      "default": "./dist/index.js"
    }
  }
}
Condition priority (Bun target):
  1. bun
  2. node
  3. import / require
  4. default
Implementation: src/options.zig:508-533 - Default conditions per target

main, module, browser

Legacy entry point fields:
Legacy fields
{
  "main": "./dist/index.js",      // CommonJS entry
  "module": "./dist/index.mjs",   // ESM entry
  "browser": "./dist/browser.js", // Browser entry
  "types": "./dist/index.d.ts"    // TypeScript types
}
Resolution priority:
  1. exports (if present)
  2. module
  3. main
  4. index.js
Implementation: src/options.zig:455-505 - Main field resolution

type Field

Determines module system:
package.json
{
  "type": "module" // All .js files are ESM
}
package.json
{
  "type": "commonjs" // All .js files are CommonJS (default)
}
Overrides:
  • .mjs always ESM
  • .cjs always CommonJS

Path Mapping (tsconfig.json)

Configure import aliases:
tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@lib/*": ["./src/lib/*"],
      "~/*": ["./*"]
    }
  }
}
Using path aliases
import { Button } from "@components/Button";
import { api } from "@lib/api";
import config from "@/config";
import data from "~/data/users.json";
Implementation: src/resolver/resolver.zig - Path resolution

Built-in Modules

Bun provides built-in modules:

Node.js Compatibility

Node.js modules
import fs from "node:fs";
import path from "node:path";
import { EventEmitter } from "node:events";
import { createServer } from "node:http";
All Node.js built-ins available with node: prefix. Implementation: src/options.zig:150-333 - Node.js built-in patterns

Bun-specific Modules

Bun modules
import { serve } from "bun";
import { Database } from "bun:sqlite";
import { dlopen, FFIType } from "bun:ffi";
import { password } from "bun:password";
import { spawn } from "bun:spawn";
Available modules:
  • bun - Main Bun APIs
  • bun:ffi - Foreign Function Interface
  • bun:sqlite - SQLite database
  • bun:test - Test runner
  • bun:jsc - JavaScriptCore internals
  • bun:wrap - Internal runtime

Import Attributes

Specify how to import modules:
Import attributes
// Import as JSON
import data from "./data.json" with { type: "json" };

// Import as text
import content from "./file.txt" with { type: "text" };

// Import assertions (older syntax)
import config from "./config.json" assert { type: "json" };
Supported types:
  • json - Parse as JSON
  • text - Load as string
  • file - Copy to output
  • toml - Parse as TOML

Dynamic Imports

Load modules at runtime:
Dynamic imports
// ESM dynamic import
const module = await import("./module.js");

// Conditional loading
if (isDevelopment) {
  const dev = await import("./dev-tools.js");
  dev.init();
}

// Dynamic path
const locale = "en";
const messages = await import(`./locales/${locale}.js`);
Benefits:
  • Code splitting
  • Lazy loading
  • Conditional imports
  • String-based paths

Circular Dependencies

Bun handles circular dependencies:
Circular dependencies
// a.js
import { b } from "./b.js";
export const a = "A";
console.log(b); // "B"

// b.js
import { a } from "./a.js";
export const b = "B";
console.log(a); // undefined (not yet initialized)
Best practice: Avoid circular dependencies when possible.

Import Maps

Map bare specifiers to URLs:
import-map.json
{
  "imports": {
    "react": "https://esm.sh/react@18",
    "lodash": "https://esm.sh/lodash@4"
  }
}
Using import maps
import React from "react"; // Loads from esm.sh
import _ from "lodash";
Import maps are experimental and primarily for browser compatibility.

Resolution Cache

Bun caches module resolution results: Cache location:
  • Resolution cache: In-memory
  • Transpiler cache: Disk (see TypeScript)
Cache invalidation:
  • File system changes (watch mode)
  • package.json modifications
  • node_modules updates

Module Loader Hooks

Customize module loading (advanced):
Module hooks
import { plugin } from "bun";

plugin({
  name: "custom-loader",
  setup(build) {
    // Custom resolution
    build.onResolve({ filter: /^@app/ }, (args) => {
      return {
        path: args.path.replace("@app", "./src"),
      };
    });

    // Custom loading
    build.onLoad({ filter: /\.custom$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(text)}`,
        loader: "js",
      };
    });
  },
});

Preloading

Load modules before main script:
Preload flag
bun --preload ./setup.ts run app.ts
bunfig.toml
preload = ["./setup.ts", "./polyfills.ts"]
Use cases:
  • Environment setup
  • Polyfills
  • Global initialization
  • Instrumentation

Performance

Optimization Tips

  1. Use explicit extensions
    import { util } from "./util.ts"; // Fast
    import { util } from "./util"; // Slower (tries multiple extensions)
    
  2. Prefer ESM over CommonJS
    • Static analysis
    • Tree-shaking
    • Async loading
  3. Use barrel files sparingly
    // Slow (loads entire barrel)
    import { Button } from "./components/index.js";
    
    // Fast (direct import)
    import { Button } from "./components/Button.js";
    
  4. Leverage dynamic imports for code splitting
    const Chart = await import("./Chart.js");
    

Debugging

Trace Resolution

Debug resolution
BUN_DEBUG_QUIET_LOGS=0 bun run app.ts 2>&1 | grep -i resolve
Print resolution
console.log(import.meta.resolveSync("react"));
// "/path/to/node_modules/react/index.js"

Resolution Failures

Common errors
# Module not found
error: Cannot find module "./missing.js"

# Check if file exists
ls -la ./missing.js

# Check extensions
ls -la ./missing.*

Implementation Details

Module resolution implementation:
  • Resolver: src/resolver/resolver.zig - Core resolution logic
  • Module loader: src/bun.js/module_loader/ - Module loading
  • Import record: src/import_record.zig - Import tracking
  • Package.json: src/resolver/package_json.zig - Package metadata

Build docs developers (and LLMs) love