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.