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, 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ü
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
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
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
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
- pnpm Workspaces — pnpm’in workspace özelliğinin resmi dokümantasyonu
- Using Changesets with pnpm — pnpm ile Changesets entegrasyonu rehberi
- Turborepo — Structuring a Repository — Turborepo’nun monorepo yapılandırma dokümantasyonu
- Changesets GitHub Action — Changesets’in resmi GitHub Action repo’su ve kullanım kılavuzu
- Tuvix.js GitHub Repository — Makalede referans verdiğim kendi Micro Frontend Framework’üm