jskelet 0.6.3 → 0.6.4

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 (153) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +628 -620
  3. package/LICENSE +21 -21
  4. package/README.md +2 -0
  5. package/bin/jskelet.mjs +130 -130
  6. package/docs/01-baslangic.md +291 -291
  7. package/docs/02-mimari.md +310 -310
  8. package/docs/03-routing.md +515 -515
  9. package/docs/04-render-ve-sablonlar.md +667 -661
  10. package/docs/05-islands.md +486 -486
  11. package/docs/06-cache.md +1467 -1443
  12. package/docs/07-yapilandirma.md +1208 -1197
  13. package/docs/08-build.md +429 -429
  14. package/docs/09-dev-araclari.md +364 -364
  15. package/docs/10-dagitim.md +348 -338
  16. package/docs/12-panel-ve-oturum.md +479 -478
  17. package/docs/README.md +83 -83
  18. package/docs/en/01-getting-started.md +298 -298
  19. package/docs/en/02-architecture.md +329 -329
  20. package/docs/en/03-routing.md +531 -531
  21. package/docs/en/04-rendering.md +675 -669
  22. package/docs/en/05-islands.md +497 -497
  23. package/docs/en/06-caching.md +1476 -1453
  24. package/docs/en/07-configuration.md +1229 -1219
  25. package/docs/en/08-build.md +447 -447
  26. package/docs/en/09-dev-tools.md +373 -373
  27. package/docs/en/10-deployment.md +351 -340
  28. package/docs/en/11-migration.md +398 -398
  29. package/docs/en/12-dashboards-and-sessions.md +489 -488
  30. package/docs/en/README.md +87 -87
  31. package/package.json +137 -137
  32. package/src/build/ensure-build.mjs +19 -19
  33. package/src/build/paths.mjs +153 -153
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +349 -349
  36. package/src/build/tasks/css.mjs +235 -235
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +357 -357
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/build/tasks/templates.mjs +20 -20
  42. package/src/client/admin/i18n.js +764 -764
  43. package/src/client/admin/login.html +74 -74
  44. package/src/client/admin/panel.css +809 -809
  45. package/src/client/admin/panel.html +495 -495
  46. package/src/client/admin/panel.js +1251 -1251
  47. package/src/client/devtools/report.html +185 -185
  48. package/src/client/devtools/report.js +745 -745
  49. package/src/client/devtools/seo.js +628 -628
  50. package/src/client/dom.js +95 -95
  51. package/src/client/form.js +192 -192
  52. package/src/client/index.js +45 -45
  53. package/src/client/registry.js +305 -305
  54. package/src/client/safe-image.js +91 -91
  55. package/src/client/shared-cookie.js +225 -225
  56. package/src/client/store.js +36 -36
  57. package/src/client/swap.js +188 -188
  58. package/src/compile/codegen.js +336 -336
  59. package/src/compile/compile-all.js +149 -149
  60. package/src/compile/errors.js +66 -66
  61. package/src/compile/expr.js +409 -409
  62. package/src/compile/index.js +17 -17
  63. package/src/compile/parse.js +541 -541
  64. package/src/compile/resolve.js +211 -211
  65. package/src/compile/scan-exports.js +51 -51
  66. package/src/config/defaults.js +541 -534
  67. package/src/config/index.js +1500 -1469
  68. package/src/config/pattern.js +107 -107
  69. package/src/generate.mjs +163 -163
  70. package/src/http/control-flow.js +71 -71
  71. package/src/http/cookies-entry.js +21 -21
  72. package/src/http/cookies.js +277 -277
  73. package/src/http/request-cache.js +46 -46
  74. package/src/http/request-context.js +165 -165
  75. package/src/http/shared-cookie.js +178 -178
  76. package/src/index.js +101 -101
  77. package/src/init.mjs +232 -230
  78. package/src/migrate/apply.mjs +262 -262
  79. package/src/migrate/babel.mjs +79 -79
  80. package/src/migrate/classify.mjs +155 -155
  81. package/src/migrate/config.mjs +126 -126
  82. package/src/migrate/fs-walk.mjs +191 -191
  83. package/src/migrate/parse.mjs +26 -26
  84. package/src/migrate/scan.mjs +177 -177
  85. package/src/migrate/transform/expr-source.mjs +168 -168
  86. package/src/migrate/transform/island.mjs +67 -67
  87. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  88. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  89. package/src/migrate/transform/page-split.mjs +435 -435
  90. package/src/migrate/write.mjs +81 -81
  91. package/src/migrate.mjs +171 -171
  92. package/src/runtime/alias-hooks.mjs +119 -119
  93. package/src/runtime/register.mjs +4 -4
  94. package/src/server/admin/actions.js +229 -229
  95. package/src/server/admin/auth.js +125 -125
  96. package/src/server/admin/event-log.js +151 -151
  97. package/src/server/admin/gate.js +209 -209
  98. package/src/server/admin/inventory.js +188 -188
  99. package/src/server/admin/mount.js +56 -56
  100. package/src/server/admin/router.js +216 -216
  101. package/src/server/admin/snapshot.js +241 -241
  102. package/src/server/assets.js +147 -147
  103. package/src/server/auth/handoff.js +309 -309
  104. package/src/server/cache-blob.js +70 -70
  105. package/src/server/cache-control.js +45 -0
  106. package/src/server/cache-deps.js +42 -42
  107. package/src/server/cache-vary.js +113 -113
  108. package/src/server/cloudflare.js +607 -607
  109. package/src/server/create-app.js +366 -366
  110. package/src/server/data-cache.js +553 -553
  111. package/src/server/dev/report.js +485 -485
  112. package/src/server/dev/socket.js +170 -170
  113. package/src/server/dev/version-check.mjs +139 -139
  114. package/src/server/disk-cache.js +233 -233
  115. package/src/server/ejs-adapter.js +59 -59
  116. package/src/server/html-cache.js +1196 -1196
  117. package/src/server/image-optimizer.js +500 -500
  118. package/src/server/logs/access-middleware.js +66 -66
  119. package/src/server/logs/file-sink.js +193 -193
  120. package/src/server/logs/pipeline.js +165 -165
  121. package/src/server/logs/s3-put.js +214 -214
  122. package/src/server/logs/s3-sink.js +112 -112
  123. package/src/server/metadata.js +102 -102
  124. package/src/server/middleware/compression.js +205 -205
  125. package/src/server/middleware/csrf.js +134 -134
  126. package/src/server/middleware/dev-gate.js +75 -75
  127. package/src/server/middleware/headers.js +37 -37
  128. package/src/server/middleware/redirects.js +32 -32
  129. package/src/server/middleware/robots-txt.js +341 -341
  130. package/src/server/middleware/static-precompressed.js +121 -121
  131. package/src/server/middleware/trailing-slash.js +53 -53
  132. package/src/server/middleware/upstream-proxy.js +141 -141
  133. package/src/server/og-image.js +369 -356
  134. package/src/server/port-guard.js +255 -255
  135. package/src/server/prewarm.js +1082 -1082
  136. package/src/server/redis.js +588 -588
  137. package/src/server/render.js +910 -910
  138. package/src/server/router.js +157 -157
  139. package/src/server/status-page.js +265 -265
  140. package/src/server/upstream-limiter.js +376 -376
  141. package/src/server/upstream-tracking.js +166 -166
  142. package/src/shared/cookie-domain.js +66 -66
  143. package/src/start.mjs +22 -22
  144. package/src/templates/layout.ejs +30 -30
  145. package/src/templates/layout.jsk +30 -30
  146. package/src/version.mjs +31 -31
  147. package/src/views/components/loader.js +101 -101
  148. package/src/views/helpers/html.js +102 -102
  149. package/src/views/helpers/tags.js +375 -375
  150. package/types/config/defaults.d.ts +6 -0
  151. package/types/config/index.d.ts +6 -0
  152. package/types/server/cache-control.d.ts +28 -0
  153. package/types/server/og-image.d.ts +5 -0
package/docs/02-mimari.md CHANGED
@@ -1,310 +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
- Ü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)
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)