Rush subspaces
What are subspaces?
Subspaces are a Rush feature that enables a single monorepo to install using multiple PNPM lockfiles. For example, if the subspace name is my-team
, there will be a folder common/config/subspaces/my-team/
containing the pnpm-lock.yaml
file and related configuration. Each Rush project belongs to exactly one subspace, and the monorepo still has one unified "workspace." Thus, a project's package.json
file can use the workspace:
specifier to depend on projects from other subspaces.
What is the benefit?
Generally it's best to have a single lockfile for the entire monorepo, as this optimizes installation time and minimizes maintenance work for managing version conflicts. However, multiple lockfiles have advantages in certain situations:
A very large codebase: A lockfile can be thought of as giant multivariable equation, which we solve by coordinating NPM package version choices across many projects to eliminate conflicts and minimize duplication. (The Lockfile Explorer docs explain this in depth.) Dividing up monorepo dependencies into smaller lockfiles does make these equations smaller and easier to solve, but with the tradeoff of increasing the total overhead for managing versions. With a very large engineering team, dividing up the work can be more important than minimizing the total amount of work.
Decoupled project sets: A large code base may have certain clusters of projects whose dependencies are not aligned with the rest of the repo. For example, suppose 50 projects comprise a legacy application that uses a deprecated or outdated framework, with no business motivation to modernize it. Moving these projects into a subspace enables their versioning to be managed independently.
Installation testing: When publishing NPM packages, certain bugs cannot be reproduced using
workspace:*
symlinking. For example, phantom dependencies or incorrect.npmignore
globs will cause failures for external consumers of a package, but may work fine when the same library is tested within the monorepo. Moving test projects into a subspace (combined with injected dependencies) produces a more accurate installation that can catch such problems, while still avoiding the overhead of actually publishing to a testing NPM registry.
How many subspaces do I need?
We generally recommend "as few as possible" to minimize additional version management overhead. One subspace per team is a sensible maximum limit. That said, this feature has been used successfully in a production monorepo with more than 1,000 subspaces.
Real world demo
Rush Stack's own repository on GitHub is currently configured with two subspaces:
- common/config/subspaces/build-tests-subspace: used to test installation of published NPM packages
- common/config/subspaces/default: contains all other projects
Feature design
Each subspace must have its name registered centrally in the common/config/subspaces.json config file. Projects are added to a subspace using their subspaceName
field in rush.json.
The configuration for each subspace goes in a folder common/config/subspaces/<subspace-name>/
, which may contain the following files:
File | Purpose |
---|---|
common-versions.json | Rush version overrides |
pnpm-config.json | PNPM version overrides |
pnpm-lock.yaml | The PNPM lockfile |
repo-state.json | A config file generated by Rush to prevent manual lockfile changes |
.npmrc | package manager configuration |
.pnpmfile-subspace.cjs | Programmatic version overrides, following the same specification as .pnpmfile.cjs but specific to the subspace |
Some files can be configured globally (applying across the entire monorepo) in addition to subspace level:
Subspace config file | Global config file | Inheritance |
---|---|---|
common-versions.json | none | global file is forbidden with subspaces enabled |
pnpm-config.json | common/config/rush/pnpm-config.json | (STILL UNDER DEVELOPMENT) subspace takes precedence, but certain fields are ignored |
pnpm-lock.yaml | none | global file is forbidden with subspaces enabled |
repo-state.json | none | global file is forbidden with subspaces enabled |
.npmrc | common/config/rush/.npmrc | subspace overrides take precedence |
.pnpmfile-subspace.cjs | common/config/rush/.pnpmfile.cjs | subspace overrides take precedence |
Note that the following config files are NOT moved:
common/config/.npmrc-publish
: This file is used for Rush NPM publishing, regardless of which subspaces the published projects belong tocommon/config/.pnpmfile.cjs
: This file can apply versioning overrides that will affect all subspaces in the monorepo. To avoid confusing interactions across lockfiles, in most cases it is better to use.pnpmfile-subspace.cjs
instead.
Without subspaces, Rush generates and installs the PNPM workspace in the common/temp/
folder. With subspaces enabled, this will be performed separately in folders such as common/temp/<subspace-name>/
.
There are two basic modes of operation:
Just a few subspaces: You can set
"preventSelectingAllSubspaces": false
insubspaces.json
, andrush install
by default will install all subspaces.Lots of subspaces: If installing all subspaces would consume too much time and disk space, then you can set
"preventSelectingAllSubspaces": true
. In this mode, when invoking commands likerush install
orrush update
, users MUST filter the subspaces in some way, such as:rush install --to my-project
to install only dependencies of a given projectrush install --subspace my-subspace
to install only a specific subspacerush install --to subspace:my-subspace
using a project selector to install for projects belonging to a given subspace
How to enable subspaces
Make sure your rush.json specifies
"rushVersion": "5.122.0"
or newer, and"pnpmVersion": "8.7.6"
or newer.Use subspaces.json to enable the feature and define the subspaces. You can copy the template for this file from the subspaces.json docs, or use
rush init
to generate it. In this tutorial, we'll create one subspace calledinstall-test
for testing NPM packages:common/config/rush/subspaces.json
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json",
/**
* Set this flag to "true" to enable usage of subspaces.
*/
"subspacesEnabled": false,
/**
* When a command such as "rush update" is invoked without the "--subspace" or "--to"
* parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces,
* this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this
* mistake by always requiring selection parameters for commands such as "rush update".
*/
"preventSelectingAllSubspaces": false,
/**
* The list of subspace names, which should be lowercase alphanumeric words separated by
* hyphens, for example "my-subspace". The corresponding config files will have paths
* such as "common/config/subspaces/my-subspace/package-lock.yaml".
*/
"subspaceNames": [
// The "default" subspace always exists even if you don't define it,
// but let's include it for clarity
"default",
"install-test" // 👈👈👈 Our secondary subspace name
]
}Create the
default
subspace folder and move the existing config files there:cd my-repo
mkdir --parents common/config/subspaces/default
# Move these files:
mv common/config/rush/common-versions.json common/config/subspaces/default/
mv common/config/rush/pnpm-lock.yaml common/config/subspaces/default/
mv common/config/rush/.npmrc common/config/subspaces/default/
# Rename this file:
mv common/config/rush/.pnpmfile.cjs common/config/subspaces/default/.pnpmfile-subspace.cjsCreate the
install-test
subspace folder:cd my-repo
mkdir --parents common/config/subspaces/install-testAssign projects to subspaces by editing
rush.json
. For example:rush.json
. . .
"projects": [
{
"packageName": "my-library-test",
"projectFolder": "test-projects/my-library-test",
"subspaceName": "install-test"
}
. . .If
"subspaceName"
is omitted for any projects, they will belong to thedefault
subspace.Now update the lockfiles for the new subspaces:
# Clean out the common/temp folder from before
rush purge
# Regenerate the "default" subspace:
rush update --full --subspace default
# Regenerate the "install-test" subspace:
rush update --full --subspace install-testNote: You can migrate to subspaces without using
--full
to regenerate any lockfiles, but it is a more involved process that may involve using a script to rewrite some paths in thepnpm-lock.yaml
files.
See also
- subspaces.json config file
- rfc-4230-rush-subspaces.md: The original spec for this feature, which explains the motivation and design in more detail