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:
{
"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:
{
"name": "@acme/utils",
"version": "0.1.0",
"exports": "./mod.ts"
}
export function greet(name: string): string {
return `Hello, ${name}!`;
}
{
"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:
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.
{
"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.
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:
{
"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:
{
"workspaces": ["pkg-a", "pkg-b"]
}
Members declare each other in their dependencies and import by name, just like
under npm:
{
"name": "@acme/b",
"dependencies": { "@acme/a": "*" }
}
$ deno install
$ deno run pkg-b/main.ts
from workspace member
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:
{
"workspace": ["./utils", "./app"],
"catalog": {
"chalk": "^5.3.0"
}
}
{
"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.