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 againstnode_modules/walking up the directory tree. - Hardcoded modules.
node:fs,bun:sqlite,bun:test. Short-circuited viasrc/bun.js/HardcodedModule.zig. - Conditional exports.
package.json'sexportsfield with conditions ("node","browser","bun","import","require","types","default"). - Imports field.
#alias-style imports defined in the importer'spackage.json. browserfield. Old-school browser package overrides.main,module,exportsordering when multiple are present.- TypeScript path mapping.
pathsintsconfig.json, plusextendschains andreferences. - Auto-install.
bunfig.tomlcan opt into installing missing packages on demand. The resolver triggers a synchronous install (handled bysrc/install/) if a bare specifier doesn't resolve. node_modules/.pnp.cjs-style PnP. Bun does not support Yarn PnP; it requires a realnode_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:
DirInfocache. Each directory you traverse gets a cachedDirInforecording whether it has apackage.json, anode_modules/, and whatmain/exportsfields look like. This avoids re-stat'ing on every resolution.PackageJSONcache. Parsed manifests are cached by absolute path.- 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.zigfor every imported specifier. PluginonResolvecallbacks run before the resolver. Bun.resolveSync— Exposes the resolver to JS viasrc/bun.js/api/BunObject.zig.- Auto-install — When enabled, an unresolved bare specifier triggers
src/install/PackageManager.zigto install on demand.
Entry points for modification
- To change condition handling, edit the
applyExportsFieldfamily inresolver.zig. - To change extension priority, edit the loader list in
src/options.zigand theextension_orderarrays consulted by the resolver. - To change tsconfig support, edit
tsconfig_json.zig. - To change file-system access, edit
bun.sys.statpaths inresolver.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.