Monorepo Setup 🏢
Learn how to set up a monorepo with Presetter, sharing configurations across multiple packages while allowing individual customizations. Perfect for organizations with multiple related projects.
Overview
This tutorial covers:
- Setting up a pnpm workspace
- Configuring shared Presetter settings
- Managing per-package configurations
- Cross-package dependencies
- Build orchestration
Prerequisites
- Node.js 20+ and pnpm installed
- Understanding of package workspaces
- Basic familiarity with monorepo concepts
Step 1: Create Monorepo Structure
Initialize your monorepo workspace:
mkdir my-awesome-monorepo
cd my-awesome-monorepo
# Initialize root package.json
pnpm init
# Create workspace structure
mkdir packages apps tools
Create workspace configuration:
packages:
- "packages/*"
- "apps/*"
- "tools/*"
Step 2: Configure Root Presetter Setup
Install Presetter at the root level:
# Install Presetter and monorepo preset
pnpm add -D presetter @presetter/preset-monorepo
# Bootstrap root configuration
presetter bootstrap
Configure the root Presetter setup:
import monorepo from '@presetter/preset-monorepo';
import { preset } from '@presetter/types';
export default preset('my-monorepo', {
extends: [monorepo],
variables: {
// Shared variables for all packages
nodeVersion: '18',
source: 'src',
output: 'lib'
},
override: {
assets: {
// Root-level configurations
'.gitignore': [
'node_modules/',
'*/node_modules/',
'dist/',
'lib/',
'build/',
'coverage/',
'.env.local',
'.DS_Store'
]
}
}
});
Step 3: Create Package Templates
Library Package Template
Create a shared library package:
mkdir packages/shared-utils
cd packages/shared-utils
pnpm init
{
"name": "@myorg/shared-utils",
"version": "1.0.0",
"type": "module",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"import": "./lib/index.js",
"types": "./lib/index.d.ts"
}
},
"files": ["lib/"],
"scripts": {
"build": "run build",
"clean": "run clean",
"test": "run test",
"typecheck": "run typecheck"
},
"devDependencies": {
"presetter": "workspace:*",
"@presetter/preset-esm": "workspace:*"
}
}
// Simple preset usage for most packages
export { default } from '@presetter/preset-esm';
export interface Logger {
info(message: string): void;
error(message: string): void;
warn(message: string): void;
}
export function createLogger(prefix: string): Logger {
return {
info: (message) => console.log(`[${prefix}] INFO: ${message}`),
error: (message) => console.error(`[${prefix}] ERROR: ${message}`),
warn: (message) => console.warn(`[${prefix}] WARN: ${message}`)
};
}
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
React App Package
Create a React application:
mkdir apps/web-app
cd apps/web-app
pnpm init
{
"name": "@myorg/web-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "run dev",
"build": "run build",
"preview": "run preview",
"test": "run test",
"lint": "run lint"
},
"dependencies": {
"@myorg/shared-utils": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"presetter": "workspace:*",
"@presetter/preset-react": "workspace:*",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}
import react from '@presetter/preset-react';
import { preset } from '@presetter/types';
export default preset('web-app', {
extends: [react],
variables: {
source: 'src',
output: 'dist'
},
override: {
assets: {
// Custom Vite configuration for the app
'vite.config.ts': (current) => ({
...current,
server: {
port: 3000,
open: true
},
build: {
sourcemap: true
}
})
}
}
});
import React from 'react';
import { createLogger, formatDate } from '@myorg/shared-utils';
const logger = createLogger('WebApp');
function App() {
React.useEffect(() => {
logger.info('App mounted');
}, []);
return (
<div style={{ padding: '2rem' }}>
<h1>My Awesome Web App</h1>
<p>Today is: {formatDate(new Date())}</p>
</div>
);
}
export default App;
Step 4: Configure Cross-Package Dependencies
Update Root Package.json
{
"name": "@myorg/monorepo",
"private": true,
"scripts": {
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"lint": "pnpm -r run lint",
"clean": "pnpm -r run clean",
"dev": "pnpm --parallel -r run dev",
"typecheck": "pnpm -r run typecheck"
},
"devDependencies": {
"presetter": "^9.0.0",
"@presetter/preset-monorepo": "^9.0.0"
}
}
Bootstrap All Packages
From the root directory:
# Install dependencies for all packages
pnpm install
# Bootstrap Presetter in all packages
pnpm -r exec presetter bootstrap
Step 5: Build Orchestration
Sequential Build (Dependencies First)
Create a build script that respects dependency order:
#!/bin/bash
set -e
echo "🏗️ Building monorepo packages..."
# Build shared utilities first
echo "📦 Building shared-utils..."
cd packages/shared-utils
pnpm run build
cd ../..
# Build applications that depend on utilities
echo "🌐 Building web-app..."
cd apps/web-app
pnpm run build
cd ../..
echo "✅ All packages built successfully!"
Parallel Development
For development, run multiple packages in parallel:
{
"scripts": {
"dev": "concurrently \"pnpm --filter @myorg/shared-utils run watch\" \"pnpm --filter @myorg/web-app run dev\"",
"dev:utils": "pnpm --filter @myorg/shared-utils run watch",
"dev:web": "pnpm --filter @myorg/web-app run dev"
},
"devDependencies": {
"concurrently": "^8.0.0"
}
}
Step 6: Shared Configuration Management
ESLint Configuration Sharing
Create a shared ESLint config package:
mkdir packages/eslint-config
cd packages/eslint-config
pnpm init
{
"name": "@myorg/eslint-config",
"version": "1.0.0",
"main": "index.js",
"files": ["index.js"],
"peerDependencies": {
"eslint": "^8.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0"
}
}
module.exports = {
extends: [
'@typescript-eslint/recommended',
'prettier'
],
rules: {
// Shared organization rules
'no-console': 'warn',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn'
}
};
Use Shared Config in Packages
import presetEsm from '@presetter/preset-esm';
export default presetEsm({
assets: {
'.eslintrc.json': {
extends: ['@myorg/eslint-config']
}
}
});
Step 7: Testing Strategy
Jest Configuration for Monorepo
import type { Config } from 'jest';
const config: Config = {
projects: [
'<rootDir>/packages/*/jest.config.ts',
'<rootDir>/apps/*/jest.config.ts'
],
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'packages/*/src/**/*.{ts,tsx}',
'apps/*/src/**/*.{ts,tsx}',
'!**/*.d.ts',
'!**/*.test.{ts,tsx}'
]
};
export default config;
Package-Specific Tests
import type { Config } from 'jest';
const config: Config = {
displayName: 'shared-utils',
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts']
};
export default config;
Step 8: Development Workflow
Quick Commands
Add convenience scripts to your shell:
# Monorepo helpers
alias repo="cd /path/to/my-awesome-monorepo"
alias build-all="pnpm run build"
alias test-all="pnpm run test"
alias dev-web="pnpm --filter @myorg/web-app run dev"
VS Code Workspace
{
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.suggest.includeCompletionsForModuleExports": true,
"eslint.workingDirectories": [
"packages/shared-utils",
"apps/web-app"
]
}
Step 9: CI/CD Integration
GitHub Actions Example
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm run lint
- run: pnpm run test
- run: pnpm run build
Best Practices
1. Dependency Management
- Use
workspace:*for internal dependencies - Pin external dependencies to specific versions
- Regular dependency updates across all packages
2. Build Strategy
- Build shared libraries first
- Use incremental builds when possible
- Cache build artifacts in CI
3. Code Sharing
- Extract common utilities to shared packages
- Use TypeScript path mapping for cleaner imports
- Share ESLint, Prettier configs
4. Testing
- Test packages in isolation
- Integration tests for cross-package functionality
- Shared test utilities
Troubleshooting
Issue: Import Resolution Fails
Problem: Can't import from workspace packages Solution: Ensure proper TypeScript configuration:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@myorg/*": ["packages/*/src"]
}
}
}
Issue: Build Order Dependencies
Problem: Builds fail due to dependency order Solution: Use pnpm's filtering and topological sorting:
# Build in dependency order
pnpm -r --filter="...@myorg/web-app" run build
Issue: Hot Reload Not Working
Problem: Changes in shared packages don't trigger reload Solution: Set up proper watch mode with linking:
{
"scripts": {
"watch": "tsc --watch --preserveWatchOutput"
}
}
Next Steps
- Custom Preset Creation - Build organization-specific presets
- API Documentation - TypeScript API reference
- Advanced Topics - Performance and optimization
Setup Time: 30-45 minutes
Difficulty: Advanced
Prerequisites: TypeScript, package workspaces, build tools