İçeriğe atla
Ayhan Sipahi Ayhan Sipahi

pnpm Catalogs: Monorepo'da Dependency Drift Çözümü

pnpm catalog'ları monorepo'larda dependency drift'i nasıl durduruyor: paylaşılan versiyonlar için tek dosya, legacy paketler için named catalog, CI'da enforcement.

JavaScript monorepo’larında dependency drift sessizce başlar. Bir service lodash’i ^4.17.20, diğeri ^4.17.21 olarak tanımlar; installer ikisini de kurar. Haftalar sonra ortak bir helper, hangi service’in import ettiğine göre farklı davranır ve hata beş makineden yalnızca birinde tekrarlanır.

pnpm catalog’ları bu açığı kaynağında kapatır. Paylaşılan her versiyon pnpm-workspace.yaml içinde bir kez tanımlanır, paketler ona catalog: ile referans verir ve bir paket versiyonu satır içinde yazdığında CI kontrolü build’i düşürür. Bedeli şu: tek bir dosya ortak bir darboğaza dönüşür. react için tam olarak istediğiniz şey budur; yalnızca tek bir service’in kullandığı bir parser için ise tam olarak istemediğiniz şeydir.

Dependency Drift Sorunu

Drift tek bir hatalı versiyon değildir. Versiyonun nerede kararlaştırıldığına dair bir yerin olmamasıdır. Her package.json kendi otoritesidir; bu yüzden bir authentication service, type’larını import ettiği payment service’ten farklı bir @types/node üzerinde kalabilir. Toolchain’de hiçbir şey itiraz etmez, ta ki bir build iki paketin de adını anmayan bir type hatasıyla düşene kadar.

Workspace araçları bu açığı kapatmaz. Local paketleri linklemek ve ortak olanları hoist etmek yalnızca tanımlı bir aralığın nasıl çözüldüğünü değiştirir. Her paket yine kendi versiyon aralığını tanımlar.

Pratikte Drift

İki service, iki package.json ve ortak tek bir kütüphane:

// packages/service-a/package.json
{
  "dependencies": {
    "lodash": "^4.17.21",
    "axios": "^1.6.2"
  }
}

// packages/service-b/package.json  
{
  "dependencies": {
    "lodash": "^4.17.20",  // Farklı versiyon!
    "axios": "^1.6.5"  // Farklı versiyon!
  }
}

Burada geçersiz bir şey yok. İki aralık da çözülür, iki service de build alır ve store lodash’in iki kopyasını tutar. Bunu birkaç düzine pakete yayın; node_modules aynı kütüphaneyi aynı anda üç dört versiyonda taşır. Kimse yeni bir özellik eklemeden bundle’ın büyümesinin yolu budur.

Geleneksel Yaklaşımların Sınırları

npm workspaces, Yarn workspaces, Lerna ve Rush monorepo’yu yönetir. Hiçbiri versiyon kararının sahibi değildir:

Yeteneknpm WorkspacesYarn Workspacespnpm Catalogs
Paylaşılan versiyon için tek tanım yerioverrides zorlar, tanımlamazresolutions zorlar, tanımlamazVar
Phantom dependency’leri engellerHayırPnP ileVar
Güncelleme tek dosyaya dokunurHayırHayırVar
Paket grubuna göre ayrı versiyonHayırHayırNamed catalog

Build log’unda hiç görünmeyen maliyet dikkattir. Her versiyon uyumsuzluğu bir kesintidir: biri iki lockfile diff’ini okur, bir kazanan seçer ve CI’ı yeniden çalıştırır. İş zor değildir; bu yüzden hiç önceliklenmez ve repo büyüdükçe geri gelmeye devam eder.

Merkezi Versiyon Yönetiminden Beklenenler

Bir governance katmanı ancak şu beşini birden karşılıyorsa yerini hak eder:

  • Paylaşılan her versiyon için, her paketin okuyabildiği tek bir tanım
  • Tanım eksik olduğunda hoist edilmiş bir kopyaya sessizce düşmemek
  • Her package.json yerine tek dosyaya dokunan bir güncelleme
  • Henüz güncel versiyona geçemeyen paketler için bir kaçış yolu
  • İnsanların hatırlaması beklenen bir konvansiyon değil, CI’ı düşüren bir kontrol

pnpm Catalog’ların Uygulanması

Catalog’lar pnpm v9.5 ile geldi ve v10 hattı boyunca stabil. Konfigürasyon workspace dosyasında durur:

# pnpm-workspace.yaml
packages:
  - 'apps/**'
  - 'packages/**'
  - 'services/**'

catalog:
  # Core dependency'ler
  typescript: ^5.9.2
  lodash: ^4.17.21
  axios: ^1.6.5
  
  # React ekosistemi (stabil versiyonlar kullanarak)
  react: ^18.3.1
  react-dom: ^18.3.1
  "@types/react": ^18.3.3
  
  # Testing
  vitest: ^1.2.0
  "@testing-library/react": ^14.1.2

Artık individual paketleriniz sadece catalog’a referans veriyor:

{
  "name": "@mycompany/user-service",
  "dependencies": {
    "lodash": "catalog:",
    "axios": "catalog:",
    "react": "catalog:"
  }
}

Versiyon artık tek bir yerde durur ve onu uygulayan şey pnpm install’dır. "lodash": "^4.17.19" satırını elle yazan bir paket tartışmayı kazanamaz; 3. adımda eklenen kontrolde takılır.

pnpm Catalog Yaklaşımı

pnpm-workspace.yaml catalog

Service A

Service B

Service C

lodash v4.17.21

Geleneksel Yaklaşım

Service A package.json

lodash v4.17.21

Service B package.json

lodash v4.17.20

Service C package.json

lodash v4.17.19

Üç Adımda Migration

Aşağıdaki adımların her biri tek başına devreye alınabilir. Hiçbiri repo’yu dondurmayı gerektirmez.

Adım 1: Otomatik Catalog Oluşturma

Halihazırda tanımlı olandan başlayın. Bu script tüm monorepo’yu gezer ve catalog’un ilk taslağını üretir:

// scripts/migrate-to-catalogs.js
const fs = require('fs');
const yaml = require('js-yaml');
const glob = require('glob');

// Tüm unique dependency'leri topla
const dependencies = new Map();

glob.sync('**/package.json', { 
  ignore: ['**/node_modules/**', '**/dist/**'] 
}).forEach(file => {
  const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
  
  Object.entries(pkg.dependencies || {}).forEach(([name, version]) => {
    if (!dependencies.has(name) || dependencies.get(name) < version) {
      dependencies.set(name, version);
    }
  });
});

// Catalog konfigürasyonu oluştur
const catalog = Object.fromEntries(dependencies);

// pnpm-workspace.yaml'ı güncelle
const workspace = yaml.load(fs.readFileSync('pnpm-workspace.yaml', 'utf8'));
workspace.catalog = catalog;
fs.writeFileSync('pnpm-workspace.yaml', yaml.dump(workspace));

console.log(`${dependencies.size} dependency catalog'a migrate edildi`);

Yukarıdaki karşılaştırma versiyonları string olarak alfabetik sıralar; bu yüzden ^4.17.9, ^4.17.21’in önüne geçer. Çıktıyı taslak olarak görün ve commit etmeden önce üretilen dosyayı okuyun.

Adım 2: Legacy Service Yönetimi

Her şey hemen en son versiyonlara geçemez. Legacy service’ler React 17’de kalırken yenileri React 18 kullanabilir. Named catalog’lar bir çözüm sağlar:

catalogs:
  # React 17'deki legacy service'ler
  legacy:
    react: ^17.0.2
    react-dom: ^17.0.2
    "@types/react": ^17.0.39

  # React 18'deki modern service'ler
  modern:
    react: ^18.3.1
    react-dom: ^18.3.1
    "@types/react": ^18.3.3

Her paket ihtiyacı olan catalog’u seçer:

{
  "name": "@mycompany/legacy-dashboard",
  "dependencies": {
    "react": "catalog:legacy",
    "react-dom": "catalog:legacy"
  }
}

Böylece güncelleme, repo geneline yayılan bir olay olmaktan çıkar; hazır olan paketteki tek satırlık bir değişikliğe dönüşür.

Adım 3: Enforcement ve Validation

Enforcement’ı olmayan bir catalog yalnızca bir isimlendirme konvansiyonudur. Bu script CI’da çalışır ve satır içi yazılmış her versiyonda build’i düşürür:

