Monorepo for .NET and NodeJS workspaces

Wait, what?

Ikechi Michael
3 min readFeb 18, 2024

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 and post-merge, create package.json files in every .NET workspace folder, containing the above scripts, without the dotnet: prefix. I called this a sync operation, and it could be executed with pnpm 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 and org-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.

--

--

Ikechi Michael

I’ve learned I don’t know anything. I've also learned that people will pay for what I know. Maybe that's why they never pay.