Rust ile Hazırladığın WASM Paketini npm’e Yayınlama

Rust ile geliştirdiğin bir WebAssembly paketini npm’e yayınlamak, ilk bakışta karmaşık görünebilir. Ama doğru araçları ve adımları bilince, bu süreç oldukça sistematik bir hal alıyor. Bu yazıda gerçek bir senaryo üzerinden gideceğiz: bir metin işleme kütüphanesi yazacağız, WASM’a derleyeceğiz ve npm registry’e publish edeceğiz.

Neden Rust + WASM + npm?

JavaScript ekosistemi inanılmaz geniş ama performans gerektiren işlerde Rust’ın sunduğu hız farkı ciddi. Özellikle kriptografik işlemler, görüntü manipülasyonu, veri sıkıştırma veya büyük veri setlerini parse etme gibi senaryolarda Rust tabanlı WASM modülleri JS’e kıyasla 10x hatta 50x hız farkı yaratabilir. npm’e yayınlamak ise bu gücü JavaScript geliştiricilerinin eline npm install kadar basit bir komutla vermek anlamına geliyor.

Geliştirme Ortamını Hazırlama

Başlamadan önce gerekli araçların kurulu olduğundan emin olalım.

# Rust kurulumu (zaten kuruluysa geç)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

# wasm-pack kurulumu - ana aracımız bu
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# wasm32 target'ı ekle
rustup target add wasm32-unknown-unknown

# Node.js ve npm versiyonlarını kontrol et
node --version   # v18+ önerilir
npm --version    # v9+ önerilir

# npm hesabına giriş yap
npm login

Burada dikkat edilmesi gereken nokta: wasm-pack, sadece derleme değil, npm paketi oluşturma ve yayınlama sürecini de yönetiyor. Bunu Rust’ın cargo publish komutunun npm karşılığı gibi düşünebilirsin.

Proje Oluşturma

Gerçek dünya senaryomuzu belirleyelim: Türkçe metin analizi yapan bir kütüphane. Kelime sayma, karakter frekansı hesaplama ve basit metin temizleme fonksiyonları içerecek.

# wasm-pack ile yeni proje oluştur
wasm-pack new turkce-metin-analiz
cd turkce-metin-analiz

# Proje yapısına bak
ls -la
# Göreceklerin:
# Cargo.toml
# src/
# src/lib.rs
# tests/
# .gitignore

wasm-pack new komutu temel şablonu oluşturuyor ama Cargo.toml‘u ihtiyaçlarımıza göre düzenleyelim.

