Deno Nedir? NodeJS’in Sonunu Getirir mi?
NodeJS günümüzde JavaScript ekosisteminin en önemli parçasını oluşturuyor. Neredeyse her proje için NodeJS’i kullanıyoruz. REST servisler yazıyoruz, websocket ile haberleşme yapıyoruz ya da en basitinden bir uygulamanın bağımlılıkları için npm (Node Package Manager) kullanıyoruz. Fakat NodeJS’i yazan Ryan Dahl’ın da 2018 yılında JS Conf EU’da bahsettiği gibi, NodeJS’in bir çok dezavantajı bulunuyor. Öncelikle bu problemlere değinelim.
1. Promise’in bulunmaması
NodeJS’in ilk versiyonu 2009 yılının Mayıs ayının 27’sinde yayımlanmıştı. Bundan yaklaşık 1 ay sonra, Haziran ayının 30’unda promise yapısı eklendi. Fakat daha sonra ise promise yapısı kaldırıldı. Kaldırılmasındaki gerekçe olarak Ryan aşağıdaki gönderiyi oluşturdu:
Ben de dahil olmak üzere birçok geliştirici, diskteki bir dosyaya erişim işlemi için düşük seviyeli (low-level) bir kod arayüzünün olmasını istiyor. Bu nedenle Promise yapısındaki gibi dosyaya erişim işlemi sonrasında yeni bir JS objesinin otomatik olarak üretilmesi gereksiz oluyor. Diğer açıdan Promise yapısını isteyenler de var. Fakat basitlik açısından Promise yerine callback’li bir yapı kullanacağız ve JS kütüphaneleri için daha iyi şekilde soyutlama katmanları oluşturmayı bir görev edineceğiz.
Kod ile açıklamak gerekirse bir dosyanın içeriğinin okunması için promise’li yapı aşağıdaki gibiydi:
readFile() fonksiyonunun yeni bir JS nesnesi return etmemesi için aşağıdaki gibi fonksiyon parametresi alacak şekilde callback’li bir yapıya dönüştürüldü:
Ryan’ın aktardığına göre, “Node’daki promise yapıları birleşik bir halde kullanıldığında belki de NodeJS ekosistemini daha hızlı bir hale getirebilirdi. Bunu gerçekleştirmediğimiz için bilemiyoruz. Diğer yandan belki de promise yapısının kaldırılması iyiydi. Çünkü bu sayede mevcut NodeJS ekosistemi kendi araçlarını geliştirerek doğru bir şekilde soyutlamayı gerçekleştirebiliyor. Fakat günümüzde kullanılan birçok async API kötü bir şekilde eskimeye maruz bırakılıyor. Açıkça söylemek gerekirse Promise’in kaldırılması düşüncesizce bir karardı. Keşke Promise yapısını kaldırmasaydım.”
2. Güvenlik
JavaScript kodunu çalıştıran V8 motoru, Python’un aksine gayet güvenli bir sandbox (çalışma ortamı) sunmaktadır. Fakat Node’un kendisi güvenliksiz bir yapıda olduğu için projeye dahil edilen herhangi bir kod parçası, istediği herhangi bir sistem çağrısını gerçekleştirebilir (Örn: diske erişebilir veya network çağrısı gerçekleştirebilir). Ryan’ın aktardığına göre, “NodeJS’teki bu güvenliksiz durum, sunucu tarafındaki bir runtime’ın belirli durumlarda güvenlikli olması için elden kaçan bir fırsattı.”
Bildiğiniz gibi eğer bir uygulamaya disk erişim yetkisi verirseniz, uygulamayı yazan programcı diskinizi herhangi bir şekilde sömürebilir. Tabii ki web uygulamaları tarayıcı üzerinde çalıştığı için böyle bir problem oluşmaz. Fakat sunucu uygulamalarında bazı kütüphanelerin diske veya network’e erişimini istemeyebilirsiniz. Örneğin bir linter aracının network’e erişmesine gerek yoktur. Fakat kötü amaçla kullanldığında tüm sistemi dahi ele geçirebilir.
3. Build sistemi (GYP) (Generate Your Projects)
Build sistemleri genellikle zor ve çok önemli oldukları için, Ryan’ın belki de en büyük pişmanlığını GYP build sistemi oluşturmaktadır. Google tarafından geliştirilen GYP‘nin temel amacı, diğer NodeJS projeleri için native eklentiler oluşturmaktır. Örneğin C dilindeki bir kod ile konuşabilen modül yazacaksanız GYP kullanmanız gerekir. Aslında V8 motoru içeren Google Chrome, çok önceden beri build sistemi olarak GYP’yi kullanıyordu. Fakat birkaç yıl sonra GN adında bir araç çıkararak GYP’yi tamamen terk etti. Bunun olmasını NodeJS ekibi de beklemiyordu: Google kendi ürettiği build aracını bırakmıştı. Fakat bu olaydan sonra yıllar geçse bile NodeJS, halen GYP aracını kullanan yegane ekosistemdi.
GYP’nin belki de en kötü özelliği, yapılandırma dosyasının acayip bir şekilde kodlanmasıdır. Aslında JSON gibi görünen bu dosyadaki key-value ifadeleri, JSON’ın aksine tek tırnak ile belirtilmiştir. Ayrıca içerisinde Python kodu da barındırmaktadır. ‘#’ işaretli yorum satırları içermesi ve listenin son elemanından sonra virgül karakterinin konulabilmesi gibi JSON’da bulunmayan özellikler yer almaktadır:
Ayrıca Node’da GYP aracını sarmalayan birçok wrapper bulunmaktadır. Bunlardan en meşhur olanı node-gyp‘dir. Soğan halkaları gibi katman üstüne katman olacak şekilde gereksiz bir karmaşıklık içermektedir. V8 motorunun kendisi de artık GYP’yi kulanmadığı için, Node’u desteklemek amacıyla bir GYP wrapper bulunmakta ve bu da gereksiz karmaşıklıkta bir kod deposuna yol açmaktadır.
Bu gibi nedenlerden dolayı sistem kütüphaneleri ile haberleşmek için GYP yerine farklı bir yöntem belirlemek gereklidir.
4. package.json
package.json dosyasının, proje içerisinde kullanılan JS kütüphanelerini belirttiğini biliyoruz. Normalde npm’in üretmiş olduğu package.json, Node’un require(“”) fonksiyonu tarafından desteklenmeye başladı. Npm daha sonra Node ile birlikte kurulu olarak gelmeye başlayınca package.json dosyası da bir standart haline geldi.
Fakat modül import edilirken kullanılan require("moduleName")
fonksiyonu tam olarak belirli bir path’i belirtmiyor. Ayrıca projede npm haricinde private olarak tutulan başka js kütüphaneleri de olabilir. Bu nedenle require() fonksiyonunu çalıştıran dosyanın bulunduğu yere göre farklı modülü çağrılabilir ve bir karışıklık oluşabilir.
Ayrıca web’de yıllardır bulunan <script>
etiketli yapının aksine, kütüphane kodların içeren dizinlerin package.json dosyasında yer alması ile gereksiz bir modül konsepti oluştu. Buna ek olarak package.json dosyası sadece gereken kütüphaneyi bağlaması gerekirken, içerisinde lisans bilgileri, repository URL’i, yazar adı gibi gereksiz bilgiler yer almaya başladı.
5. node_modules dizini
İçerisinde birbirine bağlı pek çok kütüphaneyi barındıran node_modules dizini gereksiz halde büyük ve bazen boyutu Gigabyte’ları bulabiliyor. Bu nedenle de modül isimlerini çözümlemek de oldukça karmaşık hale gelebiliyor. NodeJS varsayılan olarak node_modules dizini üzerinden çalıştığı için artık bu problemi çözmek de imkansız bir hal almış durumda.
6. require() fonksiyonunda dosya uzantısının yer almaması
JavaScript veya TypeScript gibi herhangi türden bir dosyayı import etmeye yarayan require fonksiyonu, dosya adını uzantısız bir şekilde parametre olarak alıyor. Örneğin kendi dosyanızı import etmek istediğinizde require('./MyFile')
şeklinde yazabiliyorsunuz. Bu başlangıçta mantıklı gibi görünse de gereksiz şekilde soyutlama içeriyor. Tarayıcılar bu şekilde çalışmıyor. Ayrıca ilgili dosyayı yükleyecek olan module loader’ın da uzantıyı tahmin etmesi gerekiyor.
7. index.js
Ryan bu dosyayı web sunucularında varsayılan olarak açılan index.html dosyasına benzer bir şekilde kodun çalıştırılması için eklemiş. Bu dosya da gereksiz bir şekilde modül yükleme sistemini karmaşık hale getiriyor. Ayrıca halihazırda package.json dosyası gerekli tutulduğu için içerisinde başlangıç dosyası da belirtilebilir.
Ryan Dahl, tüm bu problemlerin çözümü için Deno adında yeni bir runtime geliştirdi.
Deno Nedir?
Deno, JavaScript ve TypeScript projeleri geliştirmek için basit, modern ve güvenli bir runtime sunar. V8 JavaScript motorunu kullanır ve performanslı olması için Rust dili ile geliştirilmiştir.
Başlıca özellikleri:
1. Güvenlik ön plandadır
Varsayılan olarak güvenli bir runtime’dır. Dosyaya erişim, ağ istekleri ve çevre birimlerine erişim, açıkça izin verilmediği sürece gerçekleştirilmez.
Bunu bir örnekle deneyimleyelim. Öncelikle Deno’yu bilgisayarımıza yükleyelim. MacOS için brew aracı kullanılarak yüklenebilir:
brew install deno
Not: Diğer işletim sistemleri için https://github.com/denoland/deno/releases adresinden indirebilirsiniz.
https://deno.land/std/examples/ sayfasında Deno ile ilgili birçok örnek bulunuyor. Buradaki örneklerden welcome.ts
‘i ela alalım. Aşağıdaki şekilde çalıştırıldığında, terminale “Welcome to Deno” yazdıracaktır:
deno run https://deno.land/std/examples/welcome.ts
Basit bir örneği ekrana yazdırabildiğimize göre artık dosyalar üzerinde işlem yapmaya başlayabiliriz. Bu amaçla parametre olarak belirtilen bir dosyanın çıktısının ekrana basılması için bir örnek yapalım. Şimdi aşağıdaki gibi dosya.txt oluşturarak içerisine “✨ Dosya testi başarılı.” ifadesini yazdıralım.
echo ✨ Dosya testi başarılı. > dosya.txt
dosya.txt’nin içeriğini ekrana basmak için https://deno.land/std/examples/cat.ts örneğini kullanabiliriz. Öncelikle deno komutuna hiçbir izin vermeden direkt olarak çalıştırmayı deneyelim:
deno run https://deno.land/std/examples/cat.ts dosya.txt
Aşağıdaki gibi “İzin reddedildi” hatasını verecektir:
Şimdi okuma yetkisini vermek için --allow-read
parametresi ile deneyelim:
deno run --allow-read https://deno.land/std/examples/cat.ts dosya.txt
Gördüğünüz gibi açıkça izin verildiğinde başarılı bir şekilde dosya içeriğini okuyarak ekrana basacaktır:
1.1 Mimarisi sayesinde güvenlikli bir yapı oluşturur
NodeJS, halihazırda sistem kütüphaneleri ile binding edildiği için, herhangi bir noktadan sisteme direkt olarak çağrı işlemi yapılabilir. Bu nedenle güvenlik açısından sisteme hangi çağrıların gerçekleştiğini kontrol etmek zorlaşmaktadır. Deno’da ise, sisteme yapılan tüm istekler protobuf üzerinden yürütülerek message passing işlemi ile gerçekleştirilir. Haberleşme tek bir noktadan yürütüldüğü için sisteme yapılan çağrılar kolaylıkla takip edilebilir:
2. TypeScript’i varsayılan olarak destekler
Deno, TypeScript kodu çalıştırmak için içerisinde varsayılan olarak TypeScript derleyicisi ile birlikte gelmektedir. Dolayısıyla TypeScript kodunu derlemek ve çalıştırmak için tsc aracına ihtiyacınız yoktur. Deno, açıkça belirtilmiş olan dosyanın uzantısına bakarak kodun TS mi yoksa JS mi olduğunu bilir ve “sihirli” bir modül çözümleme işlemine gerek kalmadan kod derlenir ve çalıştırılır.
Ayrıca Typescript dosyasında, JavaScript kodları kullanılırken, type definition dosyalarının belirlenmesi ise 3 şekilde yapılabilir:
2.1 Compiler hint ile (Derleyiciye ipucu sunularak)
Deno’da TypeScript kodu içerisine bir JavaScript kodu import ettiğinizde, eklediğiniz koda ait type definition dosyasının yani “.d.ts” dosyasının da nerede olduğunu belirtebilirsiniz. Bunun için dosyanın en üstüne yorum satırı ile birlikte @deno-types
ipucu kullanılarak tip dosyası belirtilmiş olur.
Örneğin jquery kütüphanesi aşağıdaki gibi tip belirleme dosyası ile import edilebilir:
// @deno-types="./jquery.d.ts"
import * as foo from "./jquery.js";
Tip dosyası belirtme işlemi, modül import edilirken kullanılan modül çözümleme mantığı ile aynı şekilde çalışır. Bu nedenle, tip dosyası belirtilirken dosya uzantısı verilmeli ve mevcut dizine göre dosya yolu relative olarak gösterilmelidir. Ayrıca uzak suncudaki bir d.ts dosyası da import eder gibi belirtilebilir.
2.2. reference direktifi kullanılarak
Deno’da bir modül import etmek yerine modül oluşturup export etmek istiyorsanız bunun için /// <reference types="" />
direktifini kullanabilirsiniz. Modül çözümleme yapısı @deno-types
direktifi ile aynı şekilde geçerlidir ve uzak sunucudaki bir tip dosyasını belirtebilirsiniz. Örnek:
/// <reference types="./foo.d.ts" />
export const foo = "foo";
2.3. X-TypeScript-Types HTTP başlığı kullanılarak
Bu yöntem de reference direktifi ile benzer şekilde çalışır. Tek farkı, JS dosyasının içeriğinin değiştirilmesine gerek kalmaz. Ayrıca tip belirleme dosyasının konumu sunucu tarafından belirlenir. Örneğin pika paket yöneticisi ile https://cdn.pika.dev/jquery sayfasına istek yaptığınızda cevap başlıklarında aşağıdaki gibi görebilirsiniz:
3. Modül sistemi basittir
Modül sistemi, Node’da yer alan uzantısız modül adı kullanımının aksine, Deno’da URL şeklinde belirtilir. Belirtilen URL’deki modül indirilir ve projeye dahil edilir. Sonraki çalışma işlemlerinde indirilen modül tekrar indirilmez. Bunun yerine ön bellekteki mevcut hali yeniden kullanılır. Modülün tekrar indirilmesi için, proje çalıştırılmadan önce deno komutuna --reload
parametresinin verilmesi yeterlidir.
Aşağıdaki örnekte Deno testing kütüphanesi import edilmektedir:
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
assertEquals("hello", "hello");
assertEquals("world", "world");
console.log("Asserted!");
Her dosya içerisinde URL vermek yerine utils.ts gibi tüm tipleri barındıran bir dosya oluşturularak relative path ile kullanılabilir.
Örnek utils.ts
:
export {
assert,
assertEquals,
assertStrContains,
} from "https://deno.land/std/testing/asserts.ts";
Kullanımı:
import { assertEquals, runTests, test } from "./utils.ts";
4. Kurulum gerektirmez
Halihazırda NodeJS’te birçok bağımlılık bulunduğu için bir setup dosyası ile kurulum yapmak gereklidir. Deno ise oldukça sade bir yapıda olduğu için https://github.com/denoland/deno/releases üzerinden çalıştırılabilir dosyayı indirilerek, herhangi bir kurulum yapmadan direkt olarak geliştirime başlanabilir.
5. İçerisinde varsayılan olarak birçok araç bulunur
5.1 bundler (deno bundle)
Belirtilen input dosyasını tüm bağımlılıkları ile birlikte derleyerek bir adet js dosyasının oluşturulmasını sağlar. Örnek kullanım:
deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js
Bu sayede oluşan colors.bundle.js dosyası bir web projesinde aşağıdaki gibi kullanılabilir:
<script type="module" src="colors.bundle.js"></script>
Daha fazla bilgi için: https://deno.land/manual/tools/bundler
5.2 debugger (–inspect, –inspect-brk)
İçerisinde varsayılan olarak debug aracı ile birlikte gelir. Bu sayede Chrome DevTools veya VSCode içerisinde debug yapılabilir. Daha fazla bilgi için: https://deno.land/manual/tools/debugger
5.3 dependency inspector (deno info)
Input olarak verilen dosyanın bağımlılıklarını görüntüler. Örneğin colors.ts dosyası için aşağıdaki gibi kullanılabilir:
deno info https://deno.land/std/examples/colors.ts
Daha fazla bilgi için: https://deno.land/manual/tools/dependency_inspector
5.4 documentation generator (deno doc)
Belirtilen dosya ile ilgili dökümanı getirir. Örneğin Deno’daki Listener interface’i ile bilgi almak için aşağıdaki gibi kullanılabilir:
deno doc --builtin Deno.Listener
Çıktı olarak dosyanın içeriği ve dosya ile ilgili diğer ek bilgiler yer almaktadır:
Daha fazla bilgi için: https://deno.land/manual/tools/documentation_generator
5.5 formatter (deno fmt)
JavaScript ve TypeScript kodunun otomatik olarak formatlanmasını sağlar. Aşağıdaki gibi kullanılabilir:
deno fmt dosya1.ts dosya2.ts
Daha fazla bilgi için: https://deno.land/manual/tools/formatter
5.6 test runner (deno test)
Deno, JavaScript ve TypeScript kodları için unit test aracı ile birlikte sunulmaktadır. Aşağıdaki gibi bir örnek oluşturulabilir.
test1.ts
dosyasını bash üzerinden aşağıdaki gibi oluşturabilirsiniz:
echo 'import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Deno.test("hello world", () => {
const x = 1 + 2;
assertEquals(x, 3);
});' > test1.ts
Test etmek için aşağıdaki gibi deno test komutu kullanılabilir:
deno test test1.ts
Ekran görüntüsü aşağıdaki gibi olacaktır:
Daha fazla bilgi için: https://deno.land/manual/testing
Peki Deno, NodeJS’in yerini alabilir mi?
Deno mevcut halde NPM paketleri ile uygumlu değil. Yeni yeni oluşmaya başlayan bir node uyumluluk katmanı var fakat henüz daha çok başlangıç aşamasında olduğu için kullanımı kısıtlı. Dolayısıyla bir npm paketini direkt olarak deno içerisinde kullanamayacaksınız.
Bu nedenle henüz Node’un yerini alamaz ama web ortamı için çok güçlü bir araç olduğunu söyleyebilirim. Deno henüz v1 sürümünde ve yaygın olarak kullanılmadığı için kesin bir yargıya varmak için erken. Zaman içindeki gelişmeleri birlikte izleyerek hangi noktalara geleceğini görelim.
Bir sonraki yazıda görüşmek üzere…