jskelet 0.6.1 → 0.6.3

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