Monorepo solutions provide a scalable architecture and tooling configuration for complex projects. If you have ever needed to deploy a Node application and publish npm packages that are related, try a modern approach with Rush.
This article will cover monorepos that maintain Node.js-based projects. A monorepo is a single repository that manages multiple projects. Google, Facebook, and Microsoft (the founder of this article’s proposed solution) have taken the leap toward monorepo architecture to battle massive code and commit volume. These projects can be isolated components or utilities that are published to package repositories or full-blown framework-based projects like Next.js, Vue, and Angular applications.
The traditional multirepo model, where multiple VCS repositories are used to maintain isolated codebases, often starts as a simple solution. This approach can quickly become cumbersome to work with as codebases grow in size and complexity or as the number of code repositories becomes unmanageable. This is where a monorepo solution would excel.
Consider the case where a development team is working on a single web application, but they now need to share code for consumption by another team. These might be core utilities or even smart components. The team successfully factors code into a separate repository, complete with a CI/CD system that can publish packages. PR reviews are now split across repositories. A developer working on the main web application now needs to wait for a new package to be published so that they can use their own library in the next web release. This problem is compounded as the number of these repositories grows.
In a monorepo solution,
We’ve all worked on projects that have tightly coupled, monolithically architectured implementations. One major benefit of a monorepo solution is that it can provide a healthy starting point for modularizing your application.
Is there a well-designed component styling solution for your React application from which other teams could benefit? Would having the styling factored out make styling changes easier by isolating its code in a single package? Factor out your styling into a package. Do you have a linting configuration that could be propagated throughout your organization? Do you maintain an analytics implementation that could be consumed by multiple third parties?
When the proper tooling is in place, monorepos make it easy to start publishing packages that would otherwise require heavy lifting to coordinate into a separate repository and configure CI/CD processes. When designed properly, publishing a new package for the first time can be as easy as setting ”shouldPublish”: false to ”shouldPublish”: true.
Internal utilities or implementations are commonly duplicated across different projects and repositories, especially when a packaging solution is not immediately available. A monorepo approach makes it possible to factor these into shared packages without the need to publish and bump these references down a long chain of dependencies. In particular, Rush can make use of a variety of versioning policies to ease the strain of cascading version bumps.
A transitive dependency is any dependency in the dependency tree that is pulled in by direct dependencies and is not specified as a direct dependency in a project’s package.json. When our Node applications grow, it is not uncommon to see transitive dependencies in the order of thousands. Install times can be reduced by caching shared dependencies across all monorepos. By virtue of PNPM or Yarn workspaces, Rush can:
Rush, a monorepo solution created by the platform team for Microsoft SharePoint, enters the conversation as a relatively underutilized solution when it comes to monorepo management. Tools such as Lerna have become mainstream, but there are growing concerns around its longevity as a project, given that a single developer has been contributing most of its code over the last three years. Consumers have been wondering if PNPM support will be introduced since 2018! It may not be introduced soon, given the project’s health, even though it has well-documented speed improvements over NPM and Yarn.
Web developers and architects alike enjoy having options when it comes to any solution. Package management is no different in this regard—the three most popular package managers today are:
Rush supports each of these agnostically, although each package manager’s performance impacts Rush itself. Yarn and PNPM work especially well with Rush because they both support workspaces, which allows the package manager to install and collect dependencies for all projects in a single pass. PNPM is a particularly interesting option with Rush, as it provides extra safety around phantom dependencies and the NPM doppelgangers issue.
Different transpilation, linting, and bundling tools (Typescript, ESLint, Webpack, Rollup, etc.) often have their own caching layer to reduce incremental build times. Rush takes this a step further with their own build caching solution.
With build caching, Rush will create a tarball of each project’s build output. When subsequent builds occur, Rush can restore this build output instead of rebuilding the project, skipping the overhead that a no-op incremental build would have incurred.
This feature becomes especially powerful when combined with remote storage integration. Rush can leverage an Amazon S3 or Azure blob storage container to cache builds across any machine with the appropriate credentials. In such a configuration, developers would be able to pull PRs or branches that were built by CI and run code with practically zero build time.
Note that build caching with Rush is experimental as of the time of writing.
When working with many dependencies spread across multiple projects, it can be helpful to ensure that only one version of a package is in use within any given project, which implies the following:
Rush can enforce consistent versioning when performing dependency installations or additions.
With Rush, it becomes easy to communicate and track changes across publishable packages. When publishable packages undergo source file changes on a branch separate from the main branch, they are flagged by the rush change --verify command. This instructs the developer to run rush change, if they haven’t already, and for each modified package this utility guides them through:
This is easily integrated into a PR trigger to ensure that no changes go undocumented. When the next package is triggered, each project gets an up-to-date CHANGELOG.md file which compiles a listing of tracked changes.
Rush has an active Zulip chat room where questions and concerns that may not be covered in documentation or GitHub issues can be addressed often more quickly than one may expect of other monorepo tooling teams.
Following from a minimal Rush example provided by the Rush Stack team, a standard directory structure may look as such:
│ └── my-app
│ ├── package.json
│ └── src
│ │── config
│ │ └── rush
│ │── git-hooks
│ │ └── commit-msg.sample
│ │── scripts
│ └── ...
│ └── my-controls
│ ├── lib
│ ├── package.json
│ └── src
This directory structure separates concerns in a comfortable manner. Note that each project within the monorepo has a familiar structure as well: src for source code, a package.json for specifying dependencies, and potentially a lib directory for build outputs.
Once configured, moving an existing web application into a Rush directory structure may be as simple as dropping tracked files into apps/<name-of-your-app>. From there, you can consider what is useful to share or reuse as the monorepo grows and you begin populating the libraries directory.
Monorepo solutions serve as a step toward a more collaborative development experience. Code reuse becomes easy. Dependency management is safer. This shouldn’t be the default for all Node projects (especially smaller ones), but if you need it, it’s a lifesaver.
From the rush --help command itself:
If you’re interested in learning how a monorepo solution could improve your team’s workflow, read the Rush Stack documentation for more information. They have guides geared towards developers and repository maintainers.
We’d love to learn more about your project.
Engagements start at $75,000.