| name | bun-workspaces |
| description | Bun monorepo workspaces - configuration, filtering, dependencies, and multi-package management
When user works with Bun monorepos, workspaces, multi-package projects, or mentions bun --filter
|
Bun Workspaces Agent
What's New in Bun Workspaces (2024-2025)
- Text-based lockfile:
bun.lock replaces binary bun.lockb (v1.2+)
- Catalogs: Define shared dependency versions once in root package.json
- Dependency-aware filtering: Scripts run in dependency order
- Glob patterns: Full glob syntax with negative patterns
- Automatic lockfile migration: From npm, yarn, pnpm
Performance
Bun workspaces are significantly faster than alternatives:
- 28x faster than npm install
- 12x faster than yarn install (v1)
- 8x faster than pnpm install
Monorepo Structure
my-monorepo/
├── package.json # Root config with workspaces field
├── bun.lock # Single lockfile for all packages
├── bunfig.toml # Optional Bun configuration
├── tsconfig.json # Shared TypeScript config
└── packages/
├── shared/
│ ├── package.json
│ ├── index.ts
│ └── tsconfig.json
├── backend/
│ ├── package.json
│ ├── src/
│ └── tsconfig.json
└── frontend/
├── package.json
├── src/
└── tsconfig.json
Root package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"dev": "bun --filter '*' dev",
"build": "bun --filter '*' build",
"test": "bun --filter '*' test",
"typecheck": "bun --filter '*' typecheck"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
Fan-out cost — scope scripts to the packages you touched
Root scripts like the above fan out over every workspace package, each booting its own node/bun toolchain (tsc, eslint, test runners — ~100 MB+ each). In a large monorepo (~35 packages in shepherdjerred/monorepo), multiple concurrent sessions each running a root-level typecheck/build/test has frozen the machine (6,000+ processes, 20-30 GB of anonymous memory within seconds → macOS jetsam freeze; see packages/docs/logs/2026-07-11_macbook-hang-jetsam-investigation.md). Default to scoping: cd packages/<name> && bun run <script> (or bun run --filter='./packages/<name>' <script> if the package is registered as a Bun workspace from the repo root — the cd form is reliable regardless). Reserve repo-wide runs for genuinely repo-wide changes, one at a time machine-wide; CI does the exhaustive pass.
Glob Patterns
{
"workspaces": [
"packages/*",
"apps/*",
"packages/**",
"!packages/**/test/**",
"!packages/deprecated"
]
}
Workspace Protocol
Reference workspace packages in dependencies:
{
"name": "@myorg/backend",
"dependencies": {
"@myorg/shared": "workspace:*"
}
}
Protocol Variants
| Syntax | Meaning | Published As |
|---|
workspace:* | Any version | "1.0.0" (actual version) |
workspace:^ | Caret range | "^1.0.0" |
workspace:~ | Tilde range | "~1.0.0" |
workspace:1.0.2 | Exact version | "1.0.2" |
When publishing, workspace: references are replaced with actual versions.
Catalogs
Define shared dependency versions once:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"],
"catalog": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.0",
"zod": "^3.22.0"
}
}
Using Catalog in Workspace
{
"name": "@myorg/frontend",
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:",
"zod": "catalog:"
}
}
Named Catalogs
{
"catalogs": {
"default": {
"typescript": "^5.0.0"
},
"testing": {
"vitest": "^1.0.0",
"playwright": "^1.40.0"
}
}
}
{
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:testing"
}
}
Commands
Installing Dependencies
bun install
bun install --frozen-lockfile
bun install --lockfile-only
bun install --filter "pkg-*"
bun install --filter "!pkg-c"
Adding Dependencies
cd packages/backend
bun add express
bun add -d typescript
bun add -E zod@3.22.0
bun add lodash --filter "@myorg/shared"
Running Scripts
bun --filter '*' dev
bun --filter '@myorg/backend' dev
bun --filter 'pkg-*' build
bun --filter '*' --filter '!@myorg/frontend' test
bun --filter './packages/backend' dev
Script Dependency Order
When packages depend on each other, scripts run in dependency order:
# If @myorg/backend depends on @myorg/shared:
bun --filter '*' build
# Runs: shared build → backend build
Other Commands
bun outdated
bun outdated --filter 'pkg-*'
bun update
bun remove lodash
bun pm ls
bun pm ls --all
Shared Configuration
TypeScript (tsconfig.json)
Root tsconfig.json:
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"composite": true,
"paths": {
"@myorg/*": ["./packages/*/src"]
}
}
}
Workspace tsconfig.json:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [{ "path": "../shared" }]
}
ESLint (eslint.config.js)
import baseConfig from "@myorg/eslint-config";
export default [
...baseConfig,
{
ignores: ["**/dist/**", "**/node_modules/**"],
},
];
bunfig.toml Configuration
[install]
auto = true
lifecycle = ["postinstall"]
[install.lockfile]
save = true
print = "yarn"
[install.scopes]
"@myorg" = { token = "$NPM_TOKEN", url = "https://npm.pkg.github.com" }
Common Patterns
Shared Library Package
{
"name": "@myorg/shared",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.js",
"types": "./dist/utils.d.ts"
}
},
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}
Internal Dependencies
import { validateUser, UserSchema } from "@myorg/shared";
import type { User } from "@myorg/shared/types";
const user = validateUser(data);
Running Type Checks
{
"scripts": {
"typecheck": "tsc --noEmit -p tsconfig.json",
"typecheck:all": "bun --filter '*' typecheck"
}
}
Lockfile Management
Text-based Lockfile (v1.2+)
bun install --save-text-lockfile --frozen-lockfile --lockfile-only
rm bun.lockb
Automatic Migration
Bun automatically migrates from:
package-lock.json (npm)
yarn.lock (Yarn v1)
pnpm-lock.yaml (pnpm)
bun install
Dependency Overrides
Force specific versions for transitive dependencies:
{
"overrides": {
"lodash": "4.17.21",
"axios": "^1.6.0"
}
}
Or Yarn-style:
{
"resolutions": {
"lodash": "4.17.21"
}
}
CI/CD Integration
GitHub Actions
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun --filter '*' typecheck
- run: bun --filter '*' build
- run: bun --filter '*' test
Docker
FROM oven/bun:1
WORKDIR /app
# Copy lockfile first for caching
COPY bun.lock package.json ./
COPY packages/*/package.json ./packages/
RUN bun install --frozen-lockfile
COPY . .
RUN bun --filter '*' build
Best Practices Summary
- Use
private: true on root package.json
- Keep root dependencies minimal - only shared dev tools
- Use workspace protocol (
workspace:*) for internal deps
- Use catalogs for consistent versions across packages
- Run
--frozen-lockfile in CI
- Commit
bun.lock to version control
- Use glob negation to exclude test/template directories
- Run scripts in parallel with
bun --filter '*'
- Configure TypeScript paths for seamless imports
- Build dependencies first - Bun handles order automatically
When to Ask for Help
- Complex publishing workflows to npm
- Custom registry authentication
- Peer dependency conflicts
- Selective version updates in catalogs
- Integration with Turborepo or Nx
- Workspace-specific bunfig.toml overrides