In large-scale engineering organizations, developing UI components, shared utilities, and tooling across isolated polyrepos (a separate Git repository per library) results in organizational inertia:
npm link dependency divergence.A Monorepo architecture powered by pnpm workspaces and the Turborepo task orchestrator dismantles these friction points.
Combining pnpm and Turborepo creates an industrial-grade foundation:
In a monorepo workspace, code is logically structured into executable applications (apps/*) and shared internal packages (packages/*):
┌──────────────────────────────────────────────────────────────────────────┐
│ ROOT MONOREPO │
│ pnpm-workspace.yaml | turbo.json | package.json │
└─────────────────────┬──────────────────────────────┬─────────────────────┘
│ │
┌─────────────▼────────────┐ ┌─────────────▼────────────┐
│ apps/web │ │ apps/docs │
│ (Next.js 15+) │ │ (Storybook) │
└─────────────┬────────────┘ └─────────────┬────────────┘
│ depends on: │ depends on:
│ - @repo/ui │ - @repo/ui
│ - @repo/tailwind-config │ - @repo/tailwind-config
│ │
┌─────────────┴──────────────────────────────┴────────────┐
│ SHARED PACKAGES │
│ │
│ ┌───────────────────┐ ┌────────────────────────────┐ │
│ │ @repo/ui │ │ @repo/tailwind-config │ │
│ │ (React 19 Kit) │ │ (Tailwind v4 Presets) │ │
│ └─────────┬─────────┘ └────────────────────────────┘ │
│ │ depends on: │
│ ┌─────────▼─────────┐ ┌────────────────────────────┐ │
│ │ @repo/tsconfig │ │ @repo/eslint-config │ │
│ │ (Strict TS) │ │ (Shared Linting Rules) │ │
│ └───────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘When an engineer runs pnpm build, Turborepo:
inputs, environment variables, and dependent packages.dist/ artifacts (>>> FULL TURBO), bypassing TypeScript compilation in roughly 50ms.Create packages/analytics and initialize a package.json with unique name @repo/analytics.
Add @repo/tsconfig and @repo/eslint-config to devDependencies via the workspace:* protocol.
Define export entry points in package.json and specify react and react-dom under peerDependencies.
In apps/web/package.json, add '@repo/analytics': 'workspace:*' to dependencies.
Execute pnpm turbo run build --filter=@repo/analytics... to validate the task execution graph.
How does the pnpm workspaces architecture prevent Phantom Dependencies, which frequently plague classic npm environments?
@company/ui package is consumed simultaneously by a customer-facing Next.js web application, a React Native mobile client, and a Vite-based back-office admin dashboard.source field in package.json, eliminating tedious watch builds for internal packages.--filter=...[HEAD^1] flag triggers builds and tests strictly for the altered package and its direct consumers.pnpm update -r react@latest, preventing version fragmentation across teams.packages:
- 'apps/*'
- 'packages/*'The turbo.json file configures the build pipeline topology:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}dependsOn: ["^build"]: The ^ symbol specifies topological dependency. Before apps/web builds, Turborepo must successfully compile all internal packages in packages/* that apps/web depends upon.cache: false, persistent: true: Dedicated mode for persistent development servers.The @repo/ui component package must never bundle its own copy of React. Instead, it marks react and react-dom as peerDependencies:
{
"name": "@repo/ui",
"version": "1.4.0",
"private": false,
"type": "module",
"exports": {
"./button": {
"types": "./dist/button.d.ts",
"import": "./dist/button.js"
},
"./card": {
"types": "./dist/card.d.ts",
"import": "./dist/card.js"
},
"./styles.css": "./dist/index.css"
},
"scripts": {
"build": "tsup src/index.ts src/button.tsx src/card.tsx --dts --format esm",
"dev": "tsup src/index.ts --watch --dts --format esm",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.2"
}
}Engineer modifies @repo/ui, executes pnpm changeset, and selects the version bump type (patch, minor, major).
GitHub Actions executes turbo run test build exclusively for affected workspace packages.
Bot opens an automated Version Packages PR, bumping versions in package.json and updating CHANGELOG.md.
On PR approval, packages are published to npm/GitHub Packages registry and Git release tags are created.
What is the exact purpose of the dependsOn: ['^build'] directive inside the 'build' task of turbo.json for apps/web?
Here is a production-grade tsup bundler configuration for a design system package, creating tree-shakeable ESM outputs with .d.ts types and automatic React 19 'use client' banner injection:
import { defineConfig, type Options } from 'tsup';
export default defineConfig((options: Options) => ({
// Multiple entry points for granular consumer tree-shaking
entry: [
'src/index.ts',
'src/components/Button/index.ts',
'src/components/Card/index.ts',
'src/components/Modal/index.ts',
],
format: ['esm'],
dts: true,
clean: !options.watch,
sourcemap: true,
minify: !options.watch,
target: 'es2022',
external: ['react', 'react-dom'],
// Auto-inject 'use client' directive for Next.js App Router client boundary support
banner: {
js: "'use client';",
},
esbuildOptions(esbuildOptions) {
esbuildOptions.jsx = 'automatic';
},
}));In hoisted package structures, apps/web might import a library (e.g. lodash) that is omitted from its own package.json but installed inside packages/ui. In an isolated Docker build or clean CI environment, the build immediately crashes with Module not found.
// ❌ Anti-pattern: Missing explicit dependency in apps/web/package.json
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*"
// lodash is omitted, yet locally resolves due to flat hoisting!
}
}
// ✅ Production Pattern: Explicitly declare all direct dependencies
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*",
"lodash": "^4.17.21"
}
}If turbo.json fails to define specific inputs, Turborepo will invalidate the build cache whenever any file in the repository changes (such as a README.md or documentation asset).
// ❌ Anti-pattern: Any documentation commit invalidates the build cache
"build": {
"outputs": ["dist/**"]
}
// ✅ Production Pattern: Explicit source file matching array
"build": {
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
}If @repo/auth depends on @repo/ui while @repo/ui imports types or hooks from @repo/auth, Turborepo cannot construct a valid execution DAG, throwing Circular dependency detected.
Remedy: Extract shared contracts, primitives, and interfaces into an independent leaf package such as @repo/types or @repo/contracts.