Workspaces

Develop complex monorepos with multiple independent packages

Bun supports workspaces in package.json. With workspaces, you develop several independent packages in a single repository, a monorepo.

A monorepo commonly has this structure:

File Tree
<root>
├── README.md
├── bun.lock
├── package.json
├── tsconfig.json
└── packages
    ├── pkg-a
    │   ├── index.ts
    │   ├── package.json
    │   └── tsconfig.json
    ├── pkg-b
    │   ├── index.ts
    │   ├── package.json
    │   └── tsconfig.json
    └── pkg-c
        ├── index.ts
        ├── package.json
        └── tsconfig.json

The "workspaces" key in the root package.json lists the subdirectories to treat as workspaces. By convention, they live in a directory called packages.

package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "workspaces": ["packages/*"],
  "devDependencies": {
    "example-package-in-monorepo": "workspace:*"
  }
}

Glob support — Bun supports full glob syntax in "workspaces", including negative patterns such as !**/excluded/**. See supported glob patterns.

package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "workspaces": ["packages/**", "!packages/**/test/**", "!packages/**/template/**"]
}

Each workspace has its own package.json. To reference another package in the monorepo, use a semver range or the workspace protocol (for example workspace:*) as the version in your package.json.

packages/pkg-a/package.json
{
  "name": "pkg-a",
  "version": "1.0.0",
  "dependencies": {
    "pkg-b": "workspace:*"
  }
}

bun install installs dependencies for all workspaces in the monorepo, de-duplicating packages if possible. To install dependencies for specific workspaces only, use the --filter flag.

# Install dependencies for all workspaces starting with `pkg-` except for `pkg-c`
bun install --filter "pkg-*" --filter "!pkg-c"

# Paths can also be used. This is equivalent to the command above.
bun install --filter "./packages/pkg-*" --filter "!pkg-c" # or --filter "!./packages/pkg-c"

When publishing, Bun replaces workspace: versions with the package's package.json version:

"workspace:*" -> "1.0.1"
"workspace:^" -> "^1.0.1"
"workspace:~" -> "~1.0.1"

A specific version takes precedence over the package's package.json version:

"workspace:1.0.2" -> "1.0.2" // Even if current version is 1.0.1

Workspaces have a few major benefits.

  • Split code into logical parts. If one package relies on another, add it as a dependency in package.json. If package b depends on a, bun install installs your local packages/a directory into node_modules instead of downloading it from the npm registry.
  • Bun can de-duplicate dependencies. If a and b share a common dependency, Bun hoists it to the root node_modules directory. This saves disk space and minimizes the "dependency hell" of multiple versions of a package installed at once.
  • Run scripts in multiple packages. Use the --filter flag to run package.json scripts in several packages at once, or --workspaces to run scripts across all workspaces.

Self-contained workspaces#

With the hoisted linker, dependencies shared by several workspaces are hoisted to the root node_modules. Some tools cannot follow that: Electron packagers and serverless bundlers walk, prune and repackage one workspace's node_modules and expect every dependency to be physically present under it. Mark such a workspace as self-contained, either in its own package.json (the same key Yarn uses; only the "workspaces" value is recognized):

apps/desktop/package.json
{
  "name": "desktop",
  "installConfig": { "hoistingLimits": "workspaces" }
}

or from the root package.json, next to the workspace globs:

package.json
{
  "workspaces": {
    "packages": ["apps/*", "packages/*"],
    "selfContained": ["apps/desktop"]
  }
}

(entries are workspace paths or package names)

For that workspace bun install then behaves as a hoisting barrier — nothing it depends on, directly or transitively (including through other workspaces it depends on), is placed above apps/desktop/node_modules, so that directory is a complete tree — and materializes those packages as real copies instead of hardlinks / clones from the cache, so tools that rewrite them cannot affect the cache or other projects. All other workspaces keep hoisting to the root as usual. The setting is recorded in bun.lock for the workspace, so installs from the lockfile reproduce the same layout. This setting has no effect with the isolated linker, where every package already resolves only its own dependencies.

Share versions with Catalogs#

When many packages need the same dependency versions, define those versions once in a catalog in the root package.json and reference them from your workspaces with the catalog: protocol. Updating the catalog updates every package that references it. See Catalogs.

⚡️ Speed — Installs are fast, even for big monorepos. Bun installs the Remix monorepo in about 500ms on Linux.

  • 28x faster than npm install
  • 12x faster than yarn install (v1)
  • 8x faster than pnpm install