jskelet 0.2.5 → 0.3.0

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