// scripts/validate-catalogs.js
const validateCatalogs = () => {
  const violations = [];
  
  glob.sync('**/package.json', {
    ignore: ['**/node_modules/**']
  }).forEach(file => {
    const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
    
    Object.entries(pkg.dependencies || {}).forEach(([name, version]) => {
      // workspace protocol ve local paketleri atla
      if (version.startsWith('workspace:') || version.startsWith('file:')) {
        return;
      }
      
      // Catalog kullanmalı mı kontrol et
      if (!version.startsWith('catalog:')) {
        violations.push(`${file}: ${name}@${version} catalog kullanmalı`);
      }
    });
  });
  
  if (violations.length > 0) {
    console.error('Catalog ihlalleri bulundu:', violations);
    process.exit(1);
  }
  
  console.log('Tüm dependency'ler catalog protocol kullanıyor');
};

validateCatalogs();

Migration Sonrası Ne Değişir

Kazanım iki mekanizmadan gelir.

Birincisi pnpm’in content-addressable store’u. Bir paketin belirli bir versiyonu diske bir kez yazılır ve ihtiyacı olan her projeye hard link ile bağlanır. Catalog’lar bu store’un işini artırır: her workspace lodash’i aynı kayıt üzerinden çözdüğünde, neredeyse aynı üç kopya yerine linklenecek tek kopya kalır.

İkincisi diff’in biçimi. Bir dependency güncellemesi eskiden o paketi tanımlayan her package.json’a dokunuyordu. Artık pnpm-workspace.yaml’a ve lockfile’a dokunuyor. Aynı hafta farklı kütüphaneleri güncelleyen iki engineer aynı satırlarda çakışmayı bırakıyor.

Migration’dan önce kendi başlangıç ölçümünüzü alın: soğuk kurulum süresi, sıcak kurulum süresi, node_modules boyutu ve ayda yalnızca bir versiyon string’ini değiştirmek için açılan PR sayısı. Migration’ın sizin repo’nuzda işe yarayıp yaramadığını söyleyen bu dört sayıdır ve başlamadan önce toplamak ucuzdur.

Yaygın Hatalar ve Çözümler

Aşırı Merkezileştirme Tuzağı

Yaygın bir hata her dependency’yi catalog’a koymaktır. Tek bir service’in kullandığı özel bir parser veya build aracı oraya ait değildir; yalnızca ilgisiz takımların gözden geçirmek zorunda kaldığı bir dosya büyütür. Catalog’u birden fazla pakette görünen dependency’lere ayırın.

Phantom Dependency Sürprizi

Migration sırasında service’ler bazen hoist edilmiş bir dependency’ye dayandıkları için kırılır. Kod, node_modules içinde bulunan ama kendi package.json’ında hiç tanımlanmamış bir paketi import eder; daha sıkı yerleşim bunu görünür kılar.

Hata CI’da değil yerelde çıksın diye development’ta strict mode ile çalışın:

# .npmrc
strict-peer-dependencies=true
shamefully-hoist=false

Publishing Tuzağı

Bir paket publish edildiğinde catalog: protokolü çözülmüş versiyonla değiştirilir; çünkü workspace dışındaki tüketicilerin okuyabileceği bir catalog yoktur. Yani publish edilen manifest, diff’te kimsenin gözden geçirmediği bir versiyon taşıyabilir.

Release job’ı çalışmadan önce paketlenen manifest’i CI’da kontrol edin:

# .github/workflows/publish.yml
- name: Published paketi validate et
  run: |
    npm pack
    tar -xzf *.tgz
    cat package/package.json | jq '.dependencies'

Gelişmiş Uygulama Pattern’leri

Multi-Environment Catalog’lar

Named catalog’lar legacy ayrımının ötesinde ikinci bir sorunu daha çözer. Service’ler farklı Node runtime’larına deploy ediliyorsa, type paketleri repo’yu değil runtime’ı takip etmek zorundadır:

catalogs:
  # Node 20 runtime'ında kalan service'ler
  node20:
    "@types/node": ^20.19.0

  # Node 22'ye geçmiş service'ler
  node22:
    "@types/node": ^22.7.0

Bir service kendini "@types/node": "catalog:node20" ile sabitler; runtime güncellemesi de repo genelinde arama yapmak yerine o pakette tek kelimelik bir değişikliğe dönüşür.

Otomatik Dependency Güncellemeleri

Catalog kayıtları tek dosyada durduğu için Renovate bunları tek bir zamanlanmış PR’da gruplayabilir:

// renovate.json
{
  "extends": ["config:recommended"],
  "packageRules": [
    {
      "matchFileNames": ["pnpm-workspace.yaml"],
      "groupName": "catalog dependency'leri",
      "schedule": ["every weekend"]
    }
  ]
}

Bir versiyon güncellemesi, o paketi tanımlayan her paket için ayrı PR yerine pnpm-workspace.yaml üzerinde tek bir PR’a dönüşür.

Baştan Verilecek Kararlar

Beş karar şimdi daha ucuz, sonra daha pahalı:

  1. Catalog’lara repo büyümeden geçin. Retrofit maliyeti, halihazırda versiyon tanımlayan paket sayısıyla ölçeklenir.

  2. Strict mode’u baştan açın. Esnek bir kurulum, phantom dependency’leri migration hepsini bir anda ortaya dökene kadar gizler.

  3. Named catalog’ları ihtiyaç duymadan önce tasarlayın. Tek catalog’u sonradan bölmek, ikinci grupta olması gereken her pakete dokunmak demektir.

  4. Validation script’ini ilk catalog’la birlikte yazın. Benimsemeden sonra eklenen enforcement, her zaman çoktan merge edilmiş ihlaller bulur.

  5. İstisnaların gerekçesini dokümante edin. catalog: protokolü kendini anlatır; bir paketin neden catalog dışında sabitlendiği ise anlatmaz.

Migration Checklist’i

Değerlendirme

  • Tüm paketlerde tanımlı dependency’leri denetle
  • Birden fazla versiyonda tanımlanmış her paketi listele
  • Soğuk kurulum süresini, sıcak kurulum süresini ve node_modules boyutunu kaydet

Pilot

  • pnpm v10 veya üstünü kur
  • En çok paylaşılan iki üç dependency’yi catalog:’a taşı
  • Başka bir şey migrate etmeden önce CI’a catalog validation ekle
  • Taşınamayan paketleri ve nedenlerini yaz

Ölçekle

  • Oluşturma script’ini kalan paketlerde çalıştır
  • Runtime veya major versiyon farkı olan yerlerde named catalog’lara ayır
  • Renovate’i pnpm-workspace.yaml’a yönlendir

Sıkılaştır

  • shamefully-hoist=false yap ve kırılanları düzelt
  • Yalnızca tek paketin kullandığı catalog kayıtlarını kaldır
  • Başlangıç ölçümüyle yeniden karşılaştır

Sonuç

pnpm workspace’iniz dependency paylaşan birkaç paketten fazlasını barındırıyorsa, doğru varsayılan catalog’lardır. Kazanç, aynı framework’ün, aynı test runner’ının ve aynı type paketlerinin her package.json’da göründüğü yerde en yüksektir.

Atlamak için iki durum var. Tek paketli bir repo’nun paylaşılan versiyon dosyasından kazanacağı bir şey yoktur. Paketleri bağımsız olarak, bilinçli biçimde farklı destek pencerelerine sahip tüketicilere publish edilen bir workspace ise catalog’la sürekli çekişir; orada paket başına aralık ve güvenlik yamaları için hedefli bir override daha az sürtünme yaratır.

Migration’a başlarsanız tüm dependency listesiyle değil, her yerde görünen iki üç paketle başlayın. Validation script’i catalog’un kendisinden daha önemlidir: CI’da bir kontrol yoksa, satır içi versiyonlar birinin acelesi olduğu ilk anda geri gelir.

Kaynaklar

  • pnpm Katalog Dokümantasyonu - pnpm-workspace.yaml dosyasında yeniden kullanılabilir bağımlılık versiyonu sabitleri tanımlamak için resmi pnpm katalog özelliği rehberi.
  • pnpm Çalışma Alanları Dokümantasyonu - pnpm’in yerleşik monorepo desteğine yönelik resmi rehber; workspace protokolü ve paket bağlantısını kapsar.
  • pnpm-workspace.yaml Referansı - Katalog ve workspace tanımları dahil pnpm-workspace.yaml yapılandırma dosyası için tam referans.
  • pnpm Paket Yöneticisi - Disk kullanımını azaltan içerik adreslenebilir depo ve hard-link yaklaşımını kapsayan resmi pnpm dokümantasyonu.
  • pnpm Paket Kaynakları - pnpm’nin paketleri nasıl çözdüğüne dair dokümantasyon; catalog: protokolünü ve bağımlılık çözümlemesini anlamak için yararlı.
  • Renovate Yapılandırma Seçenekleri - Yukarıda catalog güncellemelerini tek PR’da gruplamak için kullanılan packageRules, matchFileNames ve zamanlama referansı.

İlgili yazılar