Skip to main content
On this page

Configure a monorepo with workspaces

A workspace is a collection of folders, each with its own deno.json or package.json, managed from a single root. Members can import each other by name, share dependencies, and run tools like deno test and deno fmt across the whole repository at once.

Create the workspace Jump to heading

The root deno.json lists the member directories:

deno.json
{
  "workspace": ["./utils", "./app"]
}

Each member gets its own deno.json with a name, a version, and an exports field pointing at its entry point, or a package.json, if the member is an npm-style package:

utils/deno.json
{
  "name": "@acme/utils",
  "version": "0.1.0",
  "exports": "./mod.ts"
}
utils/mod.ts
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
app/deno.json
{
  "name": "@acme/app",
  "version": "0.1.0",
  "exports": "./main.ts"
}

Other members import @acme/utils by its bare name: no relative paths and no import map entry needed. Deno finds the member through the root workspace list and resolves the import through its exports:

app/main.ts
import { greet } from "@acme/utils";

console.log(greet("workspace"));
>_
$ deno run app/main.ts
Hello, workspace!

Matching members with globs Jump to heading

Listing every member gets tedious in larger repositories. Each /* segment matches one folder depth: packages/* matches packages/foo but not packages/foo/subpackage.

deno.json
{
  "workspace": ["packages/*", "examples/*/*"]
}

Options that must live at the root Jump to heading

Some options control resolution for the whole workspace and are only read from the root deno.json: nodeModulesDir, vendor, minimumDependencyAge, links, lock, and allowScripts. Setting them in a member has no effect and Deno will warn about it.

Note

name, version, and exports go the other way: they belong in members, not in the root.

Sharing dependencies from the root Jump to heading

Members inherit the root imports map, so common dependencies can be declared once:

deno.json
{
  "workspace": ["./utils", "./app"],
  "imports": {
    "@std/assert": "jsr:@std/assert@^1.0.0"
  }
}

A member can declare its own imports too; within that member's folder, its entries override the root's.

Using existing npm, yarn, and pnpm workspaces Jump to heading

Deno understands the workspaces field of package.json directly, so a monorepo set up for npm or yarn needs no conversion:

package.json
{
  "workspaces": ["pkg-a", "pkg-b"]
}

Members declare each other in their dependencies and import by name, just like under npm:

pkg-b/package.json
{
  "name": "@acme/b",
  "dependencies": { "@acme/a": "*" }
}
>_
$ deno install
$ deno run pkg-b/main.ts
from workspace member

Note

pnpm workspaces are the exception: Deno does not read pnpm-workspace.yaml, so move its package list into the workspace field of a root deno.json (or a workspaces field in package.json).

Centralized versions with catalog: Jump to heading

Since Deno 2.8, the root can declare a catalog of version requirements that members reference from their package.json dependencies with the catalog: specifier:

deno.json
{
  "workspace": ["./utils", "./app"],
  "catalog": {
    "chalk": "^5.3.0"
  }
}
app/package.json
{
  "name": "app",
  "dependencies": {
    "chalk": "catalog:"
  }
}

To bump every member to a new version, edit the catalog entry once.

For the full option matrix, publishing workspace packages, and workspace: protocol support in package.json, see Workspaces and monorepos.

Did you find what you needed?

Edit this page
Privacy policy