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-checkoutandpost-merge, createpackage.jsonfiles in every .NET workspace folder, containing the above scripts, without thedotnet:prefix. I called this asyncoperation, and it could be executed withpnpm syncin 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.