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:
minorversiyon artışı - Mevcut fonksiyon parametresi değişikliği:
majorversiyon artışı - Bug fix ve performans iyileştirmeleri:
patchversiyon artışı - Alpha/beta sürümler için:
0.1.0-beta.1formatı
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.
