Monorepo Setup Guide
This guide explains how to set up and manage a monorepo using pnpm workspaces, Turborepo, and Changesets.
Table of Contents
- Prerequisites
- Project Structure
- Package Management with pnpm
- Build System with Turborepo
- Version Management with Changesets
- Continuous Integration
Prerequisites
Before you begin, ensure you have the following installed:
# Install Node.js (v12 or higher)
# Install pnpm globally
npm install -g pnpm
# Required package dependencies
pnpm add -D @changesets/changelog-github @changesets/cli @commitlint/cli @commitlint/config-conventional turbo
These packages are essential for the monorepo setup:
@changesets/changelog-github
and@changesets/cli
manage version control and changelog generation@commitlint/cli
and@commitlint/config-conventional
enforce commit message conventionsturbo
provides the build system for managing dependencies between packages
Project Structure
Our monorepo follows this structure:
root/
├── .changeset/
├── .github/
├── packages/
│ ├── core/
│ ├── react/
│ ├── angular/
│ ├── vue/
│ └── web-component/
├── examples/
├── documentation/
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
Package Management with pnpm
Setting Up pnpm Workspaces
- Create a
pnpm-workspace.yaml
file in your root directory:
packages:
- 'packages/*'
- 'examples/*'
- Configure your root
package.json
:
{
"name": "@mindfiredigital/pivothead-table",
"private": true,
"workspaces": ["packages/*", "examples/*"],
"scripts": {
"build": "turbo run lint && turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules",
"preinstall": "npx only-allow pnpm"
}
}
Installing Dependencies
# Install dependencies across all packages
pnpm install
# Add a dependency to a specific package
pnpm add <package> --filter @mindfiredigital/package-core
Build System with Turborepo
Configuring Turborepo
Create a turbo.json
in your root directory:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false
}
}
}
Running Tasks
# Build all packages
pnpm turbo run build
# Build specific packages
pnpm turbo run build --filter=@mindfiredigital/package-core...
Version Management with Changesets
Setup Changesets
- Install changesets:
pnpm add -D @changesets/cli @changesets/changelog-github
- Configure changesets in
.changeset/config.json
:
{
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "mindfiredigital/Package" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
Managing Versions
- Create a changeset:
pnpm changeset
- Commit format for automatic changeset generation:
# For new features
git commit -m "feat(core): add new table feature"
# For bug fixes
git commit -m "fix(react): resolve rendering issue"
# For breaking changes
git commit -m "BREAKING CHANGE: complete API redesign"
Commit Lint Configuration
We use commitlint to enforce commit message conventions. Here's our commitlint.config.cjs
:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': [
2,
'always',
['core', 'react', 'web-component', 'angular', 'docs', 'release'],
],
'scope-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
},
ignores: [message => message.includes('[skip-commitlint]')],
};
Continuous Integration
GitHub Actions Workflow
Our release workflow (.github/workflows/release.yml
) handles automatic versioning and publishing:
name: Deployment Workflow PivotHead
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
name: '@mindfiredigital/pivothead'
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
pull-requests: write
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Enable Corepack
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build application
run: pnpm turbo run build
- name: Create Release Pull Request or Publish
uses: changesets/action@v1.4.1
with:
commit: 'chore(release): version packages'
title: 'chore(release): version packages'
version: node .github/changeset-version.js
publish: npx changeset publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Automatic Changeset Generation
We use a custom script (.github/changeset-autogenerate.mjs
) to automatically generate changesets from commit messages:
// This script automatically creates changesets based on commit messages
import { execSync } from 'child_process';
import fs from 'fs';
// Get the most recent commit message
const commitMessage = execSync('git log -1 --format=%s').toString().trim();
// Define valid scopes
const validScopes = [
'core',
'react',
'angular',
'vue',
'svelte',
'web-component',
];
// Define regex patterns
const commitPatterns = {
major: /^BREAKING CHANGE: (.+)/,
minor: /^feat\(([^)]+)\): (.+)/,
patch: /^fix\(([^)]+)\): (.+)/,
};
// Identify type, package, and description
let packageScope = null;
let changeType = null;
let description = null;
if (commitPatterns.major.test(commitMessage)) {
changeType = 'major';
description = commitMessage.match(commitPatterns.major)?.[1];
} else if (commitPatterns.minor.test(commitMessage)) {
const scope = commitMessage.match(commitPatterns.minor)?.[1];
if (validScopes.includes(scope)) {
changeType = 'minor';
packageScope = scope;
description = commitMessage.match(commitPatterns.minor)?.[2];
}
} else if (commitPatterns.patch.test(commitMessage)) {
const scope = commitMessage.match(commitPatterns.patch)?.[1];
if (validScopes.includes(scope)) {
changeType = 'patch';
packageScope = scope;
description = commitMessage.match(commitPatterns.patch)?.[2];
}
}
// Generate and write changeset if valid package found
if (packageScope) {
packageScope = packageScope.trim();
description = description?.trim() || 'No description provided.';
// Determine the full package name based on scope
const packageName =
packageScope === 'core'
? '@mindfiredigital/pivothead'
: `@mindfiredigital/pivothead-${packageScope}`;
// Generate changeset content
const changesetContent = `---
'${packageName}': ${changeType}
---
${description}
`;
// Write to a changeset file
fs.writeFileSync(`.changeset/auto-${Date.now()}.md`, changesetContent);
console.log(`✅ Changeset file created for package: ${packageName}`);
} else {
console.log(
'⚠️ No valid package scope found in commit message. Valid scopes are: core, react, angular, vue, svelte, web-component'
);
}
We use a script (.github/changeset-version.js
) to automatically updtate the versions:
// This script automatically updates the versions of package
const { exec } = require('child_process');
exec('npx changeset version');
exec('npm install');
Common Commands
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run development mode
pnpm dev
# Create a changeset
pnpm changeset
# Publish packages
pnpm release
# Clean build artifacts
pnpm clean
Best Practices
Commit Messages: Always follow the conventional commit format:
feat(scope): message
for featuresfix(scope): message
for bug fixesBREAKING CHANGE: message
for breaking changes
Package Versioning: Let changesets handle version bumps automatically.
Dependencies:
- Use
pnpm add
with the--filter
flag to add package-specific dependencies - Use workspace protocols for internal dependencies
- Use
Build Pipeline:
- Ensure all packages define their build dependencies correctly in
turbo.json
- Use the
--filter
flag with Turbo for targeted builds
- Ensure all packages define their build dependencies correctly in
Troubleshooting
Common issues and solutions:
pnpm install fails:
- Clear pnpm store:
pnpm store prune
- Delete node_modules:
pnpm clean
- Clear pnpm store:
Turbo cache issues:
- Clear Turbo's cache:
pnpm turbo clean
- Clear Turbo's cache:
Changeset conflicts:
- Remove .changeset directory
- Regenerate changesets:
pnpm changeset:autogenerate