[package]
name = "turkce-metin-analiz"
version = "0.1.0"
edition = "2021"
description = "Türkçe metin analizi için WebAssembly kütüphanesi"
license = "MIT"
repository = "https://github.com/kullaniciad/turkce-metin-analiz"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = { version = "0.1.7", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"

[dependencies.web-sys]
version = "0.3"
features = ["console"]

[profile.release]
opt-level = "s"        # Boyutu optimize et
lto = true             # Link Time Optimization

opt-level = "s" ayarı önemli: WASM için boyut optimizasyonu çoğu zaman hız optimizasyonundan daha değerli çünkü kullanıcılar bu dosyayı ağ üzerinden indirecek.

Rust Kodunu Yazma

Şimdi asıl işi yapacak kodu yazalım. src/lib.rs dosyasını açıp şöyle dolduralım:

use wasm_bindgen::prelude::*;
use std::collections::HashMap;

// Panic hook'unu aktif et (debug için hayat kurtarıcı)
#[wasm_bindgen(start)]
pub fn main() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

/// Metindeki kelime sayısını döner
#[wasm_bindgen]
pub fn kelime_say(metin: &str) -> u32 {
    if metin.trim().is_empty() {
        return 0;
    }
    metin
        .split_whitespace()
        .filter(|s| !s.is_empty())
        .count() as u32
}

/// Metindeki karakter frekansını hesaplar
/// JSON formatında döner: {"a": 5, "b": 3, ...}
#[wasm_bindgen]
pub fn karakter_frekansi(metin: &str) -> Result<JsValue, JsValue> {
    let mut frekans: HashMap<char, u32> = HashMap::new();
    
    for karakter in metin.chars() {
        if karakter.is_alphabetic() {
            *frekans.entry(karakter.to_lowercase().next().unwrap()).or_insert(0) += 1;
        }
    }
    
    // HashMap'i sıralı bir yapıya çevir
    let mut sirali: Vec<(String, u32)> = frekans
        .into_iter()
        .map(|(k, v)| (k.to_string(), v))
        .collect();
    sirali.sort_by(|a, b| b.1.cmp(&a.1));
    
    serde_wasm_bindgen::to_value(&sirali)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

/// Metni temizler: fazla boşlukları kaldırır, lowercase yapar
#[wasm_bindgen]
pub fn metin_temizle(metin: &str) -> String {
    metin
        .split_whitespace()
        .collect::<Vec<&str>>()
        .join(" ")
        .to_lowercase()
}

/// Ortalama kelime uzunluğunu hesaplar
#[wasm_bindgen]
pub fn ortalama_kelime_uzunlugu(metin: &str) -> f64 {
    let kelimeler: Vec<&str> = metin.split_whitespace().collect();
    if kelimeler.is_empty() {
        return 0.0;
    }
    
    let toplam_uzunluk: usize = kelimeler.iter().map(|k| k.chars().count()).sum();
    toplam_uzunluk as f64 / kelimeler.len() as f64
}

/// Struct tabanlı analiz sonucu - daha zengin veri döner
#[wasm_bindgen]
pub struct MetinAnaliz {
    kelime_sayisi: u32,
    karakter_sayisi: u32,
    cumle_sayisi: u32,
    ortalama_kelime: f64,
}

#[wasm_bindgen]
impl MetinAnaliz {
    #[wasm_bindgen(constructor)]
    pub fn new(metin: &str) -> MetinAnaliz {
        MetinAnaliz {
            kelime_sayisi: kelime_say(metin),
            karakter_sayisi: metin.chars().count() as u32,
            cumle_sayisi: metin.split(['.', '!', '?']).filter(|s| !s.trim().is_empty()).count() as u32,
            ortalama_kelime: ortalama_kelime_uzunlugu(metin),
        }
    }

    #[wasm_bindgen(getter)]
    pub fn kelime_sayisi(&self) -> u32 { self.kelime_sayisi }

    #[wasm_bindgen(getter)]
    pub fn karakter_sayisi(&self) -> u32 { self.karakter_sayisi }

    #[wasm_bindgen(getter)]
    pub fn cumle_sayisi(&self) -> u32 { self.cumle_sayisi }

    #[wasm_bindgen(getter)]
    pub fn ortalama_kelime(&self) -> f64 { self.ortalama_kelime }
}

#[wasm_bindgen] macro’su burada sihiri yapıyor. Rust fonksiyonlarını JavaScript’in anlayabileceği bir arayüze çeviriyor. getter attribute’ları sayesinde JavaScript tarafında analiz.kelime_sayisi şeklinde property gibi erişim mümkün oluyor.

Test Yazma ve Çalıştırma

Publish öncesi testler kritik. Hem Rust native testleri hem de WASM testleri yazalım.

# src/lib.rs içine test modülü ekle (dosyanın sonuna)
#[cfg(test)]
mod testler {
    use super::*;

    #[test]
    fn kelime_sayma_testi() {
        assert_eq!(kelime_say("merhaba dünya"), 2);
        assert_eq!(kelime_say("  boşluklu   metin  "), 2);
        assert_eq!(kelime_say(""), 0);
        assert_eq!(kelime_say("tek"), 1);
    }

    #[test]
    fn metin_temizleme_testi() {
        assert_eq!(metin_temizle("  MERHABA   DÜNYA  "), "merhaba dünya");
        assert_eq!(metin_temizle("Rust   ile   WASM"), "rust ile wasm");
    }

    #[test]
    fn ortalama_uzunluk_testi() {
        // "ab cd ef" -> (2+2+2)/3 = 2.0
        assert_eq!(ortalama_kelime_uzunlugu("ab cd ef"), 2.0);
        assert_eq!(ortalama_kelime_uzunlugu(""), 0.0);
    }
}
# Native Rust testlerini çalıştır
cargo test

# WASM testlerini tarayıcı modunda çalıştır
wasm-pack test --headless --firefox

# Ya da Chrome ile
wasm-pack test --headless --chrome

Testler geçtikten sonra build aşamasına geçebiliriz.

WASM Paketini Build Etme

wasm-pack birkaç farklı build hedefi destekliyor. npm için bundler veya web target kullanılır.

# npm/bundler hedefi için build (webpack, rollup, vite ile kullanım)
wasm-pack build --target bundler --out-dir pkg

# Vanilla JS veya ES modules için
wasm-pack build --target web --out-dir pkg-web

# Node.js için
wasm-pack build --target nodejs --out-dir pkg-node

# Release build (production için - daha küçük boyut)
wasm-pack build --target bundler --release --out-dir pkg

Build tamamlanınca pkg/ dizinine bak:

ls -lh pkg/
# turkce_metin_analiz_bg.wasm     <- Asıl WASM dosyası
# turkce_metin_analiz.js          <- JS wrapper
# turkce_metin_analiz.d.ts        <- TypeScript tanımları
# turkce_metin_analiz_bg.wasm.d.ts
# package.json                    <- npm paketi için

wasm-pack otomatik olarak TypeScript tanımlarını da oluşturuyor. Bu, TypeScript kullanan geliştiriciler için büyük kolaylık.

package.json Düzenleme

Otomatik oluşturulan package.json‘ı publish öncesi düzenlemek gerekiyor:

cat pkg/package.json
# Gördüklerini şöyle düzenle:
{
  "name": "@kullaniciadi/turkce-metin-analiz",
  "version": "0.1.0",
  "description": "Türkçe metin analizi için WebAssembly kütüphanesi",
  "main": "turkce_metin_analiz.js",
  "types": "turkce_metin_analiz.d.ts",
  "files": [
    "turkce_metin_analiz_bg.wasm",
    "turkce_metin_analiz.js",
    "turkce_metin_analiz.d.ts",
    "turkce_metin_analiz_bg.wasm.d.ts"
  ],
  "keywords": ["wasm", "webassembly", "rust", "türkçe", "metin-analiz"],
  "repository": {
    "type": "git",
    "url": "https://github.com/kullaniciadi/turkce-metin-analiz"
  },
  "license": "MIT",
  "homepage": "https://github.com/kullaniciadi/turkce-metin-analiz#readme"
}

Scoped paket kullanmak (@kullaniciadi/paket-adi) özellikle genel isimli paketlerde namespace çakışmasını önler. npm’de pek çok genel isim zaten kapılmış durumda.

npm’e Yayınlama

Artık her şey hazır. Publish adımlarını dikkatli takip edelim.

# npm'e giriş yaptığından emin ol
npm whoami

# Paketi publish et (pkg/ dizininden)
cd pkg
npm publish --access public

# Scoped paket public yayınlanmazsa --access public şart
# Private publish için (ücretli npm hesabı gerekir):
# npm publish --access restricted

# Belirli bir tag ile publish (beta sürüm için):
# npm publish --tag beta --access public

İlk publish sonrası versiyon güncellemeleri için süreci şöyle yönetebilirsin:

# Projenin kök dizinine dön
cd ..

# Değişikliklerini yap, testleri çalıştır
cargo test
wasm-pack test --headless --firefox

# Yeni build al
wasm-pack build --target bundler --release --out-dir pkg

# pkg/package.json'da versiyonu manuel güncelle ya da:
cd pkg
npm version patch   # 0.1.0 -> 0.1.1
npm version minor   # 0.1.0 -> 0.2.0
npm version major   # 0.1.0 -> 1.0.0

npm publish --access public

CI/CD Pipeline Kurma

Manuel publish yerine GitHub Actions ile otomatik yayın daha güvenli ve tekrarlanabilir.

mkdir -p .github/workflows
cat > .github/workflows/publish.yml << 'EOF'
name: Publish WASM Package

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Rust kurulum
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      
      - name: wasm-pack kurulum
        run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
      
      - name: Testleri çalıştır
        run: cargo test
      
      - name: WASM build al
        run: wasm-pack build --target bundler --release --out-dir pkg
      
      - name: Node.js kur
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'
      
      - name: npm'e publish et
        run: |
          cd pkg
          npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
EOF

NPM_TOKEN‘ı GitHub repo ayarlarında Secrets bölümüne eklemeyi unutma. npm’den npm token create komutuyla oluşturabilirsin.

Paketin Kullanımını Test Etme

Publish ettiğin paketi gerçek bir projede test edelim:

mkdir test-projesi
cd test-projesi
npm init -y
npm install @kullaniciadi/turkce-metin-analiz

# Webpack veya Vite kurulu bir proje için:
cat > index.js << 'EOF'
import init, { 
  kelime_say, 
  metin_temizle, 
  MetinAnaliz 
} from '@kullaniciadi/turkce-metin-analiz';

async function main() {
  // WASM modülünü başlat
  await init();
  
  const ornek = "Rust ile WebAssembly geliştirmek gerçekten çok keyifli! Her şey o kadar hızlı çalışıyor ki.";
  
  console.log("Kelime sayısı:", kelime_say(ornek));
  console.log("Temiz metin:", metin_temizle(ornek));
  
  const analiz = new MetinAnaliz(ornek);
  console.log("Tam analiz:", {
    kelimeler: analiz.kelime_sayisi,
    karakterler: analiz.karakter_sayisi,
    cumleler: analiz.cumle_sayisi,
    ort_kelime: analiz.ortalama_kelime.toFixed(2)
  });
  
  // Belleği temizle
  analiz.free();
}

main().catch(console.error);
EOF

Dikkat: Rust’tan gelen struct instance’larında free() çağırmak önemli. wasm-bindgen bellek yönetimini manuel yapmanı gerektiriyor çünkü JavaScript’in garbage collector’ü WASM belleğini otomatik temizleyemiyor.

Yaygın Sorunlar ve Çözümleri

Süreçte karşılaşabileceğin bazı tipik problemler:

Paket boyutu büyük çıkıyor: Cargo.toml‘a wasm-opt ekle ve release build kullan. wasm-pack build --release komutunun --dev yerine kullanıldığından emin ol.

TypeScript tipleri eksik veya hatalı: wasm-bindgen‘in en güncel versiyonunu kullandığından emin ol. Eski versiyonlarda tip üretimi eksik olabiliyor.

CORS hatası alıyorum: WASM dosyaları application/wasm MIME tipiyle serve edilmeli. Geliştirme sunucunu buna göre ayarla.

npm publish 402 hatası: Scoped paketler için --access public flag’ini eklemeyi unutuyorsun.

Struct instance free() sonrası kullanım: JavaScript tarafında bir WASM struct’ını free() çağrısından sonra kullanmaya çalışırsan runtime hatası alırsın. Bunu önlemek için nullish checking veya try-finally bloğu kullan.

Build sonrası WASM dosyası bulunamıyor: pkg/ dizinindeki dosya adı, Rust crate ismindeki tirelerin alt çizgiye dönmesiyle oluşur. turkce-metin-analiz crate’i, turkce_metin_analiz.js üretir.

Versiyon Stratejisi ve Semver

npm paketleri için semver’e uymak kritik. Kullanıcıların paketini güncellerken karşılaşabilecekleri breaking change’leri minimal tutmak için:

  • Yeni fonksiyon ekleme: minor versiyon artışı
  • Mevcut fonksiyon parametresi değişikliği: major versiyon artışı
  • Bug fix ve performans iyileştirmeleri: patch versiyon artışı
  • Alpha/beta sürümler için: 0.1.0-beta.1 formatı

wasm-bindgen API’sini değiştirdiğinde Rust tarafındaki değişiklik otomatik olarak TypeScript tanımlarına yansıyor. Bu nedenle bir fonksiyonun imzasını değiştirmek her zaman major bump gerektirir.

Sonuç

Rust ile WASM paketi geliştirip npm’e yayınlamak başlangıçta toolchain kurulumu açısından biraz zahmetli görünse de süreci bir kez oturtunca son derece akıcı hale geliyor. wasm-pack bu sürecin en kritik parçası: derleme, paketleme, TypeScript tanımları üretme ve publish işlemlerini tek elden yönetiyor.

Gerçek dünyada bu yaklaşım en çok şu senaryolarda değer yaratıyor: mevcut bir Rust kütüphanesini JavaScript ekosistemine açmak, performans kritik hesaplamaları JS’den WASM’a taşımak veya aynı iş mantığını hem native hem de web ortamında kullanmak istediğinde. Bir kez Rust’ta yazıp WASM, native binary ve hatta shared library olarak deploy edebilmek, kod tekrarını ortadan kaldırıyor.

CI/CD pipeline’ını erken kurmak da büyük fark yaratıyor. Tag push’u ile otomatik publish akışını baştan yerleştirirsen, sürüm yönetimi son derece disiplinli bir hal alıyor. Artık “acaba hangi versiyonu publish etmiştim” karmaşası yaşamıyorsun.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir