Rush vs Nx: A Comparison of TypeScript Monorepo Management Tools
November 11, 2020With the increasing popularity of the monorepo strategy for storing source code, there is a growing number of tools to manage projects in a monorepo, such as Bazel, Lerna, Rush, or Nx.
This article will focus on comparing Rush and Nx: differences in their goals, capabilities, and their approach to structuring projects.
Goals of Rush and Nx
Both Rush and Nx have similar goals. They both aim to provide a scalable solution to manage large monorepos for web projects.
As stated by Rush:
Rush: a scalable monorepo manager for the web
Rush Stack is a mission to provide reusable tech for running large scale monorepos for the web
As stated by Nx:
Nx is a set of extensible dev tools for monorepos, which helps you develop like Google, Facebook, and Microsoft.
Each tool is oriented towards a different audience, based on the way the tool is published and marketed by its sponsor.
Rush was published by Microsoft after it was internally developed by the OneDrive + SharePoint teams to solve the specific scaling problems encountered by these teams. It is built with first-class support for publishing and managing changelogs for a large number of npm packages. It's published as open-source with a permissive license, but feature development is mainly suited to Microsoft's needs.
Nx is published by Nrwl, a consulting company whose mission is to help its clients build software better and faster. Nrwl built Nx specifically to help software organizations become more productive and organize their code in a more safe, predictable, and scalable fashion. Nrwl is actively building a community around Nx, including blog posts, training, and consulting to assist organization in adopting Nx.
Comparison of Capabilities
Feature | Rush | Nx |
---|---|---|
Manage a complex dependency graph of application and library projects | ✅ | ✅ |
Ability to build all projects | ✅ rush rebuild | ✅ nx run-many --all |
Ability to build a specific project and its dependencies | ✅ rush build --to | ✅ nx run-many --with-deps (coming in v9.0.0) |
Ability to build a specific project and its dependents | ✅ rush build --from | ❌ |
Ability to build projects in parallel | ✅ single machine only | ✅ single machine, or CI execution plan via nx print-affected |
Ability to build all projects affected by edited source code | ✅ rush build | ✅ nx affected:build |
Isolation of npm dependencies per project | ✅ | ❌ not included. By default, all projects use a shared set of packages defined in the root package.json |
Generate code to scaffold new projects and components quickly | ❌ | ✅ using community schematics or custom workspace schematics |
View a visual representation of the dependency graph | ❌ | ✅ nx dep-graph |
Community plugins + examples using modern tools/frameworks such as Next.js, Cypress, Jest, and Storybook | ❌ | ✅ community packages under the @nrwl npm scope |
Ability to publish npm packages to a registry, with a CLI semver workflow | ✅ rush change + rush publish | ❌ |
Define arbitrary commands in each project to launch a dev server, run tests, build production code, or any other task | ✅ via npm scripts | ✅ via custom builders |
Sponsor | Microsoft | Nrwl |
Project Structure
Rush and Nx have different approaches to structuring projects in a monorepo:
- With Rush, each project must contain:
package.json
file defining the project's dependenciesbuild
scripttest
script
- Beyond these constraints, a project's structure is up to the developer; the
build
andtest
scripts can be implemented arbitrarily. - Nx emphasizes an opinionated structure of projects. Tasks such as building and testing are not arbitrarily-defined, but instead are encapsulated in plugins, which are collections of builders and schematics. Each plugin is typically associated with a specific tool, such as
@nrwl/jest
for Jest,@nrwl/next
for Next.js, or@nrwl/cypress
for Cypress.
The result of these differences is that Rush has a higher degree of control over project configuration, but requires writing your own npm scripts to build, lint, and test a project. Whereas an Nx project has opinionated structure, commands, and generators, which can ease the maintenance burden over time, but can require writing your own plugins to control how things are built.
The constraints imposed by Nx afford several advantages:
- Predictable, consistent developer experience between projects.
- Easier to upgrade core dependencies.
- Easy to set up new projects using the same tools and conventions as existing projects.
Dependencies + Linking Projects
An important difference between Rush and Nx is how they handle installation of npm dependencies, and managing inter-dependencies between projects in the monorepo.
Rush
Rush can be configured to use either npm, Yarn, or pnpm to install npm dependencies. Running rush install
performs these steps:
- Install the correct version of the configured package manager, if it's not already installed.
- Install npm dependencies to
common/temp/node_modules
. - Link dependencies declared by each project to its respective
node_modules
subdirectory.
For example, we might end up with this directory structure:
my-monorepo/rush.jsoncommon/temp/node_modules/react/luxon/projects/my-app/package.jsonindex.tsnode_modules/react/ -> ../../../common/temp/node_modules/react@my-monorepo/my-datetime-library -> ../../my-datetime-librarymy-datetime-library/package.jsonindex.tsnode_modules/luxon/ -> ../../../common/temp/node_modules/luxon
Observe that:
- The main dependencies store is tucked away in a subdirectory
common/temp/node_modules/
. - Each project declares its own dependencies in a
package.json
file. - Rush creates a
node_modules
directory in each project, containing symbolic links to dependencies which were declared inpackage.json
. Links are created for both npm dependencies and inter-dependencies on other projects in the monorepo.
The result of this structure is that modules in projects are prevented from accidentally importing a package which haven't been declared in package.json
. This is because the Node.js package resolution algorithm doesn't find a folder called node_modules
as it walks up the directory tree from within projects/
.
This idea of ensuring that only declared dependencies are available in node_modules
is especially important when publishing npm packages from the monorepo. Without this guarantee, a package which imports an undeclared dependency might "work" in the monorepo, but it will fail when published to the npm registry.
Read more about Rush's unique approach here.
Nx
Nx is not opinionated about how npm dependencies are installed. You can directly use any package manager with Nx, whether npm, Yarn, pnpm, or others. Nx does not apply any special directory structure or symbolic links, so there are no built-in safety guaratees like those provided by Rush.
It's possible to use Yarn workspaces or pnpm workspaces with Nx, which can provide some additional safety guarantees. However, a node_modules
folder still must exist at the root of the monorepo to store Nx's own npm dependencies, so these may still be accidentally imported by individual projects.
There is a feature request for Nx to more directly support per-project npm dependencies: https://github.com/nrwl/nx/issues/1777
For projects which depend on other projects in the monorepo, Nx uses TypeScript's tsconfig.json
"paths"
config to enable imports across projects:
my-monorepo/package.jsontsconfig.jsonnode_modules/react/luxon/projects/my-app/index.tsmy-datetime-library/index.ts
tsconfig.json
{"compilerOptions": {"paths": {"@my-monorepo/my-app": ["projects/my-app/index.ts"],"@my-monorepo/my-datetime-library": ["projects/my-datetime-library/index.ts"]}}}
my-app/index.ts
import { DateTime } from '@my-monorepo/my-datetime-library';// ...
This is a simpler approach, however it relies on every project being written in TypeScript in order to work. It requires additional setup to write a plain JavaScript library, or to use other compile-to-JS languages.
Nx supports some types of build targets which do not use tsc
directly, such as Cypress, which uses webpack internally. In these cases, tsconfig-paths-webpack-plugin is used to augment webpack's module resolution to resolve cross-project dependencies.
Conclusion
Rush and Nx each serve similar purposes, but with slightly different philosophies.
Rush emphasizes safety of npm dependencies and adherence with conventions used in Node.js, in particular package.json
, npm scripts, and the Node.js package resolution algorithm to resolve project inter-dependencies.
Nx emphasizes extensibility with plugins and code generation, making it easy to scaffold new projects quickly and ensure that builds and code are consistent across projects. Common large-codebase requirements like linting, formatting, and planning CI builds are built in.
As with anything in technology, there are tradeoffs for each approach. It often comes down to organizational requirements, and ultimately developer preferences.
Personally, I prefer Rush for its safety guarantees, adherence to Node.js conventions, and greater control of builds via npm scripts.
Here's a list of some overlapping technologies to explore further: