jskelet 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +287 -287
  7. package/docs/03-routing.md +480 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1209 -1202
  11. package/docs/08-build.md +366 -366
  12. package/docs/09-dev-araclari.md +335 -335
  13. package/docs/10-dagitim.md +329 -329
  14. package/docs/12-panel-ve-oturum.md +384 -384
  15. package/docs/README.md +105 -105
  16. package/docs/en/01-getting-started.md +292 -292
  17. package/docs/en/02-architecture.md +305 -305
  18. package/docs/en/03-routing.md +497 -497
  19. package/docs/en/04-rendering.md +504 -504
  20. package/docs/en/05-islands.md +492 -492
  21. package/docs/en/06-caching.md +1239 -1232
  22. package/docs/en/07-configuration.md +986 -986
  23. package/docs/en/08-build.md +383 -383
  24. package/docs/en/09-dev-tools.md +342 -342
  25. package/docs/en/10-deployment.md +332 -332
  26. package/docs/en/11-migration.md +359 -359
  27. package/docs/en/12-dashboards-and-sessions.md +392 -392
  28. package/docs/en/README.md +112 -112
  29. package/package.json +102 -102
  30. package/src/build/ensure-build.mjs +15 -15
  31. package/src/build/paths.mjs +143 -143
  32. package/src/build/resolve-peer.mjs +36 -36
  33. package/src/build/tasks/client.mjs +268 -268
  34. package/src/build/tasks/css.mjs +124 -124
  35. package/src/build/tasks/fonts.mjs +146 -146
  36. package/src/build/tasks/icons.mjs +224 -224
  37. package/src/build/tasks/images.mjs +244 -244
  38. package/src/build/tasks/precompress.mjs +78 -78
  39. package/src/client/cache-panel/i18n.js +670 -0
  40. package/src/client/cache-panel/login.html +74 -71
  41. package/src/client/cache-panel/panel.css +756 -740
  42. package/src/client/cache-panel/panel.html +308 -307
  43. package/src/client/cache-panel/panel.js +915 -808
  44. package/src/client/devtools/report.html +185 -185
  45. package/src/client/devtools/report.js +725 -725
  46. package/src/client/dom.js +95 -95
  47. package/src/client/form.js +192 -192
  48. package/src/client/index.js +35 -35
  49. package/src/client/registry.js +297 -297
  50. package/src/client/safe-image.js +91 -91
  51. package/src/client/store.js +36 -36
  52. package/src/client/swap.js +188 -188
  53. package/src/config/pattern.js +107 -107
  54. package/src/http/control-flow.js +71 -71
  55. package/src/http/cookies.js +257 -257
  56. package/src/http/request-cache.js +46 -46
  57. package/src/http/request-context.js +162 -162
  58. package/src/index.js +83 -83
  59. package/src/init.mjs +221 -221
  60. package/src/runtime/alias-hooks.mjs +119 -119
  61. package/src/runtime/register.mjs +4 -4
  62. package/src/server/assets.js +147 -147
  63. package/src/server/cache-deps.js +42 -42
  64. package/src/server/cache-panel.js +759 -738
  65. package/src/server/cloudflare.js +607 -595
  66. package/src/server/create-app.js +291 -291
  67. package/src/server/data-cache.js +462 -462
  68. package/src/server/dev/report.js +369 -369
  69. package/src/server/dev/socket.js +170 -170
  70. package/src/server/dev/version-check.mjs +139 -139
  71. package/src/server/html-cache.js +817 -817
  72. package/src/server/metadata.js +102 -102
  73. package/src/server/middleware/compression.js +205 -205
  74. package/src/server/middleware/csrf.js +134 -134
  75. package/src/server/middleware/dev-gate.js +62 -62
  76. package/src/server/middleware/headers.js +37 -37
  77. package/src/server/middleware/redirects.js +32 -32
  78. package/src/server/middleware/static-precompressed.js +100 -100
  79. package/src/server/middleware/upstream-proxy.js +141 -141
  80. package/src/server/prewarm.js +601 -601
  81. package/src/server/redis.js +569 -569
  82. package/src/server/router.js +128 -128
  83. package/src/server/status-page.js +164 -164
  84. package/src/server/upstream-limiter.js +376 -376
  85. package/src/server/upstream-tracking.js +166 -166
  86. package/src/start.mjs +7 -7
  87. package/src/templates/layout.ejs +44 -44
  88. package/src/version.mjs +31 -31
  89. package/src/views/components/loader.js +85 -85
  90. package/src/views/helpers/html.js +102 -102
  91. package/src/views/helpers/tags.js +245 -245
