Using Workspaces in a Monorepo Project with vlt
Using Workspaces in a Monorepo Project with vlt
When using the vlt client, you can define multiple different
workspaces in a single monorepo project.
The vlt client supports workspaces defined by glob patterns, and you
can also define named groups to perform vlt operations on a specific
subset of your workspaces.
Getting Started
It's possible to use the vlt init command to create new workspaces
by using the --workspace (or -w for short) option, e.g:
$ vlt init -w packages/aThis will ensure the folder is created and a package.json file is
added to it, along with adding the required configuration to the
vlt.json file if needed.
Manually defining Workspaces
If you want to set up new workspaces, it's also possible to use the
vlt.json file in the root of your project with a workspaces field.
At its most explicit, the workspaces field can be an object with
group names as the keys, and either a single string or an array of
strings to identify the workspace paths in each group. The strings are
interpreted as glob patterns, so if a path has glob special characters
that should be interpreted literally, use the \ character to escape
them.
For example:
{
"workspaces": {
"www": "www/*"
"utils": [
"utils/*",
"www/utils"
],
}
}It's perfectly fine for a workspace to be found in multiple groups,
like the www/utils workspace in the example above.
If that level of group detail isn't needed, then you can also just
point the workspaces field at a string or array of string glob
patterns. For example:
{
"workspaces": ["www/*", "utils/*"]
}Or even:
{
"workspaces": "packages/*"
}Using npm/yarn workspaces from package.json
If your project already declares workspaces the npm or yarn way, in
the workspaces field of the root package.json, vlt reads them, and
vlt install saves them to vlt.json (see
Saving workspaces to vlt.json):
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}The yarn-classic object form is also understood, including the
nohoist key, which vlt parses and ignores (and drops when saving to
vlt.json):
{
"workspaces": {
"packages": ["packages/*"]
}
}vlt.json takes precedence: if it has a workspaces field, that is
the definition vlt uses and the package.json field is ignored
entirely. The two are not merged. Note that this is keyed on the
field, not the file — a vlt.json that exists but says nothing about
workspaces still falls through to package.json.
Named groups are a vlt.json feature. They are deliberately rejected
in package.json, because a package.json using them would be
meaningless to every other package manager:
{
"workspaces": {
"apps": "apps/*"
}
}Putting that in package.json is an error; put it in vlt.json
instead.
Using pnpm workspaces from pnpm-workspace.yaml
If neither vlt.json nor the root package.json declares workspaces,
vlt reads the packages list of a root pnpm-workspace.yaml, and
vlt install saves it to vlt.json:
packages:
- 'packages/*'
- '!packages/legacy'Other keys in that file are ignored, so catalog: specs still need a
catalog in vlt.json (see Catalogs). Only a
plain list of strings is read; anything else (anchors, tags, unquoted
! patterns) makes vlt ignore the whole list, so declare the
workspaces in vlt.json instead. Workspace names must be unique (pnpm
allows duplicates); otherwise list workspaces in vlt.json without
them.
Saving workspaces to vlt.json
vlt install copies workspaces read from package.json or
pnpm-workspace.yaml to the workspaces field of the project
vlt.json (created if missing, other fields kept), in order, !
exclusions included, and prints:
Saved workspaces from pnpm-workspace.yaml to vlt.jsonThe source file is left untouched, so other tools keep reading it. An
existing workspaces field in vlt.json is never overwritten.
vlt init -w copies them the same way before adding a workspace
outside these globs. Nothing is written by vlt ci,
--frozen-lockfile or --expect-lockfile. Commit the updated
vlt.json.
From then on vlt.json takes precedence: later edits to
package.json or pnpm-workspace.yaml workspaces are ignored by vlt,
so update vlt.json as well. To keep the original file as the only
source, opt out before the first vlt install (or remove the saved
workspaces field) with --no-migrate-workspaces or in vlt.json:
{
"config": { "migrate-workspaces": false }
}vlt ci reads workspaces from vlt.json only, even with
--no-migrate-workspaces. If they are declared only in package.json
or pnpm-workspace.yaml, it warns and ignores them; run vlt install
and commit vlt.json first.
Linking workspaces referenced by plain ranges
npm and yarn v1 have no workspace: protocol, and pnpm with
link-workspace-packages does not need one, so their package.json
files reference local packages by plain ranges such as ^1.0.0. vlt
links such a dependency to the workspace only while the workspace
version satisfies the range and fetches it from the registry
otherwise. To always link them, add one graph modifier to the same
vlt.json:
{
"modifiers": {
":project > :workspace": "workspace:*"
}
}It applies to every direct dependency of the root or of a workspace
that is named after a workspace, and can be removed once the
package.json files use workspace: specs. See
Implicit workspace references
for the details.
Excluding paths
A pattern beginning with ! excludes the paths it matches, in
vlt.json, package.json and pnpm-workspace.yaml:
{
"workspaces": ["packages/*", "!packages/legacy"]
}Following npm's behavior, !packages/legacy excludes only that
directory, while !packages/legacy/** excludes everything beneath it
as well. A later positive pattern overrides an earlier exclusion, so
["packages/**", "!packages/b/**", "packages/b/a"] keeps
packages/b/a. To match a directory whose name genuinely begins with
!, escape it as \!.
Working with Workspaces
The simplest way to peform an option on a single workspace is to just
cd into that folder, and then use whatever vlt operation as you
would normally. Any installations will be added as dependencies to
that single workspace, scripts will be run in that workspace only,
etc.
If you want to work with multiple workspaces at one time, there are a few options.
Running Commands on All Workspaces
You can use the -r --recursive flag to run a script across all
workspaces in the monorepo project.
For example:
$ vlt run --recursive testwill run the test script in all workspaces.
Running Commands on Specific Workspaces By Path
To run a command on certain workspaces by their path, you can use the
-w<path> --workspace=<path> configuration option.
For example, to run the tests on certain workspaces by path name, you can run this:
$ vlt --workspace=src/* --workspace=www/components run testwould run the test command on all the workspaces under src/*, as
well as the www/components workspace.
Note that glob patterns may have to be quoted so that the shell does not expand them incorrectly.
Running Commands on Specific Workspaces By Group
If you had configured your workspaces into named groups, then you can
reference those groups using the --workspace-group=<name>
configuration option.
For example, with the vlt.json defined above:
{
"workspaces": {
"www": "www/*"
"utils": [
"utils/*",
"www/utils"
],
}
}we could do:
$ vlt --workspace-group utils run testto run tests on all our utils workspaces.
Execution Order
Commands that target multiple workspaces (run, run-exec, exec,
exec-local, version, pack, publish, pkg) process them in
dependency order: a workspace comes after the workspaces it depends
on. This applies to --recursive, --workspace, --workspace-group
and --scope.
With --scope, query order is kept where possible (a dependency may
move ahead of unrelated matches), and dependencies through workspaces
the query did not match still count. --workspace and
--workspace-group only see dependencies among the selected
workspaces. Cycles are broken, so order within a cycle is not
guaranteed.
See also
- Affected Dependencies — running scripts on only the workspaces a change could have broken