← All articles

The Perfect TS Monorepo: A 2026 Addendum

(updated )

The original My Quest for the Perfect TS Monorepo has been sitting with a “this is starting to feel outdated” banner on it for over a year. Rather than trying to rewrite it, I want to leave it as a record of what the landscape looked like in 2023 and 2024, and add this short addendum covering what has actually shifted since.

The targets I listed as essential have not changed:

  • Fast, deterministic task orchestration with caching
  • ESM in and out
  • Packages that can be deployed in isolation
  • IDE go-to-definition into the TypeScript source
  • Live updates across packages during development

What has changed is that a lot of the ways I used to have to achieve those targets have become either easier or irrelevant.

What Actually Decides Build-or-Not

Correction (June 2026): The version of this section I first published in April 2026 said the debate was settled and that “building wins,” full stop. That was wrong, or at least far too absolute. Building is the right answer for this boilerplate, but not because building always wins; it wins here for a specific reason that I had flattened into a slogan. The corrected version follows.

The original article weighed two patterns for shared packages: the unbuilt “internal packages” approach, where the manifest points straight at TypeScript source, versus the built-package approach with a bundler producing dist/. I leaned toward the latter and presented both as legitimate. That instinct was fine; the “building always wins” framing I later reached for was not.

What actually decides it is whether a bundler already owns your deploy path.

This boilerplate deploys its backend to Firebase Functions, and Firebase has no bundler in its deploy path. It uploads a directory shaped like an installed npm package and runs the entry point. So your shared packages have to be real, resolvable, built packages, and that is exactly why the boilerplate builds: packages are bundled with tsdown, TypeScript project references link them so both the IDE and tsc --noEmit resolve types back to source, nothing runs tsc --build, and nothing generates tsbuildinfo files. TypeScript is purely a type checker; tsdown does all the compilation. You get cacheable output, clean .mjs imports, and IDE go-to-definition into source, all at once. On top of that, isolate-package (see the Firebase section below) needs those built packages to assemble a self-contained deployable. Building is not a stylistic preference here; it is load-bearing.

But flip the deploy target and the answer flips with it. I am now building a larger production monorepo on a completely different stack: Cloudflare Workers for the APIs, Vite and Astro for the frontends, and a React Native app on the side. Every one of those targets runs its own bundler (wrangler/esbuild, Vite, Astro, Metro) that consumes the whole workspace graph, raw .ts and all. There the shared packages export TypeScript source directly: no build step, no dist/, no project references, no composite. The manifest just points exports at ./src/index.ts. It works flawlessly, the type-check is fast, go-to-definition lands on the real source for free, and there is simply nothing to build, because the consumer was always going to bundle anyway. The “internal packages” approach I had written off is the obviously correct choice on that stack.

So the honest version: do not ask “should I build my shared packages?” in the abstract. Ask whether something downstream is already going to bundle them. If a real bundler owns your deploy path, lean on it and ship source. If your platform wants an installable, self-contained package tree (Firebase Functions, a bare node dist/index.js, anything you publish to npm), then build. Both of my own monorepos are “right”; they just answer to different deploy targets.

ESM Is Boring Now

A big chunk of the original article was about ESM and CJS interop. In the boilerplate today every package declares "type": "module" and outputs .mjs. Node 24 runs ESM natively, Next.js 16 runs ESM natively, firebase-functions runs on Node 24. The paper cuts around dynamic imports, file extensions, VSCode auto-import settings, and Webpack workarounds still technically exist, but they rarely matter in a greenfield project. If you are not producing CJS anywhere, most of that section is not going to bite you.

The Toolchain Has Consolidated

ESLint and Prettier are gone from the boilerplate, and a short detour through Biome along the way. In their place: oxlint and oxfmt from the oxc project. Both are Rust-based and fast enough that the combined pre-commit step (via lefthook, running in parallel on staged files) feels instant. oxlint has a type-aware mode that is genuinely reliable, and import/no-cycle catches circular imports at the root, removing the need for a separate pass.

tsdown also quietly closed the declaration-map story. In the original I had to suggest running the bundler and then tsc --emitDeclarationOnly to get .d.ts.map files for go-to-definition. tsdown emits both the .d.ts and the declaration maps directly, so the extra step is no longer needed.

On the TypeScript side, @codecompose/typescript-config has matured to the point where every tsconfig in the boilerplate is two or three lines of extends and references. It ships presets for shared-library, service, and nextjs, leaning on the ${configDir} substitution that TypeScript introduced in v5.5. Nothing revolutionary. Just the right amount of abstraction to stop copying twenty-line tsconfigs around.

Firebase Deployment Is Mostly Painless

The original’s Firebase section described a manual dance with isolate-package, edits to firebase.json, and a fork of firebase-tools to keep emulator hot-reload working. That is all condensed down to swapping one dependency. Replace your firebase-tools with firebase-tools-with-isolate, and isolation runs automatically at deploy time whenever the functions source directory sits inside a detected monorepo. No flag, no rewiring. Emulators keep running against source. Standalone projects are unaffected. Both pnpm and npm workspaces are supported; faithful npm lockfile pruning just landed in a recent release.

I do want to be honest about what this is, though. Firebase still does not officially support monorepos, and the solution is still a fork of the official tooling. It is a well-behaved fork (roughly 50 lines of patch on top of upstream, republished automatically whenever upstream cuts a release, with the version number tracking upstream one-to-one, so switching back to stock firebase-tools is a single dependency change) but a fork is a fork. Until the isolation step lives upstream or Firebase reworks its deploy pipeline to understand workspaces natively, anyone adopting this is relying on a third party to keep the patches current.

The day-to-day pain of the problem is gone. The architectural gap underneath is not.

What Remains The Same (But Matters Less Now)

The original article was structured as a knowledge transfer. “Here are the twelve things I had to learn the hard way, so you do not have to.” Most of those things are still true: CJS cannot import ESM at the top level, path aliases still need to be thought about, project references still require composite: true in the right places, and Firebase still does not understand workspaces natively.

What has changed is how much of that you personally need to hold in your head.

A coding agent in 2026 has seen thousands of monorepo setups. It knows the common shapes, the common gotchas, the current crop of bundlers, and the idiomatic way to wire up project references. Setting up this kind of plumbing is exactly the shape of work a modern coding agent handles well: mechanical, well-documented, and with a clear right answer in most cases. A lot of the discipline and recall these kinds of setups used to require has been offloaded. (The same theme shows up in my barrel files article.)

That changes what an article like the original is for. It is no longer a reference you memorize. It is more useful as context you hand to an agent, along with a running boilerplate to point at, so it can produce something similar in shape. The boilerplate at github.com/0x80/typescript-monorepo is more valuable as a machine-readable reference in 2026 than it was in 2023.

This is not a suggestion that you ignore how monorepos work. You still want to recognize when something is off, and you still want opinions about the trade-offs. But the bar for actually setting one up has dropped significantly, and most of the original article’s details have moved from “things you must know” to “things the agent will handle, unless you want to override it”.

In Short

If you read the original article today, treat the overall shape of the problem as still accurate, and the specific prescriptions as outdated in ways the boilerplate and this addendum cover. The goals of a good TypeScript monorepo in 2026 are the same as they were in 2023. Achieving them is a lot quieter than it used to be.

If you would like to leave feedback or have questions about this article, feel free to contact me on Bluesky . You can also follow me there to find out when new articles are published.