package/docs/02-mimari.md CHANGED
@@ -1,287 +1,287 @@
1
- # 02 — Mimari ve kararların gerekçeleri
2
-
3
- Bu belge JSkelet'in nasıl çalıştığını değil, **neden böyle çalıştığını**
4
- anlatır. Bir isteğin sunucudan tarayıcıya kadar izlediği yol, island modelinin
5
- neden görünürlüğe bağlı olduğu, HTML'in neden tam üretildiği, önbelleğin neden
6
- süreç belleğinde durduğu ve middleware sırasının neden yer değiştirmemesi
7
- gerektiği burada. Gerekçelerin çoğu kaynak dosyaların başlıklarındaki ölçüm
8
- notlarından geliyor; API'lerin kendisi için [03](./03-routing.md),
9
- [04](./04-render-ve-sablonlar.md), [05](./05-islands.md) ve
10
- [06](./06-cache.md) numaralı belgelere bakın.
11
-
12
- ## Temel önerme
13
-
14
- Bir haber ya da içerik sitesinde ziyaretçinin gördüğü şeyin neredeyse tamamı
15
- sunucuda hazırdır. Etkileşim ise nokta nokta dağılmıştır: bir arama kutusu, bir
16
- drawer, bir grafik, bir yorum formu. Bu profilde tüm sayfayı istemcide yeniden
17
- kurmak (hidrasyon) ödediğiniz en büyük maliyettir ve karşılığında ziyaretçi
18
- hiçbir şey kazanmaz.
19
-
20
- JSkelet bu gözlemi mimarinin merkezine alır:
21
-
22
- 1. **Sunucu HTML'i tamdır.** JS çalışmasa bile sayfa okunur, gezilebilir ve
23
- indekslenebilir.
24
- 2. **JS yalnızca davranış ekler.** Her etkileşimli parça bağımsız bir "island"
25
- olarak, kendi modülüyle, kendi zamanında bağlanır.
26
- 3. **Sayfa üretimi önbelleklenir.** Aynı HTML'i her istekte yeniden üretmenin
27
- anlamı yok; TTL'li bir bellek önbelleği ISR'nin yerini tutar.
28
-
29
- ## Bir isteğin yolu
30
-
31
- ```
32
- İstek
33
- ├─ rewrites(beforeFiles) config → proxy ya da req.url değişimi
34
- ├─ compression brotli/gzip pazarlığı (kalite 5)
35
- ├─ headers statik cache + config headers()
36
- ├─ devGate DEV_TOKEN varsa token yoksa 404
37
- ├─ redirects config redirects(), ilk eşleşen kazanır
38
- ├─ staticPrecompressed build'de üretilmiş .br/.gz kopyalar (kalite 11)
39
- ├─ express.static public/ altındaki dosyalar
40
- ├─ (dev) devtools yalnızca NODE_ENV=development
41
- ├─ body parser'lar urlencoded 64kb + json 256kb
42
- ├─ rewrites(afterFiles) statik denendikten sonra
43
- ├─ route'lar
44
- │ └─ route(controller)
45
- │ └─ withHtmlCache TTL + stale-while-revalidate
46
- │ └─ withUpstreamTracking
47
- │ └─ withRequestCache
48
- │ └─ controller → renderPage → EJS
49
- ├─ 404 → hooks.notFound()
50
- └─ hata yönetimi redirect/notFound + 500 fallback
51
- ```
52
-
53
- ## Middleware sırası neden bu sıra
54
-
55
- `src/server/create-app.js` dosyasının asıl değeri sıradır; her konumun bir
56
- sebebi var ve yer değiştirmek sessiz bozulmalara yol açıyor.
57
-
58
- - **`rewrites(beforeFiles)` statik dosyalardan da önce.** Aksi hâlde
59
- `/assets/x.js` yolunu başka bir yere taşıyan bir kural hiç işlemez, çünkü
60
- `express.static` isteği önce yanıtlar.
61
- - **`compression`, static'ten önce.** Sonra gelirse statik dosyalar hiç
62
- sıkışmaz.
63
- - **`headers` → `devGate` → `redirects`.** Gate'in 404'ü redirect'ten önce
64
- gelmeli: yayına açılmamış bir ortam, yönlendirme kurallarını bile dışarıya
65
- sızdırmamalı.
66
- - **`staticPrecompressed`, `express.static`ten önce.** Build'de üretilmiş
67
- `.br`/`.gz` kopyalar varsa onlar servis edilir (brotli kalite 11); yoksa
68
- istek altındaki `static`e düşer ve middleware anında sıkıştırır (kalite 5).
69
- Hash'li ve `immutable` bir dosyayı her istekte yeniden sıkıştırmak boşa CPU.
70
- - **Body parser'lar statikten sonra.** Görsel isteklerinde gövde ayrıştırma
71
- maliyeti ödenmesin.
72
- - **`rewrites(afterFiles)`, statik denendikten sonra ve sayfalardan önce.**
73
- Next.js'teki iki fazlı rewrite semantiğinin karşılığı.
74
- - **404 ve hata yönetimi en sonda.** Hata yöneticisi `notFound`/`redirect`
75
- kontrol akışını da yakalar, çünkü bunlar bir controller dışında (ör. bir
76
- middleware içinde) da fırlatılabilir.
77
-
78
- Framework `x-powered-by`'ı kapatır ve yerine markalanabilir bir başlık yazar,
79
- `etag`i `strong` yapar ve `trust proxy`yi açar. `trust proxy` ters proxy
80
- arkasında doğru protokol ve istemci IP'si için gerekli
81
- ([10-dagitim.md](./10-dagitim.md)).
82
-
83
- ## Island modeli: neden görünürlüğe bağlı hidrasyon
84
-
85
- `src/client/registry.js` her `[data-island]` elementini bir
86
- `IntersectionObserver`'a verir (`rootMargin: "200px 0px"`). Ekranda olanlar
87
- zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana kadar **hiç
88
- indirilmez**. Ana sayfadaki grafik kütüphanesi gibi ağır modüller böylece ilk
89
- yükten tamamen çıkar.
90
-
91
- Üç davranış var, hepsi HTML'den kontrol edilir:
92
-
93
- - **Varsayılan:** görünürlüğe bağlı.
94
- - **`data-island-eager`:** görünürlükten bağımsız, hemen bağlanır. Header,
95
- çerez bandı gibi global davranışlar için.
96
- - **`data-island-idle`:** görünür olsa bile `load` tamamlanıp ana iş parçacığı
97
- boşalana kadar bekler. İlk ekranda görünen ama kritik olmayan ağır modüller
98
- (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
99
-
100
- İki ek ayrıntı ölçümden geldi:
101
-
102
- - **Bağlama işi boş zamana kaydırılır** (`requestIdleCallback`, `timeout: 500`).
103
- Aynı anda görünen çok sayıda island tek bir uzun task'a dönüşürse TBT ve INP
104
- bozulur.
105
- - **Düzen kutusu olmayan elementler doğrudan bağlanır.** `hidden` bir
106
- drawer/dialog'un düzen kutusu yoktur ve `IntersectionObserver` onu asla
107
- bildirmez; bu yüzden `hydrate()` ölçümleri tek seferde okur
108
- (`getClientRects().length`) ve kutusu olmayanları gözlemciye vermek yerine
109
- hemen bağlar.
110
-
111
- Buradan çıkan bir sonuç: **görsel hata yönetimi island değildir.** Görsel
112
- ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine ayrı island bağlamak
113
- (gözlemci + dinamik import + mount) sırf hata ihtimali için ciddi bir hidrasyon
114
- yükü. `startSafeImages()` bunun yerine belgeye tek bir yakalama fazı
115
- dinleyicisi kurar ([05-islands.md](./05-islands.md)).
116
-
117
- ## Sunucu HTML'i neden tam
118
-
119
- Layout ve sayfa şablonu, ziyaretçinin göreceği içeriğin tamamını üretir.
120
- İstemci tarafında "iskelet göster, sonra doldur" deseni yoktur. Bunun üç
121
- karşılığı var:
122
-
123
- 1. **SEO:** kazıyıcı JS beklemek zorunda kalmaz.
124
- 2. **LCP:** en büyük içerik öğesi ilk HTML yanıtında gelir; JS'in indirilmesi,
125
- ayrıştırılması ve çalıştırılması LCP yolunda değildir.
126
- 3. **CLS:** içerik sonradan enjekte edilmediği için düzen kaymaz.
127
-
128
- Aynı ilke `<head>` tarafında da uygulanır. Layout kaynak ipuçlarını
129
- (`preconnect`, LCP `preload`) `<head>`in **en başına** basar; bunları
130
- geciktirmek doğrudan LCP'ye yazılır.
131
-
132
- ### Neden tek, render-blocking stylesheet
133
-
134
- Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
135
- kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında CLS
136
- 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış tek
137
- sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci ziyarette
138
- zaten `immutable` önbellekten geliyor.
139
-
140
- Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
141
- yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
142
- setini göndermek 1500+ ikon, yani birkaç megabayt; tarama sprite'ı tipik olarak
143
- 10-30 sembolde tutuyor ([08-build.md](./08-build.md)).
144
-
145
- ## Cache stratejisi: ISR yerine bellek içi TTL
146
-
147
- `src/server/html-cache.js` route + query anahtarlı, TTL'li, LRU bir HTML
148
- önbelleği tutar (en fazla 500 girdi). TTL dolduğunda girdi hemen atılmaz: `stale`
149
- pencerede eski HTML anında döner ve tazeleme arkada çalışır
150
- (stale-while-revalidate, `STALE_FACTOR = 1`, yani stale penceresi TTL kadar).
151
-
152
- Kazanç: ilk ısıtmadan sonra hiçbir istek render'ı beklemez. Bedel: HTML'deki
153
- veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
154
- kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
155
- güncelleniyor ve gecikme ekranda görünmüyor.
156
-
157
- Diske yazmama kararı bilinçli. Next'teki build-time prerender'ın karşılığı
158
- prewarm'dır ama çıktı diske yazılmaz: önbellek süreç belleğinde yaşadığı için
159
- ısıtma da süreç ayağa kalkınca yapılır. Kazanç aynı — ilk ziyaretçi soğuk
160
- render'ı beklemez — fakat veri dondurulmaz; her girdi route'un `revalidate`
161
- süresiyle yaşlanır ([06-cache.md](./06-cache.md)).
162
-
163
- ### Sıkıştırılmış gövdenin önbellekte durması
164
-
165
- Önbelleğe alınan her girdi, brotli/gzip çıktısını HTML ile birlikte saklar
166
- (`encoded` haritası, HTML ile aynı ömrü paylaşır). Aynı sayfa her istekte
167
- yeniden brotli'lenmez. `Content-Encoding` bu yolda `route()` içinde ayarlandığı
168
- için sıkıştırma middleware'i devreye girmez.
169
-
170
- ### Neden geçici ve kalıcı upstream hataları farklı ele alınır
171
-
172
- Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir ve böyle
173
- bir HTML önbelleğe **yazılmaz**: sonraki istek yeniden dener.
174
-
175
- Ancak bu yalnızca *geçici* hatalar için geçerli (ağ hatası, 408, 425, 429 ve tüm
176
- 5xx). 400/403/404 gibi deterministik cevaplar tekrar denemekle düzelmez; onlar
177
- yüzünden önbelleği kapatmak sayfayı her ziyarette baştan render etmek olur —
178
- içerik yine aynı eksik hâliyle döner, ziyaretçi sadece render süresini öder. Bu
179
- yüzden kalıcı hatalar yalnızca loglanır, önbelleği engellemez.
180
-
181
- Bu bilginin framework'e ulaşma yönü de bilinçli olarak terstir: framework veri
182
- katmanını tanımaz, veri katmanı framework'e haber verir
183
- (`reportUpstreamFailure()`). Hiç çağıran olmazsa maliyet boş bir dizidir.
184
-
185
- ### Üç kapsamın iç içe sırası
186
-
187
- `route()` şu sırayı kurar:
188
-
189
- ```
190
- withHtmlCache( withUpstreamTracking( withRequestCache( controller ) ) )
191
- ```
192
-
193
- Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
194
- tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
195
- eksik veriyle üretilen çıktı önbelleğe yazılmasın.
196
-
197
- ## Hata toleransı: hiçbir eksik siteyi indirmez
198
-
199
- Framework boyunca tekrarlanan bir ilke var: eksik yapılandırma ya da eksik build
200
- çıktısı, hata yerine bozulmuş ama çalışan bir sayfa üretir.
201
-
202
- - **Config dosyası yoksa ya da okunamıyorsa** uyarı basılır ve sunucu
203
- varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi açılamaz hâle
204
- getirmemeli. Aynı şekilde `headers()`/`redirects()`/`rewrites()`/`cache()`
205
- bölümlerinden biri hata verirse yalnızca o bölüm yok sayılır.
206
- - **Hook'lar hata verirse** framework kendi varsayılanına döner ve uyarır.
207
- - **Build çalışmadıysa** `asset()` `/assets/<isim>` döner, `hasAsset()` false
208
- olur ve layout stylesheet/script etiketlerini hiç basmaz. `jskelet build`
209
- unutulduğunda hata yerine stilsiz ama çalışan bir sayfa görürsünüz.
210
- - **Dev'de bozuk bir route modülü** uyarı basıp atlanır; **üretimde fırlatır.**
211
- Yarım route tablosuyla yayına çıkmak, sessizce 404 dönen sayfalar demek.
212
- - **404 render'ı da patlarsa** şablonsuz, minimal bir HTML döner; ziyaretçi boş
213
- yanıt görmesin.
214
- - **Tek bir istek hatası süreci düşürmez:** `unhandledRejection` ve
215
- `uncaughtException` loglanır ve süreç ayakta kalır. Bir haber sitesinde tek
216
- sayfanın hatası tüm siteyi indirmemeli.
217
-
218
- ## Neden dosya sistemi tabanlı routing yok
219
-
220
- Sıra önemli. `/:slug` gibi tek segmentli bir yakalayıcı `/about` rotasından önce
221
- kaydedilirse "about" bir slug sanılır. Sırayı dosya adına gizlemek yerine
222
- görünür kılmak teşhisi kolaylaştırıyor: ya `jskelet.config.mjs` → `routes` ile
223
- açık bir liste verirsiniz, ya da `routes/` dizinini alfabetik taratıp dosya
224
- adlarına sayısal önek koyarsınız (`10-pages.js`, `50-blog.js`,
225
- `99-catch-all.js`). Ayrıntı: [03-routing.md](./03-routing.md).
226
-
227
- ## Neden tek bir config gerçek kaynağı
228
-
229
- `src/config/index.js` proje kökünü, dizin yollarını, markalamayı, hook'ları ve
230
- kuralları normalize eder. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
231
- Sebebi somut: framework `node_modules/` içine girdiğinde `../..` sayarak kök
232
- bulmaya çalışan her dosya bozulur. Aynı gerekçeyle build tarafında da tek bir
233
- mutasyon noktası var (`initBuildPaths()`).
234
-
235
- `getConfig()` `loadConfig()` çağrılmadan kullanılırsa boş bir proje kökü
236
- varsaymak yerine hata verir: sessiz yanlış yol, "stylesheet neden yok" gibi
237
- teşhisi zor sorunlara dönüşüyor.
238
-
239
- ## Neden bu bağımlılık listesi
240
-
241
- Çalışma zamanı bağımlılıkları dörttür: `express`, `ejs`, `esbuild`,
242
- `tailwind-merge`. Geri kalan her şey (Tailwind, PostCSS, lightningcss, sharp,
243
- Phosphor ikonları) **opsiyonel peer bağımlılığıdır** ve yoksa ilgili build adımı
244
- atlanır.
245
-
246
- İki karar ayrıca açıklanmayı hak ediyor:
247
-
248
- - **`compression` paketi yerine `node:zlib`.** Paket brotli desteklemiyor ve
249
- yedi geçişli bir bağımlılık ağacı getiriyor; brotli + gzip pazarlığını elle
250
- yapmak yeterli. Brotli tercih edilir: ana sayfa HTML'inde gzip'e göre ~%35
251
- daha küçük.
252
- - **`tailwind-merge` çalışma zamanında kalır.** Sınıf hesabı yalnızca sunucuda
253
- yapılır, client bundle'a hiç girmez, dolayısıyla sayfa ağırlığına etkisi
254
- yoktur. Elle yazılmış bir grup tablosu ise `border-2` + `border-transparent`
255
- gibi genişlik/renk çiftlerini birbirine karıştırıp sınıf düşürdüğü için
256
- görsel regresyon üretiyordu.
257
-
258
- Opsiyonel paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün
259
- kendisinden değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa düz
260
- bir `import "postcss"` framework'ün ağacına bakar — uygulamanınkine değil.
261
-
262
- ## Neden alias ve uzantı hook'ları
263
-
264
- `node --import jskelet/register` iki iş yapar:
265
-
266
- 1. `jsconfig.json` / `tsconfig.json` içindeki `compilerOptions.paths`
267
- alias'larını çözer (`@/lib/x` → `<root>/lib/x`). Editör ve çalışma zamanı aynı
268
- dosyadan beslendiği için ikisi birbirinden ayrışmaz.
269
- 2. Uzantısız göreli import'lara uzantı ekler (`./cache` → `./cache.js`). Node ESM
270
- bunu yapmaz ve bundler'dan taşınan kodda en sık karşılaşılan kırılma noktası
271
- budur.
272
-
273
- esbuild tarafındaki `@/` çözümü de aynı davranışı taklit eder, böylece `lib/`
274
- altındaki modüller hem sunucuda hem tarayıcıda aynı import stilini kullanabilir.
275
-
276
- `--import` bir modül **belirteci** bekler, dosya yolu değil. Windows'ta `H:\...`
277
- mutlak yolu `h:` şemalı bir URL sanılıp reddediliyor; bu yüzden framework her
278
- yerde `pathToFileURL(...).href` kullanır. Aynı sebeple config, route modülleri
279
- ve bileşenler de `file://` URL'le import edilir.
280
-
281
- ## Sırada ne var
282
-
283
- - Route ve controller sözleşmesi: [03-routing.md](./03-routing.md)
284
- - Şablon katmanı ve metadata: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
285
- - Island runtime API'si: [05-islands.md](./05-islands.md)
286
- - Önbelleğin ayarları ve prewarm: [06-cache.md](./06-cache.md)
287
- - Dev akışının iç işleyişi: [09-dev-araclari.md](./09-dev-araclari.md)
1
+ # 02 — Mimari ve kararların gerekçeleri
2
+
3
+ Bu belge JSkelet'in nasıl çalıştığını değil, **neden böyle çalıştığını**
4
+ anlatır. Bir isteğin sunucudan tarayıcıya kadar izlediği yol, island modelinin
5
+ neden görünürlüğe bağlı olduğu, HTML'in neden tam üretildiği, önbelleğin neden
6
+ süreç belleğinde durduğu ve middleware sırasının neden yer değiştirmemesi
7
+ gerektiği burada. Gerekçelerin çoğu kaynak dosyaların başlıklarındaki ölçüm
8
+ notlarından geliyor; API'lerin kendisi için [03](./03-routing.md),
9
+ [04](./04-render-ve-sablonlar.md), [05](./05-islands.md) ve
10
+ [06](./06-cache.md) numaralı belgelere bakın.
11
+
12
+ ## Temel önerme
13
+
14
+ Bir haber ya da içerik sitesinde ziyaretçinin gördüğü şeyin neredeyse tamamı
15
+ sunucuda hazırdır. Etkileşim ise nokta nokta dağılmıştır: bir arama kutusu, bir
16
+ drawer, bir grafik, bir yorum formu. Bu profilde tüm sayfayı istemcide yeniden
17
+ kurmak (hidrasyon) ödediğiniz en büyük maliyettir ve karşılığında ziyaretçi
18
+ hiçbir şey kazanmaz.
19
+
20
+ JSkelet bu gözlemi mimarinin merkezine alır:
21
+
22
+ 1. **Sunucu HTML'i tamdır.** JS çalışmasa bile sayfa okunur, gezilebilir ve
23
+ indekslenebilir.
24
+ 2. **JS yalnızca davranış ekler.** Her etkileşimli parça bağımsız bir "island"
25
+ olarak, kendi modülüyle, kendi zamanında bağlanır.
26
+ 3. **Sayfa üretimi önbelleklenir.** Aynı HTML'i her istekte yeniden üretmenin
27
+ anlamı yok; TTL'li bir bellek önbelleği ISR'nin yerini tutar.
28
+
29
+ ## Bir isteğin yolu
30
+
31
+ ```
32
+ İstek
33
+ ├─ rewrites(beforeFiles) config → proxy ya da req.url değişimi
34
+ ├─ compression brotli/gzip pazarlığı (kalite 5)
35
+ ├─ headers statik cache + config headers()
36
+ ├─ devGate DEV_TOKEN varsa token yoksa 404
37
+ ├─ redirects config redirects(), ilk eşleşen kazanır
38
+ ├─ staticPrecompressed build'de üretilmiş .br/.gz kopyalar (kalite 11)
39
+ ├─ express.static public/ altındaki dosyalar
40
+ ├─ (dev) devtools yalnızca NODE_ENV=development
41
+ ├─ body parser'lar urlencoded 64kb + json 256kb
42
+ ├─ rewrites(afterFiles) statik denendikten sonra
43
+ ├─ route'lar
44
+ │ └─ route(controller)
45
+ │ └─ withHtmlCache TTL + stale-while-revalidate
46
+ │ └─ withUpstreamTracking
47
+ │ └─ withRequestCache
48
+ │ └─ controller → renderPage → EJS
49
+ ├─ 404 → hooks.notFound()
50
+ └─ hata yönetimi redirect/notFound + 500 fallback
51
+ ```
52
+
53
+ ## Middleware sırası neden bu sıra
54
+
55
+ `src/server/create-app.js` dosyasının asıl değeri sıradır; her konumun bir
56
+ sebebi var ve yer değiştirmek sessiz bozulmalara yol açıyor.
57
+
58
+ - **`rewrites(beforeFiles)` statik dosyalardan da önce.** Aksi hâlde
59
+ `/assets/x.js` yolunu başka bir yere taşıyan bir kural hiç işlemez, çünkü
60
+ `express.static` isteği önce yanıtlar.
61
+ - **`compression`, static'ten önce.** Sonra gelirse statik dosyalar hiç
62
+ sıkışmaz.
63
+ - **`headers` → `devGate` → `redirects`.** Gate'in 404'ü redirect'ten önce
64
+ gelmeli: yayına açılmamış bir ortam, yönlendirme kurallarını bile dışarıya
65
+ sızdırmamalı.
66
+ - **`staticPrecompressed`, `express.static`ten önce.** Build'de üretilmiş
67
+ `.br`/`.gz` kopyalar varsa onlar servis edilir (brotli kalite 11); yoksa
68
+ istek altındaki `static`e düşer ve middleware anında sıkıştırır (kalite 5).
69
+ Hash'li ve `immutable` bir dosyayı her istekte yeniden sıkıştırmak boşa CPU.
70
+ - **Body parser'lar statikten sonra.** Görsel isteklerinde gövde ayrıştırma
71
+ maliyeti ödenmesin.
72
+ - **`rewrites(afterFiles)`, statik denendikten sonra ve sayfalardan önce.**
73
+ Next.js'teki iki fazlı rewrite semantiğinin karşılığı.
74
+ - **404 ve hata yönetimi en sonda.** Hata yöneticisi `notFound`/`redirect`
75
+ kontrol akışını da yakalar, çünkü bunlar bir controller dışında (ör. bir
76
+ middleware içinde) da fırlatılabilir.
77
+
78
+ Framework `x-powered-by`'ı kapatır ve yerine markalanabilir bir başlık yazar,
79
+ `etag`i `strong` yapar ve `trust proxy`yi açar. `trust proxy` ters proxy
80
+ arkasında doğru protokol ve istemci IP'si için gerekli
81
+ ([10-dagitim.md](./10-dagitim.md)).
82
+
83
+ ## Island modeli: neden görünürlüğe bağlı hidrasyon
84
+
85
+ `src/client/registry.js` her `[data-island]` elementini bir
86
+ `IntersectionObserver`'a verir (`rootMargin: "200px 0px"`). Ekranda olanlar
87
+ zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana kadar **hiç
88
+ indirilmez**. Ana sayfadaki grafik kütüphanesi gibi ağır modüller böylece ilk
89
+ yükten tamamen çıkar.
90
+
91
+ Üç davranış var, hepsi HTML'den kontrol edilir:
92
+
93
+ - **Varsayılan:** görünürlüğe bağlı.
94
+ - **`data-island-eager`:** görünürlükten bağımsız, hemen bağlanır. Header,
95
+ çerez bandı gibi global davranışlar için.
96
+ - **`data-island-idle`:** görünür olsa bile `load` tamamlanıp ana iş parçacığı
97
+ boşalana kadar bekler. İlk ekranda görünen ama kritik olmayan ağır modüller
98
+ (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
99
+
100
+ İki ek ayrıntı ölçümden geldi:
101
+
102
+ - **Bağlama işi boş zamana kaydırılır** (`requestIdleCallback`, `timeout: 500`).
103
+ Aynı anda görünen çok sayıda island tek bir uzun task'a dönüşürse TBT ve INP
104
+ bozulur.
105
+ - **Düzen kutusu olmayan elementler doğrudan bağlanır.** `hidden` bir
106
+ drawer/dialog'un düzen kutusu yoktur ve `IntersectionObserver` onu asla
107
+ bildirmez; bu yüzden `hydrate()` ölçümleri tek seferde okur
108
+ (`getClientRects().length`) ve kutusu olmayanları gözlemciye vermek yerine
109
+ hemen bağlar.
110
+
111
+ Buradan çıkan bir sonuç: **görsel hata yönetimi island değildir.** Görsel
112
+ ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine ayrı island bağlamak
113
+ (gözlemci + dinamik import + mount) sırf hata ihtimali için ciddi bir hidrasyon
114
+ yükü. `startSafeImages()` bunun yerine belgeye tek bir yakalama fazı
115
+ dinleyicisi kurar ([05-islands.md](./05-islands.md)).
116
+
117
+ ## Sunucu HTML'i neden tam
118
+
119
+ Layout ve sayfa şablonu, ziyaretçinin göreceği içeriğin tamamını üretir.
120
+ İstemci tarafında "iskelet göster, sonra doldur" deseni yoktur. Bunun üç
121
+ karşılığı var:
122
+
123
+ 1. **SEO:** kazıyıcı JS beklemek zorunda kalmaz.
124
+ 2. **LCP:** en büyük içerik öğesi ilk HTML yanıtında gelir; JS'in indirilmesi,
125
+ ayrıştırılması ve çalıştırılması LCP yolunda değildir.
126
+ 3. **CLS:** içerik sonradan enjekte edilmediği için düzen kaymaz.
127
+
128
+ Aynı ilke `<head>` tarafında da uygulanır. Layout kaynak ipuçlarını
129
+ (`preconnect`, LCP `preload`) `<head>`in **en başına** basar; bunları
130
+ geciktirmek doğrudan LCP'ye yazılır.
131
+
132
+ ### Neden tek, render-blocking stylesheet
133
+
134
+ Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
135
+ kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında CLS
136
+ 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış tek
137
+ sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci ziyarette
138
+ zaten `immutable` önbellekten geliyor.
139
+
140
+ Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
141
+ yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
142
+ setini göndermek 1500+ ikon, yani birkaç megabayt; tarama sprite'ı tipik olarak
143
+ 10-30 sembolde tutuyor ([08-build.md](./08-build.md)).
144
+
145
+ ## Cache stratejisi: ISR yerine bellek içi TTL
146
+
147
+ `src/server/html-cache.js` route + query anahtarlı, TTL'li, LRU bir HTML
148
+ önbelleği tutar (en fazla 500 girdi). TTL dolduğunda girdi hemen atılmaz: `stale`
149
+ pencerede eski HTML anında döner ve tazeleme arkada çalışır
150
+ (stale-while-revalidate, `STALE_FACTOR = 1`, yani stale penceresi TTL kadar).
151
+
152
+ Kazanç: ilk ısıtmadan sonra hiçbir istek render'ı beklemez. Bedel: HTML'deki
153
+ veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
154
+ kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
155
+ güncelleniyor ve gecikme ekranda görünmüyor.
156
+
157
+ Diske yazmama kararı bilinçli. Next'teki build-time prerender'ın karşılığı
158
+ prewarm'dır ama çıktı diske yazılmaz: önbellek süreç belleğinde yaşadığı için
159
+ ısıtma da süreç ayağa kalkınca yapılır. Kazanç aynı — ilk ziyaretçi soğuk
160
+ render'ı beklemez — fakat veri dondurulmaz; her girdi route'un `revalidate`
161
+ süresiyle yaşlanır ([06-cache.md](./06-cache.md)).
162
+
163
+ ### Sıkıştırılmış gövdenin önbellekte durması
164
+
165
+ Önbelleğe alınan her girdi, brotli/gzip çıktısını HTML ile birlikte saklar
166
+ (`encoded` haritası, HTML ile aynı ömrü paylaşır). Aynı sayfa her istekte
167
+ yeniden brotli'lenmez. `Content-Encoding` bu yolda `route()` içinde ayarlandığı
168
+ için sıkıştırma middleware'i devreye girmez.
169
+
170
+ ### Neden geçici ve kalıcı upstream hataları farklı ele alınır
171
+
172
+ Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir ve böyle
173
+ bir HTML önbelleğe **yazılmaz**: sonraki istek yeniden dener.
174
+
175
+ Ancak bu yalnızca *geçici* hatalar için geçerli (ağ hatası, 408, 425, 429 ve tüm
176
+ 5xx). 400/403/404 gibi deterministik cevaplar tekrar denemekle düzelmez; onlar
177
+ yüzünden önbelleği kapatmak sayfayı her ziyarette baştan render etmek olur —
178
+ içerik yine aynı eksik hâliyle döner, ziyaretçi sadece render süresini öder. Bu
179
+ yüzden kalıcı hatalar yalnızca loglanır, önbelleği engellemez.
180
+
181
+ Bu bilginin framework'e ulaşma yönü de bilinçli olarak terstir: framework veri
182
+ katmanını tanımaz, veri katmanı framework'e haber verir
183
+ (`reportUpstreamFailure()`). Hiç çağıran olmazsa maliyet boş bir dizidir.
184
+
185
+ ### Üç kapsamın iç içe sırası
186
+
187
+ `route()` şu sırayı kurar:
188
+
189
+ ```
190
+ withHtmlCache( withUpstreamTracking( withRequestCache( controller ) ) )
191
+ ```
192
+
193
+ Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
194
+ tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
195
+ eksik veriyle üretilen çıktı önbelleğe yazılmasın.
196
+
197
+ ## Hata toleransı: hiçbir eksik siteyi indirmez
198
+
199
+ Framework boyunca tekrarlanan bir ilke var: eksik yapılandırma ya da eksik build
200
+ çıktısı, hata yerine bozulmuş ama çalışan bir sayfa üretir.
201
+
202
+ - **Config dosyası yoksa ya da okunamıyorsa** uyarı basılır ve sunucu
203
+ varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi açılamaz hâle
204
+ getirmemeli. Aynı şekilde `headers()`/`redirects()`/`rewrites()`/`cache()`
205
+ bölümlerinden biri hata verirse yalnızca o bölüm yok sayılır.
206
+ - **Hook'lar hata verirse** framework kendi varsayılanına döner ve uyarır.
207
+ - **Build çalışmadıysa** `asset()` `/assets/<isim>` döner, `hasAsset()` false
208
+ olur ve layout stylesheet/script etiketlerini hiç basmaz. `jskelet build`
209
+ unutulduğunda hata yerine stilsiz ama çalışan bir sayfa görürsünüz.
210
+ - **Dev'de bozuk bir route modülü** uyarı basıp atlanır; **üretimde fırlatır.**
211
+ Yarım route tablosuyla yayına çıkmak, sessizce 404 dönen sayfalar demek.
212
+ - **404 render'ı da patlarsa** şablonsuz, minimal bir HTML döner; ziyaretçi boş
213
+ yanıt görmesin.
214
+ - **Tek bir istek hatası süreci düşürmez:** `unhandledRejection` ve
215
+ `uncaughtException` loglanır ve süreç ayakta kalır. Bir haber sitesinde tek
216
+ sayfanın hatası tüm siteyi indirmemeli.
217
+
218
+ ## Neden dosya sistemi tabanlı routing yok
219
+
220
+ Sıra önemli. `/:slug` gibi tek segmentli bir yakalayıcı `/about` rotasından önce
221
+ kaydedilirse "about" bir slug sanılır. Sırayı dosya adına gizlemek yerine
222
+ görünür kılmak teşhisi kolaylaştırıyor: ya `jskelet.config.mjs` → `routes` ile
223
+ açık bir liste verirsiniz, ya da `routes/` dizinini alfabetik taratıp dosya
224
+ adlarına sayısal önek koyarsınız (`10-pages.js`, `50-blog.js`,
225
+ `99-catch-all.js`). Ayrıntı: [03-routing.md](./03-routing.md).
226
+
227
+ ## Neden tek bir config gerçek kaynağı
228
+
229
+ `src/config/index.js` proje kökünü, dizin yollarını, markalamayı, hook'ları ve
230
+ kuralları normalize eder. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
231
+ Sebebi somut: framework `node_modules/` içine girdiğinde `../..` sayarak kök
232
+ bulmaya çalışan her dosya bozulur. Aynı gerekçeyle build tarafında da tek bir
233
+ mutasyon noktası var (`initBuildPaths()`).
234
+
235
+ `getConfig()` `loadConfig()` çağrılmadan kullanılırsa boş bir proje kökü
236
+ varsaymak yerine hata verir: sessiz yanlış yol, "stylesheet neden yok" gibi
237
+ teşhisi zor sorunlara dönüşüyor.
238
+
239
+ ## Neden bu bağımlılık listesi
240
+
241
+ Çalışma zamanı bağımlılıkları dörttür: `express`, `ejs`, `esbuild`,
242
+ `tailwind-merge`. Geri kalan her şey (Tailwind, PostCSS, lightningcss, sharp,
243
+ Phosphor ikonları) **opsiyonel peer bağımlılığıdır** ve yoksa ilgili build adımı
244
+ atlanır.
245
+
246
+ İki karar ayrıca açıklanmayı hak ediyor:
247
+
248
+ - **`compression` paketi yerine `node:zlib`.** Paket brotli desteklemiyor ve
249
+ yedi geçişli bir bağımlılık ağacı getiriyor; brotli + gzip pazarlığını elle
250
+ yapmak yeterli. Brotli tercih edilir: ana sayfa HTML'inde gzip'e göre ~%35
251
+ daha küçük.
252
+ - **`tailwind-merge` çalışma zamanında kalır.** Sınıf hesabı yalnızca sunucuda
253
+ yapılır, client bundle'a hiç girmez, dolayısıyla sayfa ağırlığına etkisi
254
+ yoktur. Elle yazılmış bir grup tablosu ise `border-2` + `border-transparent`
255
+ gibi genişlik/renk çiftlerini birbirine karıştırıp sınıf düşürdüğü için
256
+ görsel regresyon üretiyordu.
257
+
258
+ Opsiyonel paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün
259
+ kendisinden değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa düz
260
+ bir `import "postcss"` framework'ün ağacına bakar — uygulamanınkine değil.
261
+
262
+ ## Neden alias ve uzantı hook'ları
263
+
264
+ `node --import jskelet/register` iki iş yapar:
265
+
266
+ 1. `jsconfig.json` / `tsconfig.json` içindeki `compilerOptions.paths`
267
+ alias'larını çözer (`@/lib/x` → `<root>/lib/x`). Editör ve çalışma zamanı aynı
268
+ dosyadan beslendiği için ikisi birbirinden ayrışmaz.
269
+ 2. Uzantısız göreli import'lara uzantı ekler (`./cache` → `./cache.js`). Node ESM
270
+ bunu yapmaz ve bundler'dan taşınan kodda en sık karşılaşılan kırılma noktası
271
+ budur.
272
+
273
+ esbuild tarafındaki `@/` çözümü de aynı davranışı taklit eder, böylece `lib/`
274
+ altındaki modüller hem sunucuda hem tarayıcıda aynı import stilini kullanabilir.
275
+
276
+ `--import` bir modül **belirteci** bekler, dosya yolu değil. Windows'ta `H:\...`
277
+ mutlak yolu `h:` şemalı bir URL sanılıp reddediliyor; bu yüzden framework her
278
+ yerde `pathToFileURL(...).href` kullanır. Aynı sebeple config, route modülleri
279
+ ve bileşenler de `file://` URL'le import edilir.
280
+
281
+ ## Sırada ne var
282
+
283
+ - Route ve controller sözleşmesi: [03-routing.md](./03-routing.md)
284
+ - Şablon katmanı ve metadata: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
285
+ - Island runtime API'si: [05-islands.md](./05-islands.md)
286
+ - Önbelleğin ayarları ve prewarm: [06-cache.md](./06-cache.md)
287
+ - Dev akışının iç işleyişi: [09-dev-araclari.md](./09-dev-araclari.md)