oven-sh/bun
Package manager
Active contributors: Jarred Sumner, Dylan Conway, Alistair Smith
bun install, bun add, bun remove, bun update, bun pm, bun outdated, bun audit, bun publish, bun pack, bun why, bun link/unlink, bun patch and bun patch-commit all share one implementation in src/install/. The package manager is a Zig-only subsystem — it does not start a JS VM unless lifecycle scripts run.
Purpose
A drop-in replacement for npm/yarn/pnpm that:
- Resolves dependency graphs from
package.jsonand writes a binary or text lockfile (bun.lock). - Downloads tarballs concurrently from the npm registry (or git, GitHub, tarball URLs, workspace, file paths).
- Materializes
node_modules/either flat (npm-compatible) or symlinked (pnpm-style isolated install). - Runs lifecycle scripts (
preinstall,install,postinstall,prepublish, ...) gated by a trusted-dependencies allowlist. - Patches packages via
bun patchand applies the patches on subsequent installs.
Directory layout
src/install/
├── PackageManager.zig # the orchestrator, ~55 KB
├── PackageInstall.zig # extract a single tarball into node_modules
├── PackageInstaller.zig # ~73 KB — drives many PackageInstalls in parallel
├── PackageManagerTask.zig # work queue items
├── PackageManagerCommand/ # bun pm subcommands (ls, hash, migrate, ...)
├── PackageManifestMap.zig # in-memory manifest cache
├── NetworkTask.zig # HTTP request abstraction over HTTPThread
├── npm.zig # npm registry client (~133 KB)
├── lockfile.zig # binary + text lockfile reader/writer (~82 KB)
├── lockfile/ # versioned lockfile decoders
├── migration.zig # convert package-lock.json / yarn.lock → bun.lock
├── hoisted_install.zig # npm-style flat node_modules
├── isolated_install.zig # pnpm-style symlinked install (~98 KB)
├── isolated_install/ # additional isolated-install helpers
├── lifecycle_script_runner.zig # spawn pre/post scripts
├── extract_tarball.zig # libarchive wrapper
├── TarballStream.zig # streaming tarball decoder
├── repository.zig # git repository handling
├── hosted_git_info.zig # github:/gitlab:/bitbucket: shorthand parser
├── pnpm.zig # parse pnpm-lock.yaml for migration
├── yarn.zig # parse yarn.lock (v1 + v2)
├── patch.zig (in src/) # patch-file format
├── patch_install.zig # apply patches on install
├── postinstall_optimizer.zig # skip useless prebuilds
├── default-trusted-dependencies.txt # default allowlist
├── PnpmMatcher.zig # version range matching
├── audit_command.zig (in cli/) # bun audit
├── outdated_command.zig (..) # bun outdated
├── pm_view_command.zig (..) # bun pm view (npm view)
├── update_interactive_command.zig (..) # bun update --interactive
├── publish_command.zig (..) # bun publish
└── pack_command.zig (..) # bun packThe src/cli/*_command.zig files are thin wrappers that parse argv and call into PackageManager.zig.
Key abstractions
| Type | File | Purpose |
|---|---|---|
PackageManager |
src/install/PackageManager.zig |
The root struct. Owns the resolved graph, the lockfile, the network task queue, the install queue, and progress reporting. |
Lockfile |
src/install/lockfile.zig |
The deterministic dependency graph. Two on-disk formats: bun.lockb (binary, legacy) and bun.lock (text v1, default). |
Resolution |
src/install/resolution.zig |
A tagged union for "where does this package come from": npm registry, git URL, github shorthand, workspace, file path, tarball, ... |
Dependency |
src/install/dependency.zig |
A constraint from a package.json (^1.2.3, git+https://..., workspace:*, npm:foo@1). |
PackageInstall |
src/install/PackageInstall.zig |
One tarball → one folder in node_modules. Handles extraction, file moves, and symlink layout. |
PackageInstaller |
src/install/PackageInstaller.zig |
The fan-out controller that drives many PackageInstalls on the thread pool. |
NetworkTask |
src/install/NetworkTask.zig |
An async HTTP request (manifest fetch, tarball download). Submitted to HTTPThread.zig. |
Npm.PackageManifest |
src/install/npm.zig |
Parsed npm view-like manifest. Cached on disk under ~/.bun/install/cache/. |
LifecycleScriptSubprocess |
src/install/lifecycle_script_runner.zig |
Spawns bun run <script> in the package directory. |
How bun install works
sequenceDiagram
participant CLI as src/cli/install_command.zig
participant PM as PackageManager.zig
participant Lock as Lockfile
participant Res as Resolver loop
participant HTTP as HTTPThread
participant FS as PackageInstaller
CLI->>PM: PackageManager.init
PM->>Lock: load bun.lock (or migrate from npm/yarn/pnpm)
PM->>Res: enqueue root manifest
loop until graph stable
Res->>HTTP: GET manifest tarball
HTTP-->>Res: bytes
Res->>Res: parse, expand transitive deps
Res->>Lock: insert
end
PM->>Lock: write bun.lock(b)
PM->>FS: install (hoisted_install or isolated_install)
FS->>HTTP: download tarballs in parallel
FS->>FS: extract via libarchive
FS->>FS: link bins, write .modules.yaml (isolated)
PM->>PM: run lifecycle scripts (filtered by trusted-deps)
PM-->>CLI: exitThe graph resolver runs lockstep with the network: each manifest fetch returns a list of dependencies; each new dependency may need its own manifest. The same HTTPThread (src/http/HTTPThread.zig) used by fetch() powers these requests, so connection pooling and HTTP/2 multiplexing apply.
Hoisted vs isolated installs
Bun supports two node_modules/ layouts. The choice is per-project, configured in bunfig.toml:
[install]
linker = "hoisted" # default — npm-compatible flat tree
# linker = "isolated" # pnpm-style symlinks| Aspect | Hoisted (hoisted_install.zig) |
Isolated (isolated_install.zig) |
|---|---|---|
| Layout | Flat node_modules/<pkg> with peer-deps hoisted |
Symlinks under node_modules/<pkg> → node_modules/.bun/<pkg>@<version>/node_modules/<pkg> |
| Phantom deps | Possible | Not possible — only declared deps are reachable |
| Disk usage | Higher (duplication) | Lower (content-addressed store) |
| Compatibility | npm/yarn parity | pnpm-style |
| File ops | mkdir + cp | mkdir + symlink (junctions on Windows) |
The isolated layout uses a .modules.yaml file in node_modules/.bun/ to track the exact graph for incremental updates.
Lockfile
bun.lock is the new default text format (a JSON-ish, sorted, stable representation). The legacy binary bun.lockb is still supported. The reader/writer is in src/install/lockfile.zig plus per-version decoders under src/install/lockfile/.
Migration from package-lock.json, yarn.lock (v1 + v2), and pnpm-lock.yaml runs automatically the first time bun install is invoked in a project. The parsers live in:
| Source format | Parser |
|---|---|
package-lock.json |
src/install/migration.zig |
yarn.lock (v1) |
src/install/yarn.zig |
yarn.lock (Berry) |
src/install/yarn.zig |
pnpm-lock.yaml |
src/install/pnpm.zig |
Network: registry and tarballs
npm.zig issues GET /<package> against the configured registry (default https://registry.npmjs.org/). Auth tokens come from .npmrc (parsed by src/install/npmrc.zig style helpers) and bunfig.toml. Per-scope registries are resolved via npmrc.
Tarballs are downloaded by NetworkTask, decoded streamingly by TarballStream.zig (using vendored libarchive), and extracted into the global cache under ~/.bun/install/cache/<pkg>@<version>/. The cache is shared across projects.
integrity.zig enforces the integrity field from the lockfile (sha512 by default) against the downloaded bytes.
Lifecycle scripts
After install, lifecycle_script_runner.zig walks the install order and executes preinstall / install / postinstall / prepublish / preprepare / prepare for each package. Scripts are gated:
- A package whose name is in
default-trusted-dependencies.txt(or intrustedDependenciesin the user'spackage.json) runs scripts unconditionally. - All others are skipped with a "Blocked: did not run install script for X" notice.
bun pm trust <pkg>adds an entry to the user'spackage.json.
postinstall_optimizer.zig recognises common prebuild scripts (e.g. node-gyp rebuild) and skips them when a prebuilt binary already shipped in the tarball.
Patches
bun patch <pkg> extracts a fresh copy of the package into node_modules/.bun-patches/<pkg>/ and prints the path. After editing, bun patch-commit <path> produces a unified diff and saves it as patches/<pkg>@<version>.patch. On subsequent installs, patch_install.zig reads patches/ and applies them after extraction. The set of applied patches is recorded in the lockfile so downstream consumers re-apply them too.
Workspaces and filters
A workspace package is matched by workspace:* / workspace:^ / workspace:~ in the dependent's package.json. The resolver short-circuits to the local path. Workspace globs (e.g. "workspaces": ["packages/*"]) are expanded by walking the file system once at startup.
bun --filter <pattern> <cmd> runs <cmd> across matching workspaces. Implementation: src/cli/filter_arg.zig + src/cli/filter_run.zig + src/cli/multi_run.zig.
Other commands
| Command | Implementation |
|---|---|
bun pm ls / pack / bin / cache / migrate / whoami |
src/cli/package_manager_command.zig + src/cli/pm_pkg_command.zig |
bun pm trust / untrust |
src/cli/pm_trusted_command.zig |
bun pm version |
src/cli/pm_version_command.zig |
bun pm view (npm view) |
src/cli/pm_view_command.zig |
bun pm why |
src/cli/pm_why_command.zig and src/cli/why_command.zig |
bun outdated |
src/cli/outdated_command.zig |
bun update --interactive |
src/cli/update_interactive_command.zig |
bun audit |
src/cli/audit_command.zig |
bun publish |
src/cli/publish_command.zig |
bun pack |
src/cli/pack_command.zig |
bun link / unlink |
src/cli/link_command.zig / src/cli/unlink_command.zig |
bun patch / patch-commit |
src/cli/patch_command.zig / src/cli/patch_commit_command.zig |
Integration points
- HTTP — All registry traffic goes through
src/http/HTTPThread.zig. See HTTP stack. - JSON parser —
src/json_parser.zigreads everypackage.json. - Resolver —
src/install/resolution.zigfor dependency resolution;src/resolver/resolver.zigseparately handles module resolution at runtime. - bunx —
bunxcalls intoPackageManagerto install packages on demand. See bunx. - Test runner —
bun testdoes not invoke the package manager except viabun install --filter.
Entry points for modification
- To change install layout, edit
hoisted_install.zigorisolated_install.zig. Both producePackageInstalljobs that the samePackageInstallerconsumes. - To add a new lockfile format, look at
src/install/lockfile/for the versioned decoders. Tag a new version inlockfile.zigand write the upgrade path inmigration.zig. - To support a new dependency source (e.g. a custom protocol), extend the
Resolutionunion inresolution.zigand the matching parser independency.zig. - To gate a new pre/post script behaviour, edit
lifecycle_script_runner.ziganddefault-trusted-dependencies.txt.
Key source files
| File | Purpose |
|---|---|
src/install/PackageManager.zig |
Orchestrator. |
src/install/lockfile.zig |
bun.lock(b) read/write. |
src/install/npm.zig |
Registry client + manifest cache. |
src/install/PackageInstaller.zig |
Parallel install driver. |
src/install/hoisted_install.zig |
npm-compatible install layout. |
src/install/isolated_install.zig |
pnpm-style install layout. |
src/install/lifecycle_script_runner.zig |
pre/post scripts. |
src/install/migration.zig |
Migrate npm/yarn lockfiles. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.