Open-Source Wikis

/

Bun

/

Systems

/

Module resolution

oven-sh/bun

Module resolution

src/resolver/ answers the question "given specifier X in source file Y, which file on disk implements it?" The same code is used by the runtime (when JS executes import/require) and the bundler (when scanning source files).

Files

File Purpose
src/resolver/resolver.zig The resolver entry. ~206 KB. Implements the full Node-compatible algorithm plus tsconfig paths, browser field, exports/imports, conditions, auto-install.
src/resolver/package_json.zig package.json reader. Caches parsed manifests. ~93 KB.
src/resolver/resolve_path.zig Path manipulation utilities (bun.path.*). ~73 KB. Cross-platform: .posix, .windows, .auto, .loose.
src/resolver/tsconfig_json.zig tsconfig.json reader for path mapping. ~23 KB.
src/resolver/dir_info.zig Cached metadata about a directory (does it contain package.json, node_modules/, etc.).
src/resolver/data_url.zig data: URI handling.

What it has to handle

  • Relative and absolute paths. ./foo, ../bar.ts, /abs/path.js.
  • Bare specifiers. lodash, @types/node/path. Resolved against node_modules/ walking up the directory tree.
  • Hardcoded modules. node:fs, bun:sqlite, bun:test. Short-circuited via src/bun.js/HardcodedModule.zig.
  • Conditional exports. package.json's exports field with conditions ("node", "browser", "bun", "import", "require", "types", "default").
  • Imports field. #alias-style imports defined in the importer's package.json.
  • browser field. Old-school browser package overrides.
  • main, module, exports ordering when multiple are present.
  • TypeScript path mapping. paths in tsconfig.json, plus extends chains and references.
  • Auto-install. bunfig.toml can opt into installing missing packages on demand. The resolver triggers a synchronous install (handled by src/install/) if a bare specifier doesn't resolve.
  • node_modules/.pnp.cjs-style PnP. Bun does not support Yarn PnP; it requires a real node_modules/.

High-level algorithm

graph TD
    Spec[specifier] --> Type{kind}
    Type -->|hardcoded| HC[lookup HardcodedModule]
    Type -->|relative/absolute| RP[normalise path]
    Type -->|bare| Bare[walk up node_modules]
    Bare --> PJ[read package.json]
    PJ --> Exports{has exports?}
    Exports -->|yes| Apply[apply conditions]
    Exports -->|no| Mainfield[main / module / index]
    Apply --> Try[try extensions]
    Mainfield --> Try
    RP --> Try
    Try --> Stat[bun.sys.stat]
    Stat --> Result[resolved path]

The "try extensions" phase honours the configured loaders (.ts, .tsx, .js, .jsx, .mjs, .cjs, .json, .toml, ...). The runtime's loader list is broader than Node's so import "./foo" can resolve to ./foo.ts without an extension hint.

Caching

Three cache layers:

  1. DirInfo cache. Each directory you traverse gets a cached DirInfo recording whether it has a package.json, a node_modules/, and what main/exports fields look like. This avoids re-stat'ing on every resolution.
  2. PackageJSON cache. Parsed manifests are cached by absolute path.
  3. Resolution cache. (specifier, dir) → resolved path, valid until the watcher invalidates it on file change.

The cache is cleared by the watcher (src/Watcher.zig) when relevant files (a package.json, a tsconfig.json, a directory listing) change. See Watcher & hot reload.

tsconfig.json

src/resolver/tsconfig_json.zig reads tsconfig.json, follows extends, and pre-compiles the paths map into a fast lookup. It also parses compilerOptions.baseUrl, paths, jsx, jsxImportSource, and a few non-standard fields used by Bun-flavoured projects.

The implementation is a strict subset of TypeScript's behaviour but covers everything the official compiler does at module-resolution time.

Browser, bun, and node conditions

When the bundler runs with --target=browser, conditions resolved are ["browser", "default", "import"] (or require). With --target=bun, it's ["bun", "node", "import"]. Custom conditions can be added via CLI flags (--conditions=foo,bar).

The runtime applies ["bun", "node", "default"] plus import or require depending on the call site.

Performance

Path joining is the inner loop. resolve_path.zig uses thread-local 4 KB scratch buffers; callers that need to keep a result must copy it. Windows path canonicalisation (long-path prefix, junction handling) lives here too.

The cache structure is shared across threads with a Mutex, but cache reads are lock-free for the common case.

Integration points

  • Runtime — Called from src/bun.js/ModuleLoader.zig. The result feeds JSC's module loader.
  • Bundler — Called from src/bundler/ParseTask.zig for every imported specifier. Plugin onResolve callbacks run before the resolver.
  • Bun.resolveSync — Exposes the resolver to JS via src/bun.js/api/BunObject.zig.
  • Auto-install — When enabled, an unresolved bare specifier triggers src/install/PackageManager.zig to install on demand.

Entry points for modification

  • To change condition handling, edit the applyExportsField family in resolver.zig.
  • To change extension priority, edit the loader list in src/options.zig and the extension_order arrays consulted by the resolver.
  • To change tsconfig support, edit tsconfig_json.zig.
  • To change file-system access, edit bun.sys.stat paths in resolver.zig (often the slow part on cold caches).

Key source files

File Purpose
src/resolver/resolver.zig The resolver.
src/resolver/package_json.zig Manifest reader.
src/resolver/resolve_path.zig Path manipulation.
src/resolver/tsconfig_json.zig tsconfig resolution.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Module resolution – Bun wiki | Factory