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/08-build.md CHANGED
@@ -1,428 +1,429 @@
1
- # 08 — Build
2
-
3
- Bu belge `jskelet build`in yaptığı her işi ve sırasını anlatır: font kopyalama,
4
- ikon sprite üretimi, Tailwind CSS derlemesi, esbuild ile island bundle'ı, görsel
5
- optimizasyonu, manifest yazımı ve önceden sıkıştırma. Ayrıca hash'li varlıkların
6
- `asset()`/`hasAsset()` ile şablonlara nasıl ulaştığı, Tailwind'in `@source`
7
- direktiflerinin neden zorunlu olduğu ve opsiyonel peer bağımlılıklarının
8
- davranışı burada. Çıktının çalışma anında nasıl servis edildiği
9
- [02-mimari.md](./02-mimari.md)'de, build'i tetikleyen watch akışı
10
- [09-dev-araclari.md](./09-dev-araclari.md)'de.
11
-
12
- ## Hat ve sırası
13
-
14
- ```
15
- 0. Templates .jsk → .jskelet/templates/*.mjs (her zaman; dosya yoksa no-op)
16
- 1. Fonts config.fonts varsa
17
- 2. Icon sprite config.icons !== false ise
18
- 3. CSS styles giriş dosyası varsa
19
- 4. Client JS client/entries/ varsa
20
- 5. Images config.images !== false, watch değil ve sharp kurulu ise
21
- 6. Manifest .jskelet/manifest.json
22
- 7. Precompress watch değilse
23
- ```
24
-
25
- Şablon derlemesi asset taramasından **önce** biter; istek yolunda parse yoktur.
26
- Tailwind `@source` ve ikon taraması kaynak `.jsk` dosyalarını okur (üretilmiş
27
- `.mjs` değil).
28
-
29
- Sıra rastgele değil:
30
-
31
- - **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
32
- ama manifest anahtarı verir.
33
- - **Precompress en sonda:** sıkıştırılacak her şey üretilmiş olmalı.
34
- - **Görseller watch turunda hiç çalışmaz:** `sharp` ile yeniden kodlama pahalı.
35
-
36
- Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir proje
37
- font adımını hiç görmez; bu, "framework her projeye kendi varsayımlarını
38
- dayatmaz" ilkesinin build tarafındaki karşılığı.
39
-
40
- Terminal çıktısı hizalı adım satırları ve sonunda bir `output` bloğu verir: her
41
- varlığın ham ve brotli boyutu, büyükten küçüğe.
42
-
43
- ## Manifest ve hash'li varlıklar
44
-
45
- Build çıktısı `public/assets/` altına **içerik hash'li** adlarla yazılır ve
46
- mantıksal ad → public URL eşlemesi `.jskelet/manifest.json` dosyasına konur:
47
-
48
- ```json
49
- {
50
- "app.css": "/assets/app.4f2a1b9c07.css",
51
- "sprite.svg": "/assets/sprite.dc973997bd.svg",
52
- "main.js": "/assets/js/main.9E1AB2C3.js",
53
- "inter-400.woff2": "/fonts/inter-400.woff2"
54
- }
55
- ```
56
-
57
- Hash sha256'nın ilk 10 hex karakteridir: çakışma için fazlasıyla yeterli ve
58
- dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
59
- `Cache-Control: public, max-age=31536000, immutable` yazılabilir.
60
-
61
- ### `asset(name)` ve `hasAsset(name)`
62
-
63
- Şablonlara otomatik geçer; sunucu kodunda `import { asset, hasAsset } from "jskelet"`.
64
-
65
- ```ejs
66
- <% if (hasAsset('app.css')) { %>
67
- <link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
68
- <% } %>
69
- <% styles.forEach(function (sheet) { %>
70
- <% if (hasAsset(sheet)) { %>
71
- <link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
72
- <% } %>
73
- <% }); %>
74
- ```
75
-
76
- - `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
77
- - `hasAsset(name)` manifest'te olup olmadığını söyler.
78
-
79
- Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
80
- stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
81
- yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
82
- basılır: ``[assets] no manifest — run `jskelet build`.``
83
-
84
- Manifest **dev'de her istekte** yeniden okunur (watch build hash'leri
85
- değiştirir), prod'da bir kez.
86
-
87
- ### Watch modunda manifest tutarlılığı
88
-
89
- Watch turunda yeniden derlenen varlık yeni bir hash'e yazılıp eskisi silinir. Bu
90
- yüzden manifest de güncellenmek zorunda (`patchManifest`): aksi hâlde HTML
91
- silinmiş dosyayı isteyip 404 alır ve sayfa dev oturumunun kalanında stilsiz ya
92
- da JS'siz kalır. CSS ve client görevlerinin ikisi de her turda kendi anahtarını
93
- yamalar; diğer anahtarlar korunur. CSS tarafı watch'ta `syncCssManifest` ile
94
- tüm `.css` anahtarlarını günceller (silinen sayfa sheet'leri de düşer).
95
-
96
- ## CSS — Tailwind v4
97
-
98
- Giriş dosyası `paths.styles` (varsayılan `styles/globals.css`). Dosya yoksa adım
99
- uyarıyla atlanır.
100
-
101
- Boru hattı: PostCSS + `@tailwindcss/postcss` → (varsa) lightningcss ile
102
- minifikasyon → `writeAsset("app.css", …)`.
103
-
104
- - **PostCSS boru hattı bir kez kurulur:** Tailwind'in kendi önbelleği plugin
105
- örneğinde yaşıyor; her derlemede yeniden oluşturmak watch turlarını belirgin
106
- şekilde yavaşlatıyor.
107
- - **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
108
- yalnızca birkaç kB daha büyük olur.
109
- - Global çıktı `app.css`'tir ve layout onu her sayfada render-blocking olarak
110
- yükler. Ayrı bir "critical CSS" üretilmemesinin ölçüm gerekçesi
111
- [02-mimari.md](./02-mimari.md)'de.
112
-
113
- ### Sayfa stylesheet'leri (`styles/pages/`)
114
-
115
- Island `entries` ile aynı sözleşme. `styles/pages/*.css` altındaki her dosya
116
- ayrı bir hash'li varlıktır (`home.css` → `/assets/home.<hash>.css`). Controller
117
- yalnızca istediği sayfada yükler:
118
-
119
- ```js
120
- return {
121
- view: "pages/home",
122
- styles: ["home.css"],
123
- };
124
- ```
125
-
126
- Dizin, `paths.styles` dosyasının yanındaki `pages/` klasörüdür (`styles` taşınırsa
127
- pages de yanında kalır). Dizin yoksa veya boşsa adım yalnızca global sheet üretir.
128
-
129
- Sayfa CSS'i sayfaya özel kurallar içindir. Tailwind utility'leri global sheet'te
130
- kalmalı — dosyada tam `@import "tailwindcss"` utility çıktısını tekrarlar.
131
-
132
- Layout `app.css`ten sonra `styles` dizisindeki her sheet için
133
- `<link data-jskelet-css="…">` basar; `hasAsset` false ise etiket yok.
134
-
135
- ### `@source` direktifleri zorunludur
136
-
137
- Tailwind v4'ün sınıf taraması `globals.css` içindeki `@source` direktiflerine
138
- bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar, bu
139
- yüzden şablonlarda geçen varyantlar (`data-[active=false]:…` gibi) **sessizce
140
- düşer**.
141
-
142
- ```css
143
- @import "tailwindcss" source(none);
144
-
145
- @source "../views";
146
- @source "../client";
147
- @source "../routes";
148
-
149
- .wrapper {
150
- max-width: 48rem;
151
- margin-inline: auto;
152
- padding-inline: 1rem;
153
- padding-block: 2rem;
154
- }
155
- ```
156
-
157
- `source(none)` otomatik tespiti kapatır ve taramayı tamamen açık hâle getirir.
158
- **Yeni bir üst dizin eklediğinizde `@source` satırını da ekleyin** — sınıfların
159
- "bazen çalışmaması"nın en yaygın sebebi budur.
160
-
161
- ### CSS watch kapsamı
162
-
163
- Watch modunda üç hedef izlenir: stylesheet'in bulunduğu dizin, `views` ve
164
- `client`. Şablon ve island dosyaları da izlenir çünkü Tailwind sınıfları oradan
165
- geliyor; yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild
166
- etmezdi. Değişiklikler 120 ms birleştirilir.
167
-
168
- ## Client JS — esbuild
169
-
170
- `client/entries/*.{js,ts,mts}` içindeki her kaynak dosya bir entry'dir (`.tsx`
171
- yok). Manifest anahtarı her zaman `*.js` olur (`main.ts` → `main.js`). Aynı stem
172
- için birden fazla uzantı build hatasıdır. Dizin yoksa ya da boşsa adım atlanır.
173
-
174
- esbuild ayarları:
175
-
176
- | Ayar | Değer | Sebebi |
177
- | --- | --- | --- |
178
- | `bundle`, `splitting` | `true` | Ortak modüller paylaşılan chunk'a çıkar |
179
- | `format` | `esm` | `type="module"` script'ler |
180
- | `target` | `chrome111`, `edge111`, `firefox111`, `safari16.4` | ESM + dinamik import + `IntersectionObserver` island modelinin alt sınırı; daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor |
181
- | `minify` | `true` | — |
182
- | `sourcemap` | yalnızca `NODE_ENV=development` | Prod'da `.map` dosyaları `public/assets` altında yayınlanmaz |
183
- | `entryNames` | `[name].[hash]` | `immutable` cache |
184
- | `chunkNames` | `chunks/[name].[hash]` | — |
185
- | `legalComments` | `none` | — |
186
-
187
- Çıktı `public/assets/js/` altına düşer ve her turda önce temizlenir.
188
- `browserslist` okunmaz; hedef listesi kod içinde sabittir.
189
-
190
- ### `@/` alias'ı
191
-
192
- esbuild tarafında `@/` proje köküne çözülür ve uzantı tamamlama yapılır
193
- (`.js`, `.mjs`, `.ts`, `.mts`, `.json`, `/index.js`, `/index.ts`). Node
194
- `alias-hooks.mjs` sunucuda yalnızca `.js` / `.mjs` / `.json` çözer; paylaşılan
195
- `@/lib` dosyaları bu yüzden `.js` kalmalıdır. Client-only `.ts` import'ları
196
- esbuild hattında çalışır.
197
-
198
- ### `clientEnv` gömülmesi
199
-
200
- Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de `process.env`
201
- okur. `config.clientEnv` ile bildirilen anahtarlar ve `NODE_ENV` build zamanında
202
- tek nesne olarak define edilir, yani listede olmayan bir anahtar okunduğunda
203
- çökme yerine `undefined` döner. İsimleri secret benzeri olan anahtarlar
204
- (`SECRET`, `API_KEY`, …) build'i düşürür; `PUBLIC` / `PUBLISHABLE` içerenler
205
- muaf. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
206
-
207
- ### Manifest anahtarları
208
-
209
- Yalnızca **gerçek entry'ler** manifest'e girer: dinamik import'lar da
210
- `entryPoint` taşır ve filtrelenmezse her island ayrı bir manifest anahtarı
211
- olurdu. Anahtar dosya adının kendisidir (`main.js`, `chart.js`), değer hash'li
212
- URL.
213
-
214
- Bu yüzden controller `entries: ["chart.js"]` yazarken hash'i bilmek zorunda
215
- değildir ([05-islands.md](./05-islands.md)).
216
-
217
- ### `metafile.json`
218
-
219
- esbuild metafile'ı `.jskelet/metafile.json` dosyasına yazılır; dev panelindeki
220
- chunk analizi giriş/çıkış kırılımını buradan okur. Yazma başarısız olursa build
221
- düşmez — analiz verisi en iyi çabadır. **Çalışma zamanı bu dosyaya bağımlı
222
- değildir.**
223
-
224
- ## Fontlar
225
-
226
- `next/font/google` yerine self-host font dosyaları.
227
-
228
- Dosyalar `public/fonts/` altında **sabit isimlerle** durur (hash yok), çünkü
229
- `@font-face` içindeki `url()` yolları elle yazılıyor; hash'lemek her build'de
230
- stylesheet'i de değiştirmek zorunda bırakırdı.
231
-
232
- Dosya yoksa **bir kez** Google Fonts'tan indirilir ve **commit edilmesi
233
- beklenir**: build'in ağa bağımlı olması CI'da kırılgan. İndirme başarısız olursa
234
- uyarı basılır ve sayfa sistem font yığınına düşer — build durmaz.
235
-
236
- Yalnızca latin subset'i (`U+0000-00FF`) indirilir: diğerleri çoğu site için ölü
237
- ağırlık ve `unicode-range` olmadan hepsini indirmek font boyutunu katlar.
238
-
239
- Kullanımı stylesheet'te elle yazılır:
240
-
241
- ```css
242
- @font-face {
243
- font-family: "Inter";
244
- font-style: normal;
245
- font-weight: 400;
246
- font-display: swap;
247
- src: url("/fonts/inter-400.woff2") format("woff2");
248
- }
249
- ```
250
-
251
- `.woff2` uzantısı ve `/fonts/` öneki varsayılan `static` kurallarında olduğu için
252
- bu dosyalara otomatik olarak `immutable` cache yazılır.
253
-
254
- ## İkon sprite
255
-
256
- **Yalnızca kaynakta kullanılan** ikonlar için bir `<symbol>` seti üretir. Tüm
257
- seti göndermek 1500+ ikon, yani birkaç megabayt; kullanım taraması sprite'ı
258
- tipik olarak 10-30 sembolde tutuyor. Çıktı hash'li `sprite.svg` olarak
259
- `public/assets/` altına yazılır ve precompress kapsamına girer.
260
-
261
- Kaynak **XOR** seçilir — ikisi birleştirilmez:
262
-
263
- 1. `icons.dir` (varsayılan `icons/`) **dizin olarak varsa** yalnızca oradaki
264
- düz SVG'ler. Boş dizin Phosphor'a düşmez; fallback için dizini silin.
265
- 2. Aksi hâlde `@phosphor-icons/core` (uygulamanın `node_modules`'ünden). Kurulu
266
- değilse adım sessizce atlanır.
267
-
268
- Yerel dosya adları:
269
-
270
- | Dosya | Sprite anahtarı |
271
- | --- | --- |
272
- | `icons/house.svg` | `house:regular` |
273
- | `icons/house-regular.svg` | `house:regular` |
274
- | `icons/arrow-right-bold.svg` | `arrow-right:bold` |
275
-
276
- - Sembol id'si: `<kebab-ad>-<weight>`, örn. `arrow-right-bold`.
277
- - `viewBox` kaynak SVG'den `<symbol>`'e taşınır; yoksa `0 0 256 256`
278
- (Phosphor ve `icon()` ile uyum için önerilen kutu).
279
- - Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib`,
280
- `features`, `shared`; `icons.scan` ile değiştirilebilir. Taranan uzantılar:
281
- `.ejs`, `.jsk`, `.js`, `.mjs`, `.ts`, `.mts`.
282
- - Ağırlıklar: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. Tanınmayan
283
- bir ağırlık `regular` sayılır.
284
-
285
- ### Tarama neyi bulur
286
-
287
- | Kaynaktaki biçim | Bulunur mu |
288
- | --- | --- |
289
- | `icon({ name: "ArrowRight", weight: "bold" })` | ✓ ad + ağırlık |
290
- | `icon({ name: cond ? "A" : "B" })` | ✓ her iki sabit ad |
291
- | `data-icon="flag:fill"` ya da `"data-icon": "flag:fill"` | ✓ |
292
- | `icon: "XLogo"` / `iconName: "XLogo"` (yapılandırma listelerinde) | ✓ ad; ağırlıklar dolaylı çağrılardan toplananlar |
293
- | `icon({ name: item.icon })` | ✗ ad statik görünmez |
294
-
295
- Son satır için iki güvenlik ağı var: ad taşıyan yapılandırma alanları
296
- (`icon: "XLogo"`) ayrıca aranır, ve development'ta `icon()` sprite'taki
297
- sembolleri okuyup eksik olan için tek seferlik uyarı basar:
298
-
299
- ```
300
- [icon] missing from sprite: x-logo-regular — write the name as a literal or add
301
- it to the build/tasks/icons.mjs scan.
302
- ```
303
-
304
- Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
305
- dizini ekleyin, ya da adı bir yapılandırma alanında `icon: "XLogo"` biçiminde
306
- tutun.
307
-
308
- Kaynakta bulunamayan adlar build sonunda özet olarak uyarılır:
309
- `N icons missing → …`
310
-
311
- ## Görsel optimizasyonu
312
-
313
- `next/image` optimizer'ının build zamanı karşılığı. `public/` altındaki elle
314
- konmuş png/jpg dosyaları için birkaç genişlikte webp üretir ve
315
- `.jskelet/images.json` manifest'ine yazar. `image()` bu manifest'e bakıp
316
- `srcset` + intrinsic `width`/`height` ekler; çağıran taraf hiçbir şey
317
- değiştirmez ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
318
-
319
- - Çıktılar hash'li olarak `public/assets/img/` altına düşer, yani `immutable`
320
- cache ve precompress kapsamına girerler.
321
- - **Kaynak dosyalar olduğu yerde kalır:** manifest'te olmayan bir görsel her
322
- zaman orijinaliyle servis edilir.
323
- - `assets` ve `fonts` dizinleri her zaman atlanır; ek dizinler `images.skip` ile.
324
- - Genişlikler kaynaktan büyük olanlar elenerek kullanılır ve kaynağın kendi
325
- genişliği (en fazla 1920) her zaman listeye girer. Retina ekranlarda bile
326
- 1920'nin üstü israf.
327
- - Varyant hash'i **kaynak + genişlikten** türetilir: aynı içerik her build'de
328
- aynı dosya adını verir, `immutable` cache bayatlamaz.
329
- - Manifest'e kodlayıcı imzası yazılır (`webp-q78-e4`). Kalite ayarı değişince
330
- imza da değişir ve tüm görseller yeniden kodlanır; aksi hâlde eski ayarla
331
- üretilmiş çıktılar sessizce kalırdı.
332
- - Kaynak değişmediyse ve çıktılar hâlâ yerindeyse yeniden kodlanmaz. Büyük bir
333
- `public/` dizininde bu, build süresini dakikalardan saniyelere indirir.
334
- - Bozuk/okunamayan tek bir görsel build'i düşürmez: uyarı basılır ve manifest'te
335
- yer almadığı için orijinal dosya servis edilmeye devam eder.
336
- - Manifest'te artık geçmeyen eski çıktılar silinir.
337
-
338
- Bu adım `sharp` gerektirir. Kurulu değilse adım sessizce atlanır ve `image()`
339
- orijinal dosyaya döner. Watch turunda hiç çalışmaz.
340
-
341
- ## Runtime uzak görsel proxy
342
-
343
- `images.remote.allowHosts` verilirse `createApp` `/_jskelet/image` ucunu
344
- mount eder. CMS / CDN kapakları build'e girmediği için `image()` bu host'lardaki
345
- URL'leri `?url=&w=` biçiminde yeniden yazar; uç sharp ile webp üretir ve
346
- `.jskelet/image-cache/` altına yazar. Upstream fetch redirect'leri elle takip
347
- edilir: her hop allowlist + private IP / DNS kontrolünden geçer (açık redirect
348
- SSRF kapalı). Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
349
-
350
- ## Precompress
351
-
352
- Build çıktısı varlıkların brotli (kalite 11) ve gzip (seviye 9) kopyalarını
353
- üretir: `app.<hash>.css.br`, `app.<hash>.css.gz`, …
354
-
355
- - Yalnızca `public/assets/` kapsanır: oradaki dosyalar hash'li ve `immutable`,
356
- yani içerikleri hiç değişmiyor ve her istekte yeniden sıkıştırmak boşa CPU.
357
- Build'de bir kez kalite 11 ile sıkıştırmak hem sunucu yükünü sıfırlar hem de
358
- çalışma anında göze alınamayacak bir oran verir (istek anındaki kalite 5'e
359
- karşı).
360
- - `public/` altındaki elle konmuş dosyalar küçük ve seyrek istendiği için
361
- çalışma anındaki sıkıştırmaya bırakılır.
362
- - Sıkıştırılan uzantılar: `.css`, `.js`, `.mjs`, `.svg`, `.json`, `.xml`,
363
- `.txt`, `.map`. Zaten sıkışık formatlar (woff2, png, jpg, webp) atlanır.
364
- - 1 KB altındaki dosyalar atlanır: kazanç başlık maliyetini karşılamıyor.
365
- - Önceki turdan kalan `.br`/`.gz` kopyalar önce silinir, bayatlamasın.
366
- - Watch modunda çalışmaz: her değişiklikte kalite-11 brotli yavaş.
367
-
368
- Bu dosyaları `staticPrecompressed` middleware'i servis eder; kopya yoksa istek
369
- `express.static`e devredilir ([02-mimari.md](./02-mimari.md)).
370
-
371
- ## Opsiyonel peer bağımlılıkları
372
-
373
- | Paket | Gerekli olduğu adım | Yoksa ne olur |
374
- | --- | --- | --- |
375
- | `postcss` | CSS | CSS adımı **hata verir** (zorunlu import) |
376
- | `@tailwindcss/postcss` | CSS | CSS adımı **hata verir** |
377
- | `tailwindcss` | CSS (peer) | Tailwind direktifleri çözülemez |
378
- | `lightningcss` | CSS minifikasyonu | Tailwind çıktısı kullanılır, birkaç kB daha büyük |
379
- | `sharp` | Görsel optimizasyonu | Adım atlanır; `image()` orijinali kullanır |
380
- | `@phosphor-icons/core` | İkon sprite (yerel `icons/` yoksa) | Adım atlanır; `icon()` boş `<use>` üretir |
381
-
382
- CSS kullanmayacaksanız `paths.styles` dosyasını hiç oluşturmayın: adım uyarıyla
383
- atlanır ve postcss'e ihtiyaç kalmaz.
384
-
385
- Paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün kendisinden
386
- değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa kaynak
387
- dosyaları kendi dizininde çalışır ve düz bir `import "postcss"` framework'ün
388
- ağacına bakar — uygulamanınkine değil. Bu yüzden çözümleme uygulama kökünden
389
- başlatılır.
390
-
391
- ## `.gitignore` önerisi
392
-
393
- ```
394
- node_modules/
395
- .jskelet/
396
- public/assets/
397
- .env
398
- ```
399
-
400
- `public/fonts/` **commit edilmelidir** (build'in ağa bağımlı olmaması için),
401
- `public/assets/` edilmemelidir (her build'de yeniden üretilir).
402
-
403
- ## `jskelet start` ve eksik build
404
-
405
- `jskelet start` önce `.jskelet/manifest.json` dosyasına bakar; yoksa build'i
406
- kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op;
407
- amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
408
- karşılaşmaması.
409
-
410
- ## Teşhis: sık görülen durumlar
411
-
412
- - **Stil hiç yok.** Build çalışmamış (`hasAsset('app.css')` false) ya da
413
- `paths.styles` dosyası mevcut değil. Build çıktısındaki `CSS` satırını
414
- kontrol edin.
415
- - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
416
- dizinde yazılmışlar.
417
- - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
418
- uyarısına bakın.
419
- - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
420
- da build atlanmış) veya bir build hatası var.
421
- - **Dev'de sayfa aniden stilsiz kaldı.** Manifest ile diskteki dosya
422
- ayrışmıştır; `jskelet dev`i yeniden başlatmak yeterli.
423
-
424
- ## Sırada ne var
425
-
426
- - Watch akışı ve CSS hot-swap: [09-dev-araclari.md](./09-dev-araclari.md)
427
- - Prod build + start ve Docker: [10-dagitim.md](./10-dagitim.md)
428
- - `entries` ve island bundle'ının kullanımı: [05-islands.md](./05-islands.md)
1
+ # 08 — Build
2
+
3
+ Bu belge `jskelet build`in yaptığı her işi ve sırasını anlatır: font kopyalama,
4
+ ikon sprite üretimi, Tailwind CSS derlemesi, esbuild ile island bundle'ı, görsel
5
+ optimizasyonu, manifest yazımı ve önceden sıkıştırma. Ayrıca hash'li varlıkların
6
+ `asset()`/`hasAsset()` ile şablonlara nasıl ulaştığı, Tailwind'in `@source`
7
+ direktiflerinin neden zorunlu olduğu ve opsiyonel peer bağımlılıklarının
8
+ davranışı burada. Çıktının çalışma anında nasıl servis edildiği
9
+ [02-mimari.md](./02-mimari.md)'de, build'i tetikleyen watch akışı
10
+ [09-dev-araclari.md](./09-dev-araclari.md)'de.
11
+
12
+ ## Hat ve sırası
13
+
14
+ ```
15
+ 0. Templates .jsk → .jskelet/templates/*.mjs (her zaman; dosya yoksa no-op)
16
+ 1. Fonts config.fonts varsa
17
+ 2. Icon sprite config.icons !== false ise
18
+ 3. CSS styles giriş dosyası varsa
19
+ 4. Client JS client/entries/ varsa
20
+ 5. Images config.images !== false, watch değil ve sharp kurulu ise
21
+ 6. Manifest .jskelet/manifest.json
22
+ 7. Precompress watch değilse
23
+ ```
24
+
25
+ Şablon derlemesi asset taramasından **önce** biter; istek yolunda parse yoktur.
26
+ Tailwind `@source` ve ikon taraması kaynak `.jsk` dosyalarını okur (üretilmiş
27
+ `.mjs` değil).
28
+
29
+ Sıra rastgele değil:
30
+
31
+ - **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
32
+ ama manifest anahtarı verir.
33
+ - **Precompress en sonda:** sıkıştırılacak her şey üretilmiş olmalı.
34
+ - **Görseller watch turunda hiç çalışmaz:** `sharp` ile yeniden kodlama pahalı.
35
+
36
+ Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir proje
37
+ font adımını hiç görmez; bu, "framework her projeye kendi varsayımlarını
38
+ dayatmaz" ilkesinin build tarafındaki karşılığı.
39
+
40
+ Terminal çıktısı hizalı adım satırları ve sonunda bir `output` bloğu verir: her
41
+ varlığın ham ve brotli boyutu, büyükten küçüğe.
42
+
43
+ ## Manifest ve hash'li varlıklar
44
+
45
+ Build çıktısı `public/assets/` altına **içerik hash'li** adlarla yazılır ve
46
+ mantıksal ad → public URL eşlemesi `.jskelet/manifest.json` dosyasına konur:
47
+
48
+ ```json
49
+ {
50
+ "app.css": "/assets/app.4f2a1b9c07.css",
51
+ "sprite.svg": "/assets/sprite.dc973997bd.svg",
52
+ "main.js": "/assets/js/main.9E1AB2C3.js",
53
+ "inter-400.woff2": "/fonts/inter-400.woff2"
54
+ }
55
+ ```
56
+
57
+ Hash sha256'nın ilk 10 hex karakteridir: çakışma için fazlasıyla yeterli ve
58
+ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
59
+ `Cache-Control: public, max-age=31536000, immutable` yazılabilir.
60
+
61
+ ### `asset(name)` ve `hasAsset(name)`
62
+
63
+ Şablonlara otomatik geçer; sunucu kodunda `import { asset, hasAsset } from "jskelet"`.
64
+
65
+ ```ejs
66
+ <% if (hasAsset('app.css')) { %>
67
+ <link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
68
+ <% } %>
69
+ <% styles.forEach(function (sheet) { %>
70
+ <% if (hasAsset(sheet)) { %>
71
+ <link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
72
+ <% } %>
73
+ <% }); %>
74
+ ```
75
+
76
+ - `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
77
+ - `hasAsset(name)` manifest'te olup olmadığını söyler.
78
+
79
+ Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
80
+ stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
81
+ yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
82
+ basılır: ``[assets] no manifest — run `jskelet build`.``
83
+
84
+ Manifest **dev'de her istekte** yeniden okunur (watch build hash'leri
85
+ değiştirir), prod'da bir kez.
86
+
87
+ ### Watch modunda manifest tutarlılığı
88
+
89
+ Watch turunda yeniden derlenen varlık yeni bir hash'e yazılıp eskisi silinir. Bu
90
+ yüzden manifest de güncellenmek zorunda (`patchManifest`): aksi hâlde HTML
91
+ silinmiş dosyayı isteyip 404 alır ve sayfa dev oturumunun kalanında stilsiz ya
92
+ da JS'siz kalır. CSS ve client görevlerinin ikisi de her turda kendi anahtarını
93
+ yamalar; diğer anahtarlar korunur. CSS tarafı watch'ta `syncCssManifest` ile
94
+ tüm `.css` anahtarlarını günceller (silinen sayfa sheet'leri de düşer).
95
+
96
+ ## CSS — Tailwind v4
97
+
98
+ Giriş dosyası `paths.styles` (varsayılan `styles/globals.css`). Dosya yoksa adım
99
+ uyarıyla atlanır.
100
+
101
+ Boru hattı: PostCSS + `@tailwindcss/postcss` → (varsa) lightningcss ile
102
+ minifikasyon → `writeAsset("app.css", …)`.
103
+
104
+ - **PostCSS boru hattı bir kez kurulur:** Tailwind'in kendi önbelleği plugin
105
+ örneğinde yaşıyor; her derlemede yeniden oluşturmak watch turlarını belirgin
106
+ şekilde yavaşlatıyor.
107
+ - **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
108
+ yalnızca birkaç kB daha büyük olur.
109
+ - Global çıktı `app.css`'tir ve layout onu her sayfada render-blocking olarak
110
+ yükler. Ayrı bir "critical CSS" üretilmemesinin ölçüm gerekçesi
111
+ [02-mimari.md](./02-mimari.md)'de.
112
+
113
+ ### Sayfa stylesheet'leri (`styles/pages/`)
114
+
115
+ Island `entries` ile aynı sözleşme. `styles/pages/*.css` altındaki her dosya
116
+ ayrı bir hash'li varlıktır (`home.css` → `/assets/home.<hash>.css`). Controller
117
+ yalnızca istediği sayfada yükler:
118
+
119
+ ```js
120
+ return {
121
+ view: "pages/home",
122
+ styles: ["home.css"],
123
+ };
124
+ ```
125
+
126
+ Dizin, `paths.styles` dosyasının yanındaki `pages/` klasörüdür (`styles` taşınırsa
127
+ pages de yanında kalır). Dizin yoksa veya boşsa adım yalnızca global sheet üretir.
128
+
129
+ Sayfa CSS'i sayfaya özel kurallar içindir. Tailwind utility'leri global sheet'te
130
+ kalmalı — dosyada tam `@import "tailwindcss"` utility çıktısını tekrarlar.
131
+
132
+ Layout `app.css`ten sonra `styles` dizisindeki her sheet için
133
+ `<link data-jskelet-css="…">` basar; `hasAsset` false ise etiket yok.
134
+
135
+ ### `@source` direktifleri zorunludur
136
+
137
+ Tailwind v4'ün sınıf taraması `globals.css` içindeki `@source` direktiflerine
138
+ bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar, bu
139
+ yüzden şablonlarda geçen varyantlar (`data-[active=false]:…` gibi) **sessizce
140
+ düşer**.
141
+
142
+ ```css
143
+ @import "tailwindcss" source(none);
144
+
145
+ @source "../views";
146
+ @source "../client";
147
+ @source "../routes";
148
+
149
+ .wrapper {
150
+ max-width: 48rem;
151
+ margin-inline: auto;
152
+ padding-inline: 1rem;
153
+ padding-block: 2rem;
154
+ }
155
+ ```
156
+
157
+ `source(none)` otomatik tespiti kapatır ve taramayı tamamen açık hâle getirir.
158
+ **Yeni bir üst dizin eklediğinizde `@source` satırını da ekleyin** — sınıfların
159
+ "bazen çalışmaması"nın en yaygın sebebi budur.
160
+
161
+ ### CSS watch kapsamı
162
+
163
+ Watch modunda üç hedef izlenir: stylesheet'in bulunduğu dizin, `views` ve
164
+ `client`. Şablon ve island dosyaları da izlenir çünkü Tailwind sınıfları oradan
165
+ geliyor; yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild
166
+ etmezdi. Değişiklikler 120 ms birleştirilir.
167
+
168
+ ## Client JS — esbuild
169
+
170
+ `client/entries/*.{js,ts,mts}` içindeki her kaynak dosya bir entry'dir (`.tsx`
171
+ yok). Manifest anahtarı her zaman `*.js` olur (`main.ts` → `main.js`). Aynı stem
172
+ için birden fazla uzantı build hatasıdır. Dizin yoksa ya da boşsa adım atlanır.
173
+
174
+ esbuild ayarları:
175
+
176
+ | Ayar | Değer | Sebebi |
177
+ | --- | --- | --- |
178
+ | `bundle`, `splitting` | `true` | Ortak modüller paylaşılan chunk'a çıkar |
179
+ | `format` | `esm` | `type="module"` script'ler |
180
+ | `target` | `chrome111`, `edge111`, `firefox111`, `safari16.4` | ESM + dinamik import + `IntersectionObserver` island modelinin alt sınırı; daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor |
181
+ | `minify` | `true` | — |
182
+ | `sourcemap` | yalnızca `NODE_ENV=development` | Prod'da `.map` dosyaları `public/assets` altında yayınlanmaz |
183
+ | `entryNames` | `[name].[hash]` | `immutable` cache |
184
+ | `chunkNames` | `chunks/[name].[hash]` | — |
185
+ | `legalComments` | `none` | — |
186
+
187
+ Çıktı `public/assets/js/` altına düşer ve her turda önce temizlenir.
188
+ `browserslist` okunmaz; hedef listesi kod içinde sabittir.
189
+
190
+ ### `@/` alias'ı
191
+
192
+ esbuild tarafında `@/` proje köküne çözülür ve uzantı tamamlama yapılır
193
+ (`.js`, `.mjs`, `.ts`, `.mts`, `.json`, `/index.js`, `/index.ts`). Node
194
+ `alias-hooks.mjs` sunucuda yalnızca `.js` / `.mjs` / `.json` çözer; paylaşılan
195
+ `@/lib` dosyaları bu yüzden `.js` kalmalıdır. Client-only `.ts` import'ları
196
+ esbuild hattında çalışır.
197
+
198
+ ### `clientEnv` gömülmesi
199
+
200
+ Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de `process.env`
201
+ okur. `config.clientEnv` ile bildirilen anahtarlar ve `NODE_ENV` build zamanında
202
+ tek nesne olarak define edilir, yani listede olmayan bir anahtar okunduğunda
203
+ çökme yerine `undefined` döner. İsimleri secret benzeri olan anahtarlar
204
+ (`SECRET`, `API_KEY`, …) build'i düşürür; `PUBLIC` / `PUBLISHABLE` içerenler
205
+ muaf. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
206
+
207
+ ### Manifest anahtarları
208
+
209
+ Yalnızca **gerçek entry'ler** manifest'e girer: dinamik import'lar da
210
+ `entryPoint` taşır ve filtrelenmezse her island ayrı bir manifest anahtarı
211
+ olurdu. Anahtar dosya adının kendisidir (`main.js`, `chart.js`), değer hash'li
212
+ URL.
213
+
214
+ Bu yüzden controller `entries: ["chart.js"]` yazarken hash'i bilmek zorunda
215
+ değildir ([05-islands.md](./05-islands.md)).
216
+
217
+ ### `metafile.json`
218
+
219
+ esbuild metafile'ı `.jskelet/metafile.json` dosyasına yazılır; dev panelindeki
220
+ chunk analizi giriş/çıkış kırılımını buradan okur. Yazma başarısız olursa build
221
+ düşmez — analiz verisi en iyi çabadır. **Çalışma zamanı bu dosyaya bağımlı
222
+ değildir.**
223
+
224
+ ## Fontlar
225
+
226
+ `next/font/google` yerine self-host font dosyaları.
227
+
228
+ Dosyalar `public/fonts/` altında **sabit isimlerle** durur (hash yok), çünkü
229
+ `@font-face` içindeki `url()` yolları elle yazılıyor; hash'lemek her build'de
230
+ stylesheet'i de değiştirmek zorunda bırakırdı.
231
+
232
+ Dosya yoksa **bir kez** Google Fonts'tan indirilir ve **commit edilmesi
233
+ beklenir**: build'in ağa bağımlı olması CI'da kırılgan. İndirme başarısız olursa
234
+ uyarı basılır ve sayfa sistem font yığınına düşer — build durmaz.
235
+
236
+ Yalnızca latin subset'i (`U+0000-00FF`) indirilir: diğerleri çoğu site için ölü
237
+ ağırlık ve `unicode-range` olmadan hepsini indirmek font boyutunu katlar.
238
+
239
+ Kullanımı stylesheet'te elle yazılır:
240
+
241
+ ```css
242
+ @font-face {
243
+ font-family: "Inter";
244
+ font-style: normal;
245
+ font-weight: 400;
246
+ font-display: swap;
247
+ src: url("/fonts/inter-400.woff2") format("woff2");
248
+ }
249
+ ```
250
+
251
+ `.woff2` uzantısı ve `/fonts/` öneki varsayılan `static` kurallarında olduğu için
252
+ bu dosyalara otomatik olarak `immutable` cache yazılır.
253
+
254
+ ## İkon sprite
255
+
256
+ **Yalnızca kaynakta kullanılan** ikonlar için bir `<symbol>` seti üretir. Tüm
257
+ seti göndermek 1500+ ikon, yani birkaç megabayt; kullanım taraması sprite'ı
258
+ tipik olarak 10-30 sembolde tutuyor. Çıktı hash'li `sprite.svg` olarak
259
+ `public/assets/` altına yazılır ve precompress kapsamına girer.
260
+
261
+ Kaynak **XOR** seçilir — ikisi birleştirilmez:
262
+
263
+ 1. `icons.dir` (varsayılan `icons/`) **dizin olarak varsa** yalnızca oradaki
264
+ düz SVG'ler. Boş dizin Phosphor'a düşmez; fallback için dizini silin.
265
+ 2. Aksi hâlde `@phosphor-icons/core` (uygulamanın `node_modules`'ünden). Kurulu
266
+ değilse adım sessizce atlanır.
267
+
268
+ Yerel dosya adları:
269
+
270
+ | Dosya | Sprite anahtarı |
271
+ | --- | --- |
272
+ | `icons/house.svg` | `house:regular` |
273
+ | `icons/house-regular.svg` | `house:regular` |
274
+ | `icons/arrow-right-bold.svg` | `arrow-right:bold` |
275
+
276
+ - Sembol id'si: `<kebab-ad>-<weight>`, örn. `arrow-right-bold`.
277
+ - `viewBox` kaynak SVG'den `<symbol>`'e taşınır; yoksa `0 0 256 256`
278
+ (Phosphor ve `icon()` ile uyum için önerilen kutu).
279
+ - Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib`,
280
+ `features`, `shared`; `icons.scan` ile değiştirilebilir. Taranan uzantılar:
281
+ `.ejs`, `.jsk`, `.js`, `.mjs`, `.ts`, `.mts`.
282
+ - Ağırlıklar: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. Tanınmayan
283
+ bir ağırlık `regular` sayılır.
284
+
285
+ ### Tarama neyi bulur
286
+
287
+ | Kaynaktaki biçim | Bulunur mu |
288
+ | --- | --- |
289
+ | `icon({ name: "ArrowRight", weight: "bold" })` | ✓ ad + ağırlık |
290
+ | `icon({ name: cond ? "A" : "B" })` | ✓ her iki sabit ad |
291
+ | `data-icon="flag:fill"` ya da `"data-icon": "flag:fill"` | ✓ |
292
+ | `icon: "XLogo"` / `iconName: "XLogo"` (yapılandırma listelerinde) | ✓ ad; ağırlıklar dolaylı çağrılardan toplananlar |
293
+ | `icon({ name: item.icon })` | ✗ ad statik görünmez |
294
+
295
+ Son satır için iki güvenlik ağı var: ad taşıyan yapılandırma alanları
296
+ (`icon: "XLogo"`) ayrıca aranır, ve development'ta `icon()` sprite'taki
297
+ sembolleri okuyup eksik olan için tek seferlik uyarı basar:
298
+
299
+ ```
300
+ [icon] missing from sprite: x-logo-regular — write the name as a literal or add
301
+ it to the build/tasks/icons.mjs scan.
302
+ ```
303
+
304
+ Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
305
+ dizini ekleyin, ya da adı bir yapılandırma alanında `icon: "XLogo"` biçiminde
306
+ tutun.
307
+
308
+ Kaynakta bulunamayan adlar build sonunda özet olarak uyarılır:
309
+ `N icons missing → …`
310
+
311
+ ## Görsel optimizasyonu
312
+
313
+ `next/image` optimizer'ının build zamanı karşılığı. `public/` altındaki elle
314
+ konmuş png/jpg dosyaları için birkaç genişlikte webp üretir ve
315
+ `.jskelet/images.json` manifest'ine yazar. `image()` bu manifest'e bakıp
316
+ `srcset` + intrinsic `width`/`height` ekler; çağıran taraf hiçbir şey
317
+ değiştirmez ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
318
+
319
+ - Çıktılar hash'li olarak `public/assets/img/` altına düşer, yani `immutable`
320
+ cache ve precompress kapsamına girerler.
321
+ - **Kaynak dosyalar olduğu yerde kalır:** manifest'te olmayan bir görsel her
322
+ zaman orijinaliyle servis edilir.
323
+ - `assets` ve `fonts` dizinleri her zaman atlanır; ek dizinler `images.skip` ile.
324
+ - Genişlikler kaynaktan büyük olanlar elenerek kullanılır ve kaynağın kendi
325
+ genişliği (en fazla 1920) her zaman listeye girer. Retina ekranlarda bile
326
+ 1920'nin üstü israf.
327
+ - Varyant hash'i **kaynak + genişlikten** türetilir: aynı içerik her build'de
328
+ aynı dosya adını verir, `immutable` cache bayatlamaz.
329
+ - Manifest'e kodlayıcı imzası yazılır (`webp-q78-e4`). Kalite ayarı değişince
330
+ imza da değişir ve tüm görseller yeniden kodlanır; aksi hâlde eski ayarla
331
+ üretilmiş çıktılar sessizce kalırdı.
332
+ - Kaynak değişmediyse ve çıktılar hâlâ yerindeyse yeniden kodlanmaz. Büyük bir
333
+ `public/` dizininde bu, build süresini dakikalardan saniyelere indirir.
334
+ - Bozuk/okunamayan tek bir görsel build'i düşürmez: uyarı basılır ve manifest'te
335
+ yer almadığı için orijinal dosya servis edilmeye devam eder.
336
+ - Manifest'te artık geçmeyen eski çıktılar silinir.
337
+
338
+ Bu adım `sharp` gerektirir. Kurulu değilse adım sessizce atlanır ve `image()`
339
+ orijinal dosyaya döner. Watch turunda hiç çalışmaz.
340
+
341
+ ## Runtime uzak görsel proxy
342
+
343
+ `images.remote.allowHosts` verilirse `createApp` `/_jskelet/image` ucunu
344
+ mount eder. CMS / CDN kapakları build'e girmediği için `image()` bu host'lardaki
345
+ URL'leri `?url=&w=` biçiminde yeniden yazar; uç sharp ile webp üretir ve
346
+ `.jskelet/image-cache/` altına yazar. Dizin 256 MB'yi geçince en eski dosya
347
+ düşer. Upstream fetch redirect'leri elle takip
348
+ edilir: her hop allowlist + private IP / DNS kontrolünden geçer (açık redirect
349
+ SSRF kapalı). Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
350
+
351
+ ## Precompress
352
+
353
+ Build çıktısı varlıkların brotli (kalite 11) ve gzip (seviye 9) kopyalarını
354
+ üretir: `app.<hash>.css.br`, `app.<hash>.css.gz`, …
355
+
356
+ - Yalnızca `public/assets/` kapsanır: oradaki dosyalar hash'li ve `immutable`,
357
+ yani içerikleri hiç değişmiyor ve her istekte yeniden sıkıştırmak boşa CPU.
358
+ Build'de bir kez kalite 11 ile sıkıştırmak hem sunucu yükünü sıfırlar hem de
359
+ çalışma anında göze alınamayacak bir oran verir (istek anındaki kalite 5'e
360
+ karşı).
361
+ - `public/` altındaki elle konmuş dosyalar küçük ve seyrek istendiği için
362
+ çalışma anındaki sıkıştırmaya bırakılır.
363
+ - Sıkıştırılan uzantılar: `.css`, `.js`, `.mjs`, `.svg`, `.json`, `.xml`,
364
+ `.txt`, `.map`. Zaten sıkışık formatlar (woff2, png, jpg, webp) atlanır.
365
+ - 1 KB altındaki dosyalar atlanır: kazanç başlık maliyetini karşılamıyor.
366
+ - Önceki turdan kalan `.br`/`.gz` kopyalar önce silinir, bayatlamasın.
367
+ - Watch modunda çalışmaz: her değişiklikte kalite-11 brotli yavaş.
368
+
369
+ Bu dosyaları `staticPrecompressed` middleware'i servis eder; kopya yoksa istek
370
+ `express.static`e devredilir ([02-mimari.md](./02-mimari.md)).
371
+
372
+ ## Opsiyonel peer bağımlılıkları
373
+
374
+ | Paket | Gerekli olduğu adım | Yoksa ne olur |
375
+ | --- | --- | --- |
376
+ | `postcss` | CSS | CSS adımı **hata verir** (zorunlu import) |
377
+ | `@tailwindcss/postcss` | CSS | CSS adımı **hata verir** |
378
+ | `tailwindcss` | CSS (peer) | Tailwind direktifleri çözülemez |
379
+ | `lightningcss` | CSS minifikasyonu | Tailwind çıktısı kullanılır, birkaç kB daha büyük |
380
+ | `sharp` | Görsel optimizasyonu | Adım atlanır; `image()` orijinali kullanır |
381
+ | `@phosphor-icons/core` | İkon sprite (yerel `icons/` yoksa) | Adım atlanır; `icon()` boş `<use>` üretir |
382
+
383
+ CSS kullanmayacaksanız `paths.styles` dosyasını hiç oluşturmayın: adım uyarıyla
384
+ atlanır ve postcss'e ihtiyaç kalmaz.
385
+
386
+ Paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün kendisinden
387
+ değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa kaynak
388
+ dosyaları kendi dizininde çalışır ve düz bir `import "postcss"` framework'ün
389
+ ağacına bakar — uygulamanınkine değil. Bu yüzden çözümleme uygulama kökünden
390
+ başlatılır.
391
+
392
+ ## `.gitignore` önerisi
393
+
394
+ ```
395
+ node_modules/
396
+ .jskelet/
397
+ public/assets/
398
+ .env
399
+ ```
400
+
401
+ `public/fonts/` **commit edilmelidir** (build'in ağa bağımlı olmaması için),
402
+ `public/assets/` edilmemelidir (her build'de yeniden üretilir).
403
+
404
+ ## `jskelet start` ve eksik build
405
+
406
+ `jskelet start` önce `.jskelet/manifest.json` dosyasına bakar; yoksa build'i
407
+ kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op;
408
+ amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
409
+ karşılaşmaması.
410
+
411
+ ## Teşhis: sık görülen durumlar
412
+
413
+ - **Stil hiç yok.** Build çalışmamış (`hasAsset('app.css')` false) ya da
414
+ `paths.styles` dosyası mevcut değil. Build çıktısındaki `CSS` satırını
415
+ kontrol edin.
416
+ - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
417
+ dizinde yazılmışlar.
418
+ - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
419
+ uyarısına bakın.
420
+ - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
421
+ da build atlanmış) veya bir build hatası var.
422
+ - **Dev'de sayfa aniden stilsiz kaldı.** Manifest ile diskteki dosya
423
+ ayrışmıştır; `jskelet dev`i yeniden başlatmak yeterli.
424
+
425
+ ## Sırada ne var
426
+
427
+ - Watch akışı ve CSS hot-swap: [09-dev-araclari.md](./09-dev-araclari.md)
428
+ - Prod build + start ve Docker: [10-dagitim.md](./10-dagitim.md)
429
+ - `entries` ve island bundle'ının kullanımı: [05-islands.md](./05-islands.md)