Monorepo for .NET and NodeJS workspaces
Wait, what?
This was my challenge a few months ago, to setup a monorepo where both .NET and NodeJS projects could live. It was for organization that maintains a ton of .NET and NodeJS projects, and needed a repo to contain its common libraries, packages, and tools.
So, what?
The requirements were:
It should be possible to build, test, and restore all projects, whether .NET or NodeJS (RQ1)
It should be possible to build, test and restore each individual project (RQ2)
The CI pipeline should be able to detect changes to a specific project and run its build pipeline. (RQ3)
The cognitive load on the dev team for using this should be minimal. (RQ4)
Okay, now what?
At first, I considered using turborepo because I had used it to maintain monorepos before, but I eventually switched to using pnpm because it had a much lower cognitive overhead than the former.
My strategy was simple:
Every project whether NodeJS or .NET would be an npm workspace
the root package.json would contain scripts like:
{
"scripts": {
"dotnet:restore": "dotnet restore",
"dotnet:build": "dotnet build",
"dotnet:test": "dotnet test"
}
}
Using git hooks on
post-checkout
andpost-merge
, createpackage.json
files in every .NET workspace folder, containing the above scripts, without thedotnet:
prefix. I called this async
operation, and it could be executed withpnpm sync
in the root folder.The above, meant we needed a way to distinguish between .NET and NodeJS project folders, so I went with a naming pattern:
OrgName.*
for .NET andorg-name-*
for NodeJS.
So on pnpm sync
, every .NET workspace folder would get the latest npm scripts.
It goes without saying that the generated OrgName.*/package.json
files are git ignored, so they don’t get into version control.
So a typical folder setup looks like:
- org-name-a:
- package.json
- org-name-b:
- package.json
- OrgName.C:
- package.json (git ignored)
- OrgName.D:
- package.json (git ignored)
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
Solving RQ1 and RQ2
The pnpm-workspace.yaml
file looks like:
packages:
- OrgName.*
- org-name-*
This ensures that running pnpm install
will install dependencies for all NodeJS workspaces, while pnpm -r restore
will install dependencies for all .NET workspaces.
This also means you can cd into any workspace folder and run npm i
for NodeJS and dotnet restore
for .NET.
Solving RQ3
pnpm
has the ability to run commands only in workspaces that have changed since a parent git branch, or whatever.
e.g. pnpm --filter "[origin/master]" -r build
Solving RQ4
Using pnpm in this setup is completely optional for devs who just want to add a workspace folder or modify one.
As soon as they checkout to a new branch or pull from the target branch, the sync
script runs to ensure their workspace folder is synced as a workspace.
When they push their changes, the CI detects the change in their workspace folder, and runs restore
, test
, and build
.
It also increments the package version and publishes the new package to nuget or npm, but that’s a different requirement for another blog post.