JavaScript Monorepo Tools

If you’re new to monorepo, read my previous post on adoption of monorepo first to have a good understanding of what it is, who’s using it, and whether it is a thing for you.

To manage a multi-language monorepo, you have to use powerful and complicated build systems like Buck, Bazel, Pants, or Please. These tools are great for enterprise-level projects but seem a bit overkill for individuals or small teams.

This post focuses only managing an almost pure JavaScript/TypeScript monorepo (Also called multi-package monorepo) including websites built with JavaScript frameworks, Node-based servers, React Native mobile apps, or shared npm packages.

monorepo-root
β”œβ”€β”€ package.json
β”œβ”€β”€ package-one
β”‚   β”œβ”€β”€ package.json
β”‚   └── index.js
└── package-two
    β”œβ”€β”€ package.json
    └── index.js

The expectation is low when you just want a streamline workflow of dealing with git, npm packages, and npm scripts. You can use built-in workspaces feature provided by JavaScript package managers or dedicated JavaScript monorepo tools like Lerna, Rush, Nx.

Workspaces by Package Managers

Notable package managers like Yarn, Npm, or Pnpm have built-in feature called workspaces aim to make working with monorepo easy.

Workspaces is a generic term that refers to the set of features in package manager CLI that provides support to managing multiple packages from your local files system from within a singular top-level, root package. Workspaces are usually defined via the workspaces property of the package.json file:

{
  "name": "monorepo-root",
  "workspaces": ["package-one", "package-two", "packages/*"]
}

This set of features makes up for a much more streamlined workflow handling linked packages from the local file system. Automating the linking process as part of install command and avoiding manually having to use link command in order to add references to packages that should be symlinked into the current node_modules folder.

When a workspace is packed into an archive (whether it’s through pack or publish), package manager will dynamically update associated semver range.

At minimum, you’ll be able to keep multiple related packages all together in a single repository, and test changes in an integrated environment, without continually re-linking.

Not possible for more advanced workspace management features like building, versioning, managing permissions, and publishing all the packages within a workspace together with a single command.

Lerna

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm. Your repository will look like this:

monorepo-root
β”œβ”€β”€ package.json
β”œβ”€β”€ lerna.json
β”œβ”€β”€ packages
β”‚   β”œβ”€β”€ package-three
β”‚       β”œβ”€β”€ package.json
β”‚       └── index.js
β”œβ”€β”€ package-one
β”‚   β”œβ”€β”€ package.json
β”‚   └── index.js
└── package-two
    β”œβ”€β”€ package.json
    └── index.js

Your lerna.json will look similar to this:

{
  "version": "1.1.3",
  "npmClient": "npm",
  "command": {
    "bootstrap": {
      "npmClientArgs": ["--no-package-lock"]
    }
  },
  "packages": ["package-one", "package-two", "packages/*"]
}

Lerna is good at versioning in both fixed and independent mode:

  • Fixed mode Lerna projects operate on a single version line kept in lerna.json. If a module has been updated since the last time a release was made, it will be updated to the new version you’re releasing.

  • Independent mode Lerna projects allows maintainers to increment package versions independently of each other. Each time you publish, you will get a prompt for each package that has changed to specify if it’s a patch, minor, major or custom change.

Lerna support filtering packages by name, hoisting common dependencies to root, automatic local linking, versioning, bulk publishing, diffing changes since last release, or running arbitrary commands.

Lerna operates in a higher level than above package managers’ workspaces but not close to a build system when it has no support for build languages, caching, action directed graphs, or incremental builds.

Rush

Rush makes life easier for JavaScript developers who build and publish many packages from a common Git repo.

Rush’s unique installation strategy produces a single shrinkwrap/lock file for all your projects that installs extremely fast. Rush supports parallel builds, subset builds, and incremental builds.

Repo policies allow new package dependencies to be reviewed before they are accepted. Rush can enforce consistent dependency versions across your repo. Different subsets of projects can publish separately with lockstep or independent versioning strategies, private releases, and so forth.

Rush’s isolated symlinking model eliminates npm phantom dependencies, ensuring you’ll never again accidentally import a library that was missing from package.json.

Rush helps to ensure that installs and builds are completely deterministic. If you define custom commands or options, they are strictly validated and documented as part of Rush’s command-line help.

Nx

Nx is a set of extensible dev tools for monorepo, has first-class support for many frontend and backend technologies like Angular, React, and Node.

Nx is smart when it analyzes your workspace and figures out what can be affected by every code change and only rebuilds what is necessary.

Nx has support for distributed and incremental builds. If someone has already built or tested similar code, Nx will use their results to speed up the command for everyone else instead of rebuilding or retesting the code from scratch.

Nx is an open platform with plugins for many modern tools and frameworks. It has support for TypeScript, React, Angular, Cypress, Jest, Prettier, Nest.js, Next.js, Storybook, Ionic among others. With Nx, you get a consistent dev experience regardless of the tools used.

Summary

Package managers’ workspaces is good at optimizing node_modules installation and nothing else, you need dedicated tools to actually orchestrate a monorepo.

Lerna is fast to get started with very little configuration, good at running scripts in packages, can be used together with package managers’ workspaces to improve bootstrap performance. Following are notable public lerna-based monorepos:

  • jest - delightful JavaScript testing
  • babel - compiler for writing next generation JavaScript
  • create-react-app - create React apps with no build configuration
  • react-router - declarative routing for React

Nx and Rush are way more complicated, optimized for JavaScript Tooling, close to a build system in terms of features and learning curve.

Above tools makes life easier for JavaScript developers who build and publish many npm packages at once. Lerna is a safe start, switch to Rush when build time getting out of hand, or a complete build system when monorepo is not pure JavaScript anymore.