Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Ease the Transition to a Monorepo with Focused Workspaces

Posted May 18, 2018 by Bryan Wain

Previously, we wrote about monorepos and how Yarn Workspaces makes working with them simpler. Unfortunately, moving to a monorepo is not always an easy choice. Without the right tooling, a monorepo can often harm the developer experience instead of help it.

The Problem With Monorepos

One of the main reasons projects usually move to a monorepo is because they make it easier to make changes across multiple packages by allowing you make those changes in a single pull request. One of the ways Yarn Workspaces improves this workflow is by automatically symlinking sibling package dependencies in the node_modules folder, allowing you to immediately see the results of a change to one package when working on another.

Unfortunately, while symlinking sibling packages all of the time makes cross-package development easier, it comes with a big downside when you want to work on a single package. If package A depends on packages B and C, you need to build all three packages just to make a change in package A. In a monorepo with many packages, this can be a very slow process compared to a multirepo setup, where you would just pull down a pre-built version of B and C when you install.

These opposing benefits of monorepo and multirepo development often leave developers with a difficult choice. Optimize for cross-package development with a monorepo or optimize for single-package development with many repos. We feel that this is a choice you should not have to make, and our goal with focused workspaces was to help turn Yarn Workspaces into a tool that gives you the full benefits of a monorepo without forcing you to give up the benefits of multiple repos.

Introducing Focused Workspaces

yarn install --focus is a new installation option available in 1.7.0 that shallowly installs a workspace’s sibling dependencies under its node_modules folder. Shallow installation can best be explained with an example.

Imagine you have a monorepo with packages A and B, where A depends on B, and B depends on External, which is not part of the monorepo. A normal installation might result in something like this:

| my_project/
|      package.json
|      node_modules/
|          A/ (symlink to packages/A)
|          B/ (symlink to packages/B)
|          External/
|      packages/
|          A/
|              node_modules/ (empty)
|          B/
|              node_modules/ (empty)

The problem here is that if you want to run A, you need to build B as well. If you haven’t made any changes to B, this is most likely going to be slower than installing B from the registry.

If you were to go to packages/A and run yarn install --focus instead, the result would look like this:

| my_project/
|      package.json
|      node_modules/
|          A/ (symlink to packages/A)
|          B/ (symlink to packages/B)
|          External/
|      packages/
|          A/
|              node_modules/
|                  B/ (not a symlink. Pulled from registry. No need to build.)
|          B/
|              node_modules/ (empty)

This allows you to run A without rebuilding B. Because of the way node module resolution works, when building A, it will find B in packages/A/node_modules/B and avoid using the symlink at the root, which points to an unbuilt version of B.

Yarn will always do a minimal install when using --focus. Note that External is not reinstalled under A. There is no need because when A tries to resolve External, it will already point at the hoisted version.

Dealing With Version Conflicts

Let’s look at a slightly more complicated example. Imagine now that both A and B depend on External, but A depends on External@1.0.0 and B depends on External@2.0.0. A and B can no longer share a copy of External because their versions are incompatible.

A regular install would produce this:

| my_project/
|      package.json
|      node_modules/
|          A/ (symlink to packages/A)
|          B/ (symlink to packages/B)
|          External/ (v2)
|      packages/
|          A/
|              node_modules/
|                  External (v1)
|          B/
|              node_modules/ (empty)

and a focused install would produce this:

| my_project/
|      package.json
|      node_modules/
|          A/ (symlink to packages/A)
|          B/ (symlink to packages/B)
|          External/ (v2)
|      packages/
|          A/
|              node_modules/
|                  External (v1)
|                  B/
|                      node_modules/
|                          External/ (v2)
|          B/
|              node_modules/ (empty)

Yarn needs to not only install B under A, but also install External under the nested copy of B to ensure that B uses v2 of External while A still uses v1.

Unfocusing

If you want to remove the shallow installations from a focused install, just rerun yarn install without --focus. All node_modules will return to the state they were in before you focused. upgrade, add, and remove will also remove any shallow installations.

You can also focus on another workspace and the original workspace will unfocus.

Focusing by Default

If you would like to have a workspace always use --focus when installing, you can use the CLI arguments feature of .yarnrc files.

--install.focus true

If you add that to packages/A/.yarnrc, you will always do a focused install when you run install from A, but not from any other packages or the root.

If you instead add it to packages/.yarnrc, you will do a focused install from all packages under packages/.

What’s Next?

We hope focused workspaces helps make your migration to a monorepo easier. This is only a first iteration and further improvements will likely be made in the future. If you notice anything wrong or have any suggestions on how we can improve your experience with workspaces, please let us know.

References

  • Original Proposal: RFC
  • Implementation PR: #5663