Rush StackShopBlogEvents
Skip to main content

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:

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:

FilePurpose
common-versions.jsonRush version overrides
pnpm-config.jsonPNPM version overrides
pnpm-lock.yamlThe PNPM lockfile
repo-state.jsonA config file generated by Rush to prevent manual lockfile changes
.npmrcpackage manager configuration
.pnpmfile-subspace.cjsProgrammatic 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 fileGlobal config fileInheritance
common-versions.jsonnoneglobal file is forbidden with subspaces enabled
pnpm-config.jsoncommon/config/rush/pnpm-config.json(STILL UNDER DEVELOPMENT) subspace takes precedence, but certain fields are ignored
pnpm-lock.yamlnoneglobal file is forbidden with subspaces enabled
repo-state.jsonnoneglobal file is forbidden with subspaces enabled
.npmrccommon/config/rush/.npmrcsubspace overrides take precedence
.pnpmfile-subspace.cjscommon/config/rush/.pnpmfile.cjssubspace 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 to
  • common/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:

  1. Just a few subspaces: You can set "preventSelectingAllSubspaces": false in subspaces.json, and rush install by default will install all subspaces.

  2. 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 like rush install or rush update, users MUST filter the subspaces in some way, such as:

    • rush install --to my-project to install only dependencies of a given project
    • rush install --subspace my-subspace to install only a specific subspace
    • rush install --to subspace:my-subspace using a project selector to install for projects belonging to a given subspace

How to enable subspaces

  1. Make sure your rush.json specifies "rushVersion": "5.122.0" or newer, and "pnpmVersion": "8.7.6" or newer.

  2. 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 called install-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
    ]
    }
  3. 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.cjs
  4. Create the install-test subspace folder:

    cd my-repo
    mkdir --parents common/config/subspaces/install-test
  5. Assign 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 the default subspace.

  6. 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-test

    Note: 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 the pnpm-lock.yaml files.

See also