Yasin Ateş

pnpm workspace ile Monorepo Mimarisi

pnpm workspace ile Monorepo Mimarisi

Birden fazla paketi olan bir proje geliştirdiğimizde, her paketi ayrı bir repo’da yönetmek zamanla kabusa dönüşebiliyor. Bu yazıda pnpm workspace ile monorepo mimarisini sıfırdan kuracak, Turborepo ile build süreçlerini hızlandıracak ve Changesets ile otomatik NPM publish pipeline’ını ayağa kaldıracağız.

🏗️ Monorepo Nedir?

Diyelim ki bir design system geliştiriyoruz. Bir core paketi var, bir theme paketi var, bir de utils paketi var. Bunların hepsini ayrı ayrı repo’larda tuttuğumuzu düşünelim.

Core paketinde bir bug fix yaptığımızda ne olacak? Theme repo’suna geçip dependency’yi güncelleyeceğiz. Sonra utils repo’suna geçeceğiz. Her birinde ayrı PR açacak, ayrı CI/CD bekleyecek, ayrı publish yapacağız. Üç paket için bile bu süreç yorucu, on pakette ise tam bir kabus.

İşte tam bu noktada monorepo devreye giriyor.

Monorepo, birden fazla projeyi tek bir repository altında toplayan mimari yaklaşımdır. Tüm paketlerimiz aynı repo’da yaşar, aynı commit history’yi paylaşır ve birbirine kolayca referans verebilir.

Peki ama bu neden önemli? Birkaç temel avantajına bakalım:

Tek bir yerde kod paylaşımı: Paylaşılan utility’ler, tipler ve konfigürasyonlar tüm paketler tarafından anında kullanılabilir. NPM’e publish etmeyi beklememize gerek yok.

Atomik değişiklikler: Birden fazla paketi etkileyen bir değişikliği tek bir commit’te yapabiliriz. Cross-repo PR senkronizasyonu derdi ortadan kalkar.

Tutarlı tooling: ESLint, Prettier, TypeScript konfigürasyonları tek bir noktadan yönetilir. “Bu repo’da neden farklı kurallar var?” sorusu tarih olur.

Kolay onboarding: Yeni bir geliştirici projeye dahil olduğunda, tek bir git clone ve pnpm install ile tüm ekosistemi ayağa kaldırabilir.

🔧 pnpm workspace Kurulumu

pnpm workspace kurulumu

pnpm, Node.js ekosistemindeki paket yöneticileri arasında monorepo desteği konusunda öne çıkan bir araçtır. npm ve yarn’dan farklı olarak, content-addressable store yapısı sayesinde tüm bağımlılıkları global bir hafızada tutar ve projelerimize hard link ile bağlar. Bu da disk alanından devasa tasarruf sağlar.

Örnek bir proje üzerinden pnpm workspace ile monorepo mimarisini inceleyelim;

Adım 1: pnpm’i Kuralım ve Projeyi Başlatalım

pnpm’i global kuralım:

npm install -g pnpm

ve yeni bir monorepo projesi başlatalım:

mkdir my-design-system
cd my-design-system
pnpm init
git init

★ pnpm init komutu root seviyede bir package.json dosyası oluşturur.

★ Bu dosya monorepo’muzun ana uygulaması olacak. Tüm workspace scriptleri buradan yönetilecek.

Adım 2: pnpm-workspace.yaml Oluşturalım

Proje root dizininde bir pnpm-workspace.yaml dosyası oluşturuyoruz:

packages:
 - "packages/*"
 - "apps/*"
 - "website"

★ packages/* ifadesi, packages dizini altındaki her klasörü ayrı bir workspace paketi olarak tanımlar.

★ apps/* ile uygulamaları, website ile de dokümantasyon sitesini workspace'e dahil ediyoruz.

★ Bu dosya olmadan pnpm, projemizi bir monorepo olarak tanımaz.

Adım 3: Root package.json’ı Yapılandıralım

Root package.json dosyasını monorepo için yapılandıralım:

{
 "name": "my-design-system",
 "private": true,
 "scripts": {
 "dev": "turbo run dev",
 "build": "turbo run build",
 "test": "turbo run test",
 "lint": "turbo run lint",
 "clean": "turbo run clean"
 },
 "devDependencies": {
 "turbo": "^2.3.1",
 "typescript": "^5.7.2",
 "tsup": "^8.3.5",
 "vitest": "^2.1.6",
 "prettier": "^3.4.1"
 },
 "packageManager": "pnpm@9.14.2",
 "engines": {
 "node": ">=18.0.0"
 }
}

★ "private": true kritik bir ayar. Root paketinin yanlışlıkla NPM'e publish edilmesini engeller.

★ "packageManager" alanı, projeyi klonlayan herkesin aynı pnpm sürümünü kullanmasını sağlar. Corepack bu alanı okuyarak doğru paket yöneticisini otomatik aktifleştirir.

★ Ortak devDependency’ler (TypeScript, Prettier gibi) root’a kurulur. Böylece her paketin ayrı ayrı kurmasına gerek kalmaz.

Adım 4: Proje Yapısını Oluşturalım

Şimdi klasör yapımızı oluşturalım:

mkdir -p packages/core
mkdir -p packages/utils
mkdir -p packages/theme
mkdir -p apps/docs

Örnek bir monorepo yapısı şöyle görünür:

my-design-system/
├── packages/
│ ├── core/ # Ana kütüphane
│ ├── utils/ # Paylaşılan yardımcı fonksiyonlar
│ └── theme/ # Tema ve stil tanımları
├── apps/
│ └── docs/ # Dokümantasyon uygulaması
├── .changeset/ # Changesets konfigürasyonu
├── .github/ # CI/CD workflow'ları
├── package.json # Root workspace config
├── pnpm-workspace.yaml # Workspace tanımı
├── turbo.json # Turborepo pipeline
├── tsconfig.base.json # Paylaşılan TypeScript config
└── pnpm-lock.yaml # Lock dosyası

★ packages/ altında kütüphaneler ve paylaşılan modüller yaşar. Her biri bağımsız bir npm paketi olarak publish edilebilir.

★ apps/ altında bu paketleri kullanan uygulamalar yer alır.

★ Root’taki konfigürasyon dosyaları tüm monorepo genelinde geçerlidir.

Her alt paketi de initialize etmemiz gerekiyor. Örneğin packages/core için:

cd packages/core
pnpm init

Bu komut her paketin kendi package.json'ını oluşturur. Paket adlarında namespace kullanmak iyi bir pratik:

{
 "name": "@myds/core",
 "version": "0.1.0",
 "main": "dist/index.js",
 "types": "dist/index.d.ts",
 "scripts": {
 "build": "tsup src/index.ts --format cjs,esm --dts",
 "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
 }
}

★ @myds/core gibi scoped isimler, NPM'de namespace çakışmasını önler.

★ main ve types alanları, diğer paketlerin bu paketi import ettiğinde doğru dosyaları bulmasını sağlar.

tsup, TypeScript paketlerini hızlıca bundle’lamak için harika bir araç. CJS ve ESM formatlarında çıktı üretir.

🔗 workspace ifadesi ile Paketler Arası Bağımlılık Kontrolü

pnpm workspace* ifadesi

Monorepo’nun en güçlü özelliklerinden biri, paketlerin birbirini lokal olarak referans edebilmesi. pnpm bunu workspace: protokolü ile sağlar.

Gelin bunu bir örnekle somutlaştıralım. Diyelim ki @myds/theme paketi, @myds/core paketine bağımlı. Bu bağımlılığı şöyle tanımlarız:

{
 "name": "@myds/theme",
 "version": "0.4.0",
 "dependencies": {
 "@myds/core": "workspace:*"
 }
}

★ workspace:* ifadesi, pnpm'e "bu paketi npm registry'den indirme, workspace'deki lokal versiyonu kullan" der. ★ Geliştirme sırasında paketler symlink ile birbirine bağlanır. Core'da yaptığımız değişiklik anında theme'de görünür.

Peki ama publish sırasında ne oluyor? İşte pnpm’in güzel tarafı: pnpm publish çalıştığında, workspace:* otomatik olarak gerçek versiyon numarasına dönüştürülür.

Yani geliştirme sırasında package.json şöyle görünürken:

{
 "dependencies": {
 "@myds/core": "workspace:*"
 }
}

NPM’e publish edildiğinde şuna dönüşür:

{
 "dependencies": {
 "@myds/core": "0.4.0"
 }
}

workspace ifadesinin Farklı Varyantları

pnpm birkaç farklı workspace protokolü sunar:

workspace:* en yaygın kullanılan varyant. "Workspace'deki mevcut versiyon her neyse onu kullan" anlamına gelir. Publish sırasında exact version'a dönüşür (ör. "0.4.0").

workspace:^ publish sırasında caret range'e dönüşür (ör. "^0.4.0"). Minor ve patch güncellemelerine izin verir.

workspace:~ publish sırasında tilde range'e dönüşür (ör. "~0.4.0"). Sadece patch güncellemelerine izin verir.

Bunu neden tercih ettiğimi açıklayayım: Eğer tüm paketlerimizi her zaman birlikte publish ediyorsak workspace:* yeterli. Ama kullanıcıların farklı versiyonları mix edebilmesini istiyorsak workspace:^ daha esnek bir seçenek olur.

Paketler Arası Dependency Ekleme

Bir workspace paketine diğerini eklemek için --filter flag'ini kullanırız:

pnpm add @myds/core --filter @myds/theme

★ --filter flag'i, komutu sadece belirtilen pakette çalıştırır.

★ pnpm otomatik olarak bağımlılığı workspace:^ olarak ekler (eğer workspace'de mevcutsa).

Belirli bir paketin tüm bağımlılarında script çalıştırmak da mümkün:

# @myds/core ve ona bağımlı tüm paketleri build edelim
pnpm --filter "@myds/core..." build

Root workspace’e bir bağımlılık eklemek için ise --workspace-root (veya kısaca -w) flag'ini kullanırız:

pnpm add -D typescript --workspace-root

⚡ Turborepo ile Build Orchestration

Turborepo

pnpm workspace tek başına paket yönetimi ve linking için yeterli olsa da, paket sayısı arttıkça build süreçleri karmaşıklaşır. “Hangi paket önce build edilmeli?”, “Değişmeyen paketleri tekrar build etmeye gerek var mı?” gibi sorunlar ortaya çıkar.

İşte tam bu noktada Turborepo devreye giriyor. Turborepo, Vercel tarafından geliştirilen bir build system’dir ve doğrudan pnpm workspace ile birlikte kullanılabilir. Yani pnpm’in workspace yapısını okur, paketler arasındaki bağımlılık grafiğini analiz eder ve task’ları buna göre optimize eder. İki temel özelliği var:

Smart task scheduling: Paketler arasındaki bağımlılık grafiğini analiz eder ve build sırasını otomatik belirler. Birbirine bağımlı olmayan paketleri paralel çalıştırır.

Hard caching: Bir paket değişmediyse, önceki build çıktısını cache’den getirir. İlk build 30 saniye sürerken, cache’li build 0.2 saniyede tamamlanabilir.

Şimdi Turborepo’yu Kuralım

Root package.json'a turbo'yu ekleyelim:

pnpm add -D turbo --workspace-root

★ --workspace-root flag'i (veya kısaca -w), paketi root workspace'e kurar. Böylece turbo tüm monorepo genelinde kullanılabilir.

turbo.json Konfigürasyonu

Proje kökünde bir turbo.json dosyası oluşturalım:

{
 "$schema": "https://turbo.build/schema.json",
 "tasks": {
 "build": {
 "dependsOn": ["^build"],
 "outputs": ["dist/**"]
 },
 "dev": {
 "cache": false,
 "persistent": true
 },
 "test": {
 "dependsOn": ["build"],
 "outputs": []
 },
 "test:watch": {
 "cache": false,
 "persistent": true
 },
 "lint": {
 "outputs": []
 },
 "clean": {
 "cache": false
 },
 "check-types": {
 "dependsOn": ["^build"],
 "outputs": []
 }
 }
}

Bir dakika, burada duralım ve şunu anlayalım: "dependsOn": ["^build"] ifadesindeki ^ işareti çok önemli. Bu, "önce benim bağımlılıklarımın build task'ını çalıştır, sonra beni build et" demek. Yani @myds/theme build edilmeden önce, onun bağımlılığı olan @myds/core build edilir.

★ "outputs": ["dist/**"] Turborepo'ya hangi dosyaların cache'leneceğini söyler. Bir sonraki build'de kaynak kodlar değişmediyse, dist/ klasörü cache'den restore edilir.

★ "cache": false bazı task'lar için cache'i devre dışı bırakır. Dev server gibi sürekli çalışan (persistent) task'larda cache mantıksız olur.

★ "persistent": true task'ın arka planda çalışmaya devam etmesini sağlar. Dev server ve watch mode gibi senaryolar için gerekli.

Artık tüm paketleri tek bir komutla build edebiliriz:

pnpm build
# Bu, turbo run build komutunu çalıştırır
# Turborepo dependency graph'ı analiz eder
# Önce core, sonra theme, sonra utils... şeklinde sıralı build yapar
# Değişmeyen paketleri cache'den getirir

🦋 Changesets ile Versiyon Yönetimi

Changesets

Monorepo’da en zorlu konulardan biri versiyon yönetimi. Birden fazla paketimiz var ve her birinin bağımsız versiyon numarası olabilir. Hangisini ne zaman bump edeceğimize, changelog’u nasıl oluşturacağımıza nasıl karar vereceğiz?

İşte tam bu noktada Changesets devreye giriyor. Changesets, her değişikliği küçük bir markdown dosyası olarak kaydetmemizi sağlayan bir versiyon yönetim aracı.

Adım 1: Changesets’i Kuralım

pnpm add -D @changesets/cli --workspace-root
pnpm changeset init

★ changeset init komutu, projemizde .changeset/ klasörünü oluşturur. ★ Bu klasör içinde bir config.json ve README.md dosyası bulunur.

Adım 2: Changeset Konfigürasyonu

.changeset/config.json dosyasını projemize göre yapılandıralım:

{
 "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
 "changelog": [
 "@changesets/changelog-github",
 { "repo": "your-org/your-monorepo" }
 ],
 "commit": false,
 "fixed": [],
 "linked": [["@myds/*"]],
 "access": "public",
 "baseBranch": "master",
 "updateInternalDependencies": "patch",
 "ignore": []
}

Gelin bu ayarları tek tek anlayalım:

changelog kısmında @changesets/changelog-github kullanıyoruz. Bu, changelog'a otomatik olarak PR linkleri ve contributor bilgisi ekler. İsterseniz bu paketi de kuralım:

pnpm add -D @changesets/changelog-github --workspace-root

linked alanı çok önemli. [["@myds/*"]] ifadesi, tüm @myds scope'undaki paketlerin versiyonlarının birbirine bağlı olduğunu söyler. Birinde major bump yaparsak, hepsinde yapılır.

access": "public" scoped paketlerin (@myds/core gibi) NPM'de public olarak publish edilmesini sağlar. Bu ayar olmadan scoped paketler private olarak publish edilmeye çalışır ve hata verir.

updateInternalDependencies": "patch" bir paket güncellendiğinde, ona bağımlı paketlerin internal dependency versiyonunu otomatik olarak patch bump eder.

baseBranch": "master" changesets'in hangi branch'i baz alacağını belirler. Projemize göre main veya master olabilir.

Adım 3: Changeset Oluşturalım

Bir değişiklik yaptığımızda, PR açmadan önce bir changeset dosyası oluşturalım:

pnpm changeset

Bu komut interaktif bir wizard başlatır:

🦋 Which packages would you like to include?
 ◯ @myds/core
 ◯ @myds/theme
 ◯ @myds/utils
🦋 Which packages should have a major bump?
🦋 Which packages should have a minor bump?
🦋 Summary: Core paketine dark mode desteği eklendi

Bu wizard sonucunda .changeset/ klasöründe rastgele isimli bir markdown dosyası oluşturulur:

---
"@myds/core": minor
"@myds/theme": patch
---

Core paketine dark mode desteği eklendi. Theme paketinde ilgili renk değişkenleri güncellendi.

★ Dosyanın frontmatter kısmında hangi paketin hangi seviyede bump edileceği belirtilir.

★ Altındaki metin, changelog’a eklenecek açıklama.

★ Bu dosya commit’e dahil edilir ve PR ile birlikte review’a gönderilir.

Adım 4: Versiyonları Uygulayalım

Tüm changeset’ler toplandıktan sonra versiyonları uygulayalım:

pnpm changeset version

★ Bu komut tüm changeset dosyalarını okur. ★ İlgili paketlerin package.json'larındaki versiyonları günceller. ★ Her paketin CHANGELOG.md dosyasını otomatik oluşturur/günceller. ★ Kullanılmış changeset dosyalarını siler.

Adım 5: Publish Edelim

Versiyonlar uygulandıktan sonra publish:

pnpm changeset publish

Bu komut değişen tüm paketleri NPM registry’ye publish eder. Peki, bunu her seferinde elle mi yapacağız? Tabii ki hayır 🤦‍♀️ Bir sonraki bölümde bunu tamamen otomatize edeceğiz

🚀 GitHub Actions ile Otomatik NPM Publish

Github action ile otomatik npm publish

Changesets’in asıl gücü, GitHub Actions ile birleştiğinde ortaya çıkar. Şimdi adım adım bir CI/CD pipeline kuralım.

Adım 1: NPM Token Oluşturalım

Öncelikle NPM’de bir Automation Token oluşturmamız gerekiyor:

npmjs.com’da profil ayarlarına gidelim.

Access Tokens bölümünden Generate New Token seçelim.

★ Token tipini Automation olarak seçelim. Bu tip, 2FA gerektirmeden publish yapmamıza izin verir.

★ Oluşan token’ı kopyalayalım (sadece bir kez gösterilir!).

Adım 2: GitHub Secrets’a Token’ları Ekleyelim

GitHub repo’muzda Settings > Secrets and variables > Actions bölümüne gidip şu secret’ı ekleyelim:

NPM_TOKEN: Az önce oluşturduğumuz NPM automation token'ı.

GITHUB_TOKEN zaten GitHub tarafından her workflow çalıştırmasında otomatik olarak sağlanır, ekstra bir şey yapmamıza gerek yok.

Adım 3: Repository İzinlerini Ayarlayalım

GitHub repo’muzda Settings > Actions > General bölümüne gidelim:

Workflow permissions altında “Read and write permissions” seçeneğini aktifleştirelim.

“Allow GitHub Actions to create and approve pull requests” kutucuğunu işaretleyelim.

Bu ayarlar olmadan Changesets bot’u PR oluşturamaz.

Adım 4: Release Workflow Dosyasını Oluşturalım

Şimdi .github/workflows/release.yml dosyasını oluşturalım:

name: Release

on:
 push:
 branches:
 - master

concurrency:
 group: ${{ github.workflow }}-${{ github.ref }}
 cancel-in-progress: false

permissions:
 contents: write
 pull-requests: write
 packages: write
 id-token: write

jobs:
 release:
 name: Release
 runs-on: ubuntu-latest
 steps:
 - name: Checkout Repo
 uses: actions/checkout@v4
 with:
 fetch-depth: 0

 - name: Setup pnpm
 uses: pnpm/action-setup@v4

 - name: Setup Node.js
 uses: actions/setup-node@v4
 with:
 node-version: 20
 cache: "pnpm"
 registry-url: "https://registry.npmjs.org"

 - name: Install Dependencies
 run: pnpm install --frozen-lockfile

 - name: Build All Packages
 run: pnpm build

 - name: Create Release Pull Request or Publish
 id: changesets
 uses: changesets/action@v1
 with:
 commit: "chore: update versions"
 title: "chore: update versions"
 publish: pnpm run ci:publish
 env:
 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
 NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Bu workflow oldukça fazla şey yapıyor. Hadi parça parça inceleyelim:

concurrency ayarı, aynı anda birden fazla release workflow'unun çalışmasını engeller. Bir publish devam ederken yeni bir push gelirse, eski workflow'u iptal etmez ama yeni workflow sırada bekler.

permissions bloğu, workflow'un repo'ya yazma, PR oluşturma ve paket publish etme yetkilerine sahip olmasını sağlar.

fetch-depth: 0 tam git history'yi çeker. Changesets, versiyonları hesaplamak için commit history'ye ihtiyaç duyar.

pnpm/action-setup@v4 pnpm'i CI ortamına kurar. package.json'daki packageManager alanını okuyarak doğru sürümü kurar.

--frozen-lockfile CI'da kritik bir flag. Lock dosyasına göre birebir aynı bağımlılıkları kurar. Eğer pnpm-lock.yaml güncel değilse hata verir.

changesets/action@v1 işin büyüsünü yapan action. İki farklı senaryo yönetir:

Senaryo A: .changeset/ klasöründe bekleyen changeset dosyaları varsa, bir "Version Packages" PR'ı oluşturur veya günceller. Bu PR, versiyon bump'larını ve changelog değişikliklerini içerir.

Senaryo B: Bekleyen changeset yoksa (yani Version Packages PR’ı merge edildikten sonra), publish komutunu çalıştırarak paketleri NPM'e publish eder.

Adım 5: ci:publish Script’ini Tanımlayalım

Root package.json'a publish script'ini ekleyelim:

{
 "scripts": {
 "ci:publish": "pnpm publish -r --access public"
 }
}

★ -r flag'i (recursive) tüm workspace paketlerini publish eder. ★ --access public scoped paketlerin public olarak publish edilmesini sağlar.

Changeset Bot

İsterseniz github.com/apps/changeset-bot adresinden Changeset Bot’u repo’nuza kurabilirsiniz. Bu bot, açılan PR’larda changeset dosyası olup olmadığını kontrol eder ve yoksa bir uyarı mesajı bırakır

🎯 Demo: Tuvix.js Monorepo Projesi

Bu makalede anlattığım tüm mimariyi gerçek hayatta görmek isterseniz, geliştirdiğim Tuvix.js projesine göz atabilirsiniz. Tuvix.js, lightweight bir micro frontend framework’ü ve tam olarak bu makalede anlattığım stack’i kullanıyor:

pnpm workspace ile 14+ paket tek bir repo’da yönetiliyor

Turborepo ile build orchestration ve caching yapılıyor

Changesets ile versiyon yönetimi ve otomatik NPM publish sağlanıyor

GitHub Actions ile CI/CD pipeline çalışıyor

pnpm-workspace.yaml, turbo.json, .changeset/config.json ve GitHub Actions workflow dosyalarının hepsini repo üzerinden inceleyebilirsiniz.

Repo’ya buradan ulaşabilirsiniz 👇

github.com/yasinatesim/tuvix.js

📬 Geri Bildirim

Makaleyi yazarken, kaynakları belirleme ve araştırma, yazım denetimi ve araştırma için Claude Opus 4.6 modelini kullandım. Resimleri üretmek için ise Gemini 3 Pro Preview 2k (Nano Banana Pro) modelini kullandım.

Yazı ile ilgili tavsiye, öneri, eleştirileri dikkate alıyorum. İletişime geçmek isterseniz bana websitemdeki sosyal medya adreslerimden veya Linkedin üzerinden ulaşabilirsiniz.

Sevgiyle kalın, Yasin 🤗

📚 Makaleyi Yazarken Kullandığım Kaynaklar

  1. pnpm Workspaces — pnpm’in workspace özelliğinin resmi dokümantasyonu
  2. Using Changesets with pnpm — pnpm ile Changesets entegrasyonu rehberi
  3. Turborepo — Structuring a Repository — Turborepo’nun monorepo yapılandırma dokümantasyonu
  4. Changesets GitHub Action — Changesets’in resmi GitHub Action repo’su ve kullanım kılavuzu
  5. Tuvix.js GitHub Repository — Makalede referans verdiğim kendi Micro Frontend Framework’üm