monorepo-setup
SKILL.md
Monorepo Setup
Configure a scalable monorepo with Turborepo and pnpm workspaces.
Core Workflow
- Initialize structure: Create workspace layout
- Configure pnpm: Setup workspaces
- Add Turborepo: Configure build pipeline
- Create shared packages: Common utilities
- Setup apps: Applications consuming packages
- Configure CI/CD: Optimized builds
Directory Structure
monorepo/
├── apps/
│ ├── web/ # Next.js app
│ ├── api/ # Express/Fastify API
│ └── mobile/ # React Native app
├── packages/
│ ├── ui/ # Shared UI components
│ ├── config/ # Shared configs
│ ├── tsconfig/ # TypeScript configs
│ ├── eslint-config/ # ESLint configs
│ └── utils/ # Shared utilities
├── tooling/
│ ├── scripts/ # Build scripts
│ └── docker/ # Docker configs
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
└── .npmrc
Root Configuration
pnpm Workspace
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
Root Package.json
{
"name": "monorepo",
"private": true,
"packageManager": "pnpm@8.15.0",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=8.0.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"typecheck": "turbo run typecheck",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,md,json}\"",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build --filter=./packages/* && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.0",
"prettier": "^3.2.0",
"turbo": "^2.0.0",
"typescript": "^5.3.0"
}
}
NPM Configuration
# .npmrc
auto-install-peers=true
strict-peer-dependencies=false
shamefully-hoist=true
node-linker=hoisted
# Private registry (optional)
# @company:registry=https://npm.company.com/
# //npm.company.com/:_authToken=${NPM_TOKEN}
Turborepo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV", "CI"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["DATABASE_URL", "API_URL"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"clean": {
"cache": false
}
}
}
Shared TypeScript Config
// packages/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "bundler",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true
},
"exclude": ["node_modules"]
}
// packages/tsconfig/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"module": "ESNext",
"target": "ES2022",
"jsx": "preserve",
"noEmit": true,
"plugins": [{ "name": "next" }],
"allowJs": true,
"incremental": true,
"resolveJsonModule": true
}
}
// packages/tsconfig/library.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Library",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022"],
"module": "ESNext",
"target": "ES2022",
"outDir": "dist",
"rootDir": "src"
}
}
Shared UI Package
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./styles.css": "./src/styles.css"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@types/react": "^18.2.0",
"react": "^18.2.0",
"tsup": "^8.0.0",
"typescript": "^5.3.0"
}
}
// packages/ui/src/button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', className = '', ...props }, ref) => {
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
};
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
{...props}
/>
);
}
);
Button.displayName = 'Button';
// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Card, type CardProps } from './card';
// packages/ui/tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
external: ['react', 'react-dom'],
});
Shared Utils Package
// packages/utils/package.json
{
"name": "@repo/utils",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.2.0"
}
}
// packages/utils/src/index.ts
export { cn } from './cn';
export { formatDate, formatRelativeTime } from './date';
export { debounce, throttle } from './timing';
export { sleep, retry } from './async';
// packages/utils/src/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
Next.js App Configuration
// apps/web/package.json
{
"name": "@repo/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
},
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0"
}
}
// apps/web/tsconfig.json
{
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
// apps/web/src/app/page.tsx
import { Button } from '@repo/ui/button';
import { formatDate } from '@repo/utils';
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">Monorepo App</h1>
<p className="mt-4">Today is {formatDate(new Date())}</p>
<Button className="mt-4">Click me</Button>
</main>
);
}
ESLint Config Package
// packages/eslint-config/package.json
{
"name": "@repo/eslint-config",
"version": "0.0.0",
"private": true,
"exports": {
"./base": "./base.js",
"./next": "./next.js",
"./react": "./react.js",
"./library": "./library.js"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0"
}
}
// packages/eslint-config/base.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
plugins: ['@typescript-eslint'],
env: {
node: true,
es2022: true,
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
},
ignorePatterns: ['dist/', 'node_modules/', '.turbo/'],
};
// packages/eslint-config/next.js
module.exports = {
extends: [
'./base.js',
'next/core-web-vitals',
'plugin:react-hooks/recommended',
],
rules: {
'react/react-in-jsx-scope': 'off',
},
};
CI/CD Configuration
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Test
run: pnpm test
Changeset Configuration
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@repo/web", "@repo/api"]
}
Best Practices
- Workspace protocol: Use
workspace:*for internal deps - Shared configs: Centralize TypeScript, ESLint
- Build caching: Enable Turborepo remote caching
- Incremental builds: Use
dependsOncorrectly - Package exports: Use proper
exportsfield - Version management: Use Changesets
- Clean scripts: Include clean in each package
- Consistent naming: Use
@repo/prefix
Output Checklist
Every monorepo should include:
- pnpm-workspace.yaml configuration
- turbo.json with proper pipeline
- Shared TypeScript configs
- Shared ESLint configs
- UI component library
- Utility package
- Proper package.json exports
- tsup for library building
- CI/CD with caching
- Changeset for versioning
Weekly Installs
11
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code8
gemini-cli7
antigravity7
windsurf7
github-copilot7
codex7