A small Node.js library and CLI that resolves workspace protocol versions in a monorepo. It reads each workspace package’s package.json, finds dependencies that use the workspace: protocol (e.g. workspace:*, workspace:^), replaces them with the actual package versions, and writes the changes back without reordering or reformatting the rest of the file.
Useful for pre-publish steps or tooling that needs concrete versions instead of workspace:* in dependency maps.
Table of contents
- Installation
- Quick start
- CLI
- Programmatic API
- How it works
- Version resolution rules
- Requirements and behavior
- License
Installation
npm install workspace-version-resolver # or pnpm add workspace-version-resolver # or bun add workspace-version-resolver
Quick start
CLI — run from the monorepo root (or pass a path). The binary is also available under the alias wvr:
npx workspace-version-resolver
# or
npx workspace-version-resolver /path/to/monorepoAPI — call the default export with the root directory:
import resolveWorkspaceVersions from 'workspace-version-resolver'; await resolveWorkspaceVersions('/path/to/monorepo');
CLI
The package ships a binary: workspace-version-resolver (alias wvr). Run via npx, bunx, or after npm install -g.
Usage
workspace-version-resolver [path]
[path]— Optional path to the monorepo root. If omitted, the current working directory is used.
Options
| Option | Alias | Type | Default | Description |
|---|---|---|---|---|
--warn-on-circular |
— | boolean | true |
When true, print a warning for each circular workspace dependency. Use --no-warn-on-circular to disable. |
--help |
-h |
boolean | — | Show help and exit. |
--version |
-v |
boolean | — | Show package version and exit. |
Examples
# Use current directory as monorepo root npx workspace-version-resolver # Explicit path npx workspace-version-resolver path/to/monorepo/ # Disable circular dependency warnings npx workspace-version-resolver --no-warn-on-circular # Help and version npx workspace-version-resolver --help npx workspace-version-resolver --version
On validation or runtime errors, the process exits with code 1 and prints the error message to stderr.
Programmatic API
The package exports a default function and the individual steps + types so you can reuse or test them.
Default export: resolveWorkspaceVersions
Runs the full pipeline: validate root → resolve workspace paths → read manifests → build graph → topological sort → resolve versions → write package.json files.
function resolveWorkspaceVersions( rootDir: string, options?: ResolveWorkspaceVersionsOptions ): Promise<void>
rootDir— Absolute or relative path to the monorepo root (directory that contains apackage.jsonwith a"workspaces"array).options— Optional:warnOnCircular?: boolean— Iftrue(default),console.warnis used when circular workspace dependencies are detected. Does not throw.
Example
import resolveWorkspaceVersions from 'workspace-version-resolver'; await resolveWorkspaceVersions(process.cwd(), { warnOnCircular: true });
Options type
interface ResolveWorkspaceVersionsOptions { /** If true (default), emit console.warn when circular dependencies are detected. */ warnOnCircular?: boolean; }
Named exports (modular steps)
You can call each step yourself for custom workflows or tests.
| Export | Description |
|---|---|
validateRoot(rootDir: string) |
Ensures rootDir is a directory and has a package.json with a "workspaces" array of strings. Returns { workspaces: string[] }. Throws on invalid root. |
resolveWorkspacePaths(rootPath, workspaces) |
Expands workspace globs (e.g. packages/*) to absolute directory paths and keeps only directories that contain a package.json. Returns string[]. |
readManifests(dirPaths) |
Reads each directory’s package.json, extracts name, version, dependencies, devDependencies, peerDependencies, and workspace-only deps. Returns PackageInfo[]. Throws on duplicate package names. |
buildDependencyGraph(packages) |
Builds a directed graph of workspace packages (by name, edges = “depends on”). Returns DependencyGraph. |
topologicalSort(graph) |
Returns a topological order (packages with no workspace deps first) and any cycles found. Returns TopoResult ({ ordered: string[], cycles: string[][] }). Does not throw on cycles. |
resolveVersions(ordered, packages) |
Resolves each workspace specifier to the depended-on package’s version. Returns ResolvedUpdates ({ byPackage: Map<string, Map<string, string>> }). |
resolveVersionSpec(specifier, targetVersion) |
Resolves a single specifier string (e.g. "workspace:*") given the target package version. Returns the resolved version string. |
writePackageJson(dirPath, updates, workspaceDeps) |
Writes back only the resolved version values into the given directory’s package.json, preserving formatting and key order. |
Types
type DependencyMap = Record<string, string>; interface PackageInfo { dirPath: string; name: string; version: string; dependencies: DependencyMap; devDependencies: DependencyMap; peerDependencies: DependencyMap; workspaceDeps: DependencyMap; // workspace: entries only } type DependencyGraph = Map<string, Set<string>>; interface TopoResult { ordered: string[]; cycles: string[][]; }
Example: custom pipeline
import path from 'node:path'; import { validateRoot, resolveWorkspacePaths, readManifests, buildDependencyGraph, topologicalSort, resolveVersions, writePackageJson, } from 'workspace-version-resolver'; const rootDir = path.resolve('./my-monorepo'); const { workspaces } = validateRoot(rootDir); const dirPaths = resolveWorkspacePaths(rootDir, workspaces); const packages = readManifests(dirPaths); const graph = buildDependencyGraph(packages); const { ordered, cycles } = topologicalSort(graph); if (cycles.length > 0) console.warn('Cycles:', cycles); const { byPackage } = resolveVersions(ordered, packages); for (const pkg of packages) { const updates = byPackage.get(pkg.dirPath); if (updates?.size) writePackageJson(pkg.dirPath, updates, pkg.workspaceDeps); }
How it works
- Validate root — Checks that
rootDiris a directory and that itspackage.jsonhas a"workspaces"array of strings. - Resolve workspace paths — Expands each workspace entry (e.g.
packages/*) to absolute directory paths and keeps only directories that contain apackage.json. - Read manifests — Reads each package’s
package.jsonand extractsname,version,dependencies,devDependencies, andpeerDependencies. Duplicate package names cause an error. - Workspace deps — From each manifest, only entries whose value starts with
workspace:are considered (e.g.workspace:*,workspace:^,workspace:~,workspace:1.2.3). - Dependency graph — Builds a directed graph of packages based only on workspace dependencies.
- Topological sort — Orders packages so that those with no workspace deps come first, then their dependents. Cycles are detected and reported (warning only, no throw).
- Resolve versions — For each workspace dependency, computes the resolved version string from the depended-on package’s version and the specifier (see Version resolution rules).
- Write back — Updates each
package.jsonby replacing only theworkspace:...values with the resolved versions, using a surgical string replace so that key order and formatting are preserved.
Version resolution rules
For each dependency that uses the workspace: protocol, the resolved value is derived from the depended-on package’s version and the specifier:
| Specifier | Resolved value | Example (target version 1.2.3) |
|---|---|---|
workspace:* |
Exact version | 1.2.3 |
workspace:^ |
Caret + version | ^1.2.3 |
workspace:~ |
Tilde + version | ~1.2.3 |
workspace:1.2.3 |
Strip workspace: |
1.2.3 |
workspace:^1.2.3 |
Strip workspace: |
^1.2.3 |
workspace:~1.0.0 |
Strip workspace: |
~1.0.0 |
Only dependency entries whose value is a string starting with workspace: are modified; all other keys and values are left unchanged.
Requirements and behavior
- Root must be a directory whose
package.jsoncontains a"workspaces"property set to an array of strings (e.g.["packages/*"]). - Workspace entries are resolved relative to the root and expanded (e.g. globs). Only directories that contain a
package.jsonare included. - Duplicate package names (same
namein more than one workspace package) cause the run to throw (or the CLI to exit with code 1). - Circular workspace dependencies are detected and reported with a warning only; the run continues and versions are still resolved where possible.
- File writes change only the version strings for workspace-resolved dependencies; property order and formatting (whitespace, newlines) are preserved.
License
MIT. See LICENSE in the repository.
Made with ❤️ by Lacus Solutions