If you are setting up a TypeScript monorepo in 2026, start with pnpm workspace + TypeScript Project References. Add Turborepo when build time, task orchestration, CI caching, or Docker packaging becomes a real bottleneck. If your repo is still a simple split like apps/web, apps/wxt, packages/ui, and packages/core, pnpm workspace + Biome + TypeScript Project References is usually enough.
This article compares those two setups, explains when pnpm + Turborepo is worth the added complexity, and shows how to keep package boundaries clean. The goal is not to collect every tool up front, but to add complexity only when the repo has earned it.
TL;DR
- Start with
pnpm workspace + TypeScript Project References. - Add
Turborepowhen build, test, lint, or CI time becomes painful. - If you have a small
apps/web+apps/wxt+ shared packages repo,pnpm workspace + Biomeis often enough. - Use
workspace:andexportsfor cross-package boundaries. Do not rely ontsconfig.pathsalone.
Recommended setup by repo shape
| Prerequisites | First configuration to choose | Supplement |
|---|---|---|
| New generic monorepo. In the future, package release, CI optimization, and Docker integration will be considered | pnpm workspace + Turborepo + TypeScript Project References + package.json of exports + Changesets | The most stable |
4 package configuration: apps/web, apps/wxt, packages/ui, packages/core. I don’t use Docker. Do not publish shared packages to npm | pnpm workspace + Biome + TypeScript Project References | Turborepo is not needed at this stage |
| Bun provides the execution environment, package manager, and testing | bun workspace is also a candidate | However, pnpm is strong in terms of safe operation |
In short, pnpm + turbo is the strongest general-purpose answer. However, in this specific four-package setup, workspace only is often enough.
Default setup for a new TypeScript monorepo
As of 2026, pnpm workspace is still the safest default foundation for a TypeScript monorepo. Workspaces are built in, local package references are explicit with the workspace: protocol, and --filter gives you precise control over which packages to run against.
Use TypeScript Project References for type boundaries. The TypeScript official documentation also clearly states that split projects can improve build time, logical separation, and build in dependency order using tsc --build. This is quite effective in monorepo.
If you add Turborepo on top of that, you get task graphs, caching, parallel execution, and turbo prune in one step. Turborepo task configuration is mostly about declaring dependsOn and outputs, so it scales well as the repo grows.
If you have a package to publish, Changesets will naturally fit in as well. pnpm’s workspace documentation also assumes that a dedicated tool such as Changesets is used for versioning of packages in a workspace.
For a general-purpose repo, the default stack looks like this.
| Role | Recommendation |
|---|---|
| workspace management | pnpm workspace |
| Type Boundaries and Incremental Builds | TypeScript Project References |
| Task execution, parallelization, and caching | Turborepo |
| public face of package boundary | exports of package.json |
| versioning / changelog | Changesets |
| lint / format | Biome or ESLint + Prettier. It’s easier to start with Biome now |
When Turborepo is still unnecessary
The configuration this time is fairly simple.
packages/corepackages/ui -> packages/coreapps/web -> packages/ui, packages/coreapps/wxt -> packages/ui, packages/coreIf the dependencies are at this level, running the filter on pnpm workspace and tsc -b is sufficient for operation.
The value of Turborepo jumps quickly in situations like these.
| Pain | Why Turborepo works |
|---|---|
| Total build / test / lint time is long | Caching and parallel execution are effective |
| I want to clarify the dependency order of tasks | dependsOn can express a graph |
| I want to speed up CI | Remote cache works |
| I want to extract only the necessary packages for Docker | turbo prune can be used |
On the other hand, if you install it at a stage where you are not yet in such trouble, it is difficult to see the value beyond adding one more configuration file. If you turn off Docker, the appeal of prune will drop a notch. Changesets is also not required if you do not publish shared packages on npm.
Therefore, based on this premise, it is reasonable to start with pnpm workspace + Biome + TypeScript Project References.
How to keep package boundaries clean
In this four-part division, it is more important to avoid ambiguity in roles than in tool selection.
| Path | Things to put | Things you shouldn’t put |
|---|---|---|
packages/core | domain, schema, validation, API client, pure function, state model | UI, routing, browser API |
packages/ui | Environment-independent React component / hook | WXT-specific processing, router, extension storage |
apps/web | Assembly as a web application, page, routing, adapter | extension specific circumstances |
apps/wxt | background / content script, browser API, WXT-specific settings | Web-specific routing, pure logic that can be placed in the shared layer |
The last thing I want to avoid is turning packages/ui into a place for all things that are shared. When browser extension specific circumstances and routing start to enter here, it becomes a connection point rather than a shared layer. If that happens, the benefits of monorepo will diminish.
Notes for apps/wxt
You should be a little careful about apps/wxt. WXT TypeScript settings usually extend .wxt/tsconfig.json from the root tsconfig.json. In a monorepo you may not take that shape, in which case you need to include .wxt/wxt.d.ts in the TypeScript project for apps/wxt.
In other words, although apps/wxt is part of the monorepo, it is more stable to treat it as an independent TypeScript project.
If your extension also uses React, How to Enable React Compiler in WXT is a good example of keeping WXT-specific build settings inside the app package instead of leaking them into shared packages.
Do not use tsconfig.paths as your main cross-package boundary
This is quite important.
TypeScript 5.7 release notes clearly states that imports that depend on baseUrl and paths cannot be rewritten when outputting JS. In other words, a design that crosses package boundaries using only tsconfig.paths may pass type checking, but is likely to be distorted at the execution or publishing stage.
Basically, you should consider cross-border references in monorepo in the following order.
- Separate packages
- Connect dependencies with
workspace: - Decide the public side with
exportsofpackage.json - Create a dependency order build with
TypeScript Project References
paths is less likely to be damaged if it is kept as an auxiliary within the package.
Why Biome fits this setup
I think the decision to remove ESLint and focus on Biome is quite natural. Biome’s big projects guide also organizes the premise of using root config and nested config differently for large repos such as monorepo and workspace. In v2, a configuration based on monorepo is also introduced.
At this scale, it is usually more straightforward to standardize lint and format on Biome and keep project boundaries and builds on the TypeScript side.
If you are standardizing Biome across the repo, these follow-ups are useful:
- Disable Biome Unused Variable Warnings in Astro/Vue/Svelte Files
- Biome npm-scripts Configuration
- Biome
Minimal setup checklist
This is enough for the initial setup.
- Place
pnpm-workspace.yamlin root - Place solution-style
tsconfig.jsonin root andreferenceseach package packages/coreandpackages/uienablecomposite- Shared package specifies
exportsofpackage.json - Place
biome.jsonin root - For
apps/wxt, check separately how to handle WXT type definitions.
At this point, you can fight for quite a long time.
When to add the next tool
| What to add | When to add |
|---|---|
Turborepo | Build / test / lint is heavy, CI optimization is required, I want to clarify the task graph |
Changesets | I want to version-manage and publish a shared package |
Nx | I wanted automatic generation, polyglot, and stronger monorepo management |
Migrating to bun workspace | I wanted to unify runtime / package manager / test to Bun |
With monorepo, it works better to add only where the pain occurs, rather than adding everything from the beginning.
Summary
There is no single optimal solution for TypeScript monorepo. pnpm workspace + Turborepo + TypeScript Project References is strong for general-purpose non-difficult answers. However, if Docker and package publishing are not too heavy yet after dividing into apps/web, apps/wxt, packages/ui, and packages/core, it would be lighter and less likely to break down if you start with pnpm workspace + Biome + TypeScript Project References.
The important thing is not to increase the number of tools, but to keep core and ui thin, and express cross-border references honestly with workspace: and exports. If that doesn’t collapse, it’s not too late to add turbo later.
hsb.horse