jskelet 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +287 -287
  7. package/docs/03-routing.md +480 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1209 -1202
  11. package/docs/08-build.md +366 -366
  12. package/docs/09-dev-araclari.md +335 -335
  13. package/docs/10-dagitim.md +329 -329
  14. package/docs/12-panel-ve-oturum.md +384 -384
  15. package/docs/README.md +105 -105
  16. package/docs/en/01-getting-started.md +292 -292
  17. package/docs/en/02-architecture.md +305 -305
  18. package/docs/en/03-routing.md +497 -497
  19. package/docs/en/04-rendering.md +504 -504
  20. package/docs/en/05-islands.md +492 -492
  21. package/docs/en/06-caching.md +1239 -1232
  22. package/docs/en/07-configuration.md +986 -986
  23. package/docs/en/08-build.md +383 -383
  24. package/docs/en/09-dev-tools.md +342 -342
  25. package/docs/en/10-deployment.md +332 -332
  26. package/docs/en/11-migration.md +359 -359
  27. package/docs/en/12-dashboards-and-sessions.md +392 -392
  28. package/docs/en/README.md +112 -112
  29. package/package.json +102 -102
  30. package/src/build/ensure-build.mjs +15 -15
  31. package/src/build/paths.mjs +143 -143
  32. package/src/build/resolve-peer.mjs +36 -36
  33. package/src/build/tasks/client.mjs +268 -268
  34. package/src/build/tasks/css.mjs +124 -124
  35. package/src/build/tasks/fonts.mjs +146 -146
  36. package/src/build/tasks/icons.mjs +224 -224
  37. package/src/build/tasks/images.mjs +244 -244
  38. package/src/build/tasks/precompress.mjs +78 -78
  39. package/src/client/cache-panel/i18n.js +670 -0
  40. package/src/client/cache-panel/login.html +74 -71
  41. package/src/client/cache-panel/panel.css +756 -740
  42. package/src/client/cache-panel/panel.html +308 -307
  43. package/src/client/cache-panel/panel.js +915 -808
  44. package/src/client/devtools/report.html +185 -185
  45. package/src/client/devtools/report.js +725 -725
  46. package/src/client/dom.js +95 -95
  47. package/src/client/form.js +192 -192
  48. package/src/client/index.js +35 -35
  49. package/src/client/registry.js +297 -297
  50. package/src/client/safe-image.js +91 -91
  51. package/src/client/store.js +36 -36
  52. package/src/client/swap.js +188 -188
  53. package/src/config/pattern.js +107 -107
  54. package/src/http/control-flow.js +71 -71
  55. package/src/http/cookies.js +257 -257
  56. package/src/http/request-cache.js +46 -46
  57. package/src/http/request-context.js +162 -162
  58. package/src/index.js +83 -83
  59. package/src/init.mjs +221 -221
  60. package/src/runtime/alias-hooks.mjs +119 -119
  61. package/src/runtime/register.mjs +4 -4
  62. package/src/server/assets.js +147 -147
  63. package/src/server/cache-deps.js +42 -42
  64. package/src/server/cache-panel.js +759 -738
  65. package/src/server/cloudflare.js +607 -595
  66. package/src/server/create-app.js +291 -291
  67. package/src/server/data-cache.js +462 -462
  68. package/src/server/dev/report.js +369 -369
  69. package/src/server/dev/socket.js +170 -170
  70. package/src/server/dev/version-check.mjs +139 -139
  71. package/src/server/html-cache.js +817 -817
  72. package/src/server/metadata.js +102 -102
  73. package/src/server/middleware/compression.js +205 -205
  74. package/src/server/middleware/csrf.js +134 -134
  75. package/src/server/middleware/dev-gate.js +62 -62
  76. package/src/server/middleware/headers.js +37 -37
  77. package/src/server/middleware/redirects.js +32 -32
  78. package/src/server/middleware/static-precompressed.js +100 -100
  79. package/src/server/middleware/upstream-proxy.js +141 -141
  80. package/src/server/prewarm.js +601 -601
  81. package/src/server/redis.js +569 -569
  82. package/src/server/router.js +128 -128
  83. package/src/server/status-page.js +164 -164
  84. package/src/server/upstream-limiter.js +376 -376
  85. package/src/server/upstream-tracking.js +166 -166
  86. package/src/start.mjs +7 -7
  87. package/src/templates/layout.ejs +44 -44
  88. package/src/version.mjs +31 -31
  89. package/src/views/components/loader.js +85 -85
  90. package/src/views/helpers/html.js +102 -102
  91. package/src/views/helpers/tags.js +245 -245
package/docs/08-build.md CHANGED
@@ -1,366 +1,366 @@
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
- 1. Fonts config.fonts varsa
16
- 2. Icon sprite config.icons !== false ise
17
- 3. CSS styles giriş dosyası varsa
18
- 4. Client JS client/entries/ varsa
19
- 5. Images config.images !== false, watch değil ve sharp kurulu ise
20
- 6. Manifest .jskelet/manifest.json
21
- 7. Precompress watch değilse
22
- ```
23
-
24
- Sıra rastgele değil:
25
-
26
- - **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
27
- ama manifest anahtarı verir.
28
- - **Precompress en sonda:** sıkıştırılacak her şey üretilmiş olmalı.
29
- - **Görseller watch turunda hiç çalışmaz:** `sharp` ile yeniden kodlama pahalı.
30
-
31
- Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir proje
32
- font adımını hiç görmez; bu, "framework her projeye kendi varsayımlarını
33
- dayatmaz" ilkesinin build tarafındaki karşılığı.
34
-
35
- Terminal çıktısı hizalı adım satırları ve sonunda bir `output` bloğu verir: her
36
- varlığın ham ve brotli boyutu, büyükten küçüğe.
37
-
38
- ## Manifest ve hash'li varlıklar
39
-
40
- Build çıktısı `public/assets/` altına **içerik hash'li** adlarla yazılır ve
41
- mantıksal ad → public URL eşlemesi `.jskelet/manifest.json` dosyasına konur:
42
-
43
- ```json
44
- {
45
- "app.css": "/assets/app.4f2a1b9c07.css",
46
- "sprite.svg": "/assets/sprite.dc973997bd.svg",
47
- "main.js": "/assets/js/main.9E1AB2C3.js",
48
- "inter-400.woff2": "/fonts/inter-400.woff2"
49
- }
50
- ```
51
-
52
- Hash sha256'nın ilk 10 hex karakteridir: çakışma için fazlasıyla yeterli ve
53
- dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
54
- `Cache-Control: public, max-age=31536000, immutable` yazılabilir.
55
-
56
- ### `asset(name)` ve `hasAsset(name)`
57
-
58
- Şablonlara otomatik geçer; sunucu kodunda `import { asset, hasAsset } from "jskelet"`.
59
-
60
- ```ejs
61
- <% if (hasAsset('app.css')) { %>
62
- <link rel="stylesheet" href="<%= asset('app.css') %>">
63
- <% } %>
64
- ```
65
-
66
- - `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
67
- - `hasAsset(name)` manifest'te olup olmadığını söyler.
68
-
69
- Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
70
- stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
71
- yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
72
- basılır: ``[assets] no manifest — run `jskelet build`.``
73
-
74
- Manifest **dev'de her istekte** yeniden okunur (watch build hash'leri
75
- değiştirir), prod'da bir kez.
76
-
77
- ### Watch modunda manifest tutarlılığı
78
-
79
- Watch turunda yeniden derlenen varlık yeni bir hash'e yazılıp eskisi silinir. Bu
80
- yüzden manifest de güncellenmek zorunda (`patchManifest`): aksi hâlde HTML
81
- silinmiş dosyayı isteyip 404 alır ve sayfa dev oturumunun kalanında stilsiz ya
82
- da JS'siz kalır. CSS ve client görevlerinin ikisi de her turda kendi anahtarını
83
- yamalar; diğer anahtarlar korunur.
84
-
85
- ## CSS — Tailwind v4
86
-
87
- Giriş dosyası `paths.styles` (varsayılan `styles/globals.css`). Dosya yoksa adım
88
- uyarıyla atlanır.
89
-
90
- Boru hattı: PostCSS + `@tailwindcss/postcss` → (varsa) lightningcss ile
91
- minifikasyon → `writeAsset("app.css", …)`.
92
-
93
- - **PostCSS boru hattı bir kez kurulur:** Tailwind'in kendi önbelleği plugin
94
- örneğinde yaşıyor; her derlemede yeniden oluşturmak watch turlarını belirgin
95
- şekilde yavaşlatıyor.
96
- - **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
97
- yalnızca birkaç kB daha büyük olur.
98
- - Çıktı tek bir dosyadır ve layout onu render-blocking olarak yükler. Ayrı bir
99
- "critical CSS" üretilmemesinin ölçüm gerekçesi
100
- [02-mimari.md](./02-mimari.md)'de.
101
-
102
- ### `@source` direktifleri zorunludur
103
-
104
- Tailwind v4'ün sınıf taraması `globals.css` içindeki `@source` direktiflerine
105
- bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar, bu
106
- yüzden şablonlarda geçen varyantlar (`data-[active=false]:…` gibi) **sessizce
107
- düşer**.
108
-
109
- ```css
110
- @import "tailwindcss" source(none);
111
-
112
- @source "../views";
113
- @source "../client";
114
- @source "../routes";
115
-
116
- .wrapper {
117
- max-width: 48rem;
118
- margin-inline: auto;
119
- padding-inline: 1rem;
120
- padding-block: 2rem;
121
- }
122
- ```
123
-
124
- `source(none)` otomatik tespiti kapatır ve taramayı tamamen açık hâle getirir.
125
- **Yeni bir üst dizin eklediğinizde `@source` satırını da ekleyin** — sınıfların
126
- "bazen çalışmaması"nın en yaygın sebebi budur.
127
-
128
- ### CSS watch kapsamı
129
-
130
- Watch modunda üç hedef izlenir: stylesheet'in bulunduğu dizin, `views` ve
131
- `client`. Şablon ve island dosyaları da izlenir çünkü Tailwind sınıfları oradan
132
- geliyor; yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild
133
- etmezdi. Değişiklikler 120 ms birleştirilir.
134
-
135
- ## Client JS — esbuild
136
-
137
- `client/entries/*.js` içindeki her `.js` dosyası bir entry'dir. Dizin yoksa ya da
138
- boşsa adım atlanır.
139
-
140
- esbuild ayarları:
141
-
142
- | Ayar | Değer | Sebebi |
143
- | --- | --- | --- |
144
- | `bundle`, `splitting` | `true` | Ortak modüller paylaşılan chunk'a çıkar |
145
- | `format` | `esm` | `type="module"` script'ler |
146
- | `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 |
147
- | `minify` | `true` | — |
148
- | `sourcemap` | `true` | Tarayıcıda teşhis |
149
- | `entryNames` | `[name].[hash]` | `immutable` cache |
150
- | `chunkNames` | `chunks/[name].[hash]` | — |
151
- | `legalComments` | `none` | — |
152
-
153
- Çıktı `public/assets/js/` altına düşer ve her turda önce temizlenir.
154
- `browserslist` okunmaz; hedef listesi kod içinde sabittir.
155
-
156
- ### `@/` alias'ı
157
-
158
- esbuild tarafında `@/` proje köküne çözülür ve uzantı tamamlama yapılır
159
- (`.js`, `.mjs`, `.json`, `/index.js`). Node tarafındaki `alias-hooks.mjs` ile
160
- aynı davranış, böylece `lib/` altındaki modüller hem sunucuda hem tarayıcıda
161
- aynı import stilini kullanabilir.
162
-
163
- ### `clientEnv` gömülmesi
164
-
165
- Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de `process.env`
166
- okur. `config.clientEnv` ile bildirilen anahtarlar ve `NODE_ENV` build zamanında
167
- tek nesne olarak define edilir, yani listede olmayan bir anahtar okunduğunda
168
- çökme yerine `undefined` döner. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
169
-
170
- ### Manifest anahtarları
171
-
172
- Yalnızca **gerçek entry'ler** manifest'e girer: dinamik import'lar da
173
- `entryPoint` taşır ve filtrelenmezse her island ayrı bir manifest anahtarı
174
- olurdu. Anahtar dosya adının kendisidir (`main.js`, `chart.js`), değer hash'li
175
- URL.
176
-
177
- Bu yüzden controller `entries: ["chart.js"]` yazarken hash'i bilmek zorunda
178
- değildir ([05-islands.md](./05-islands.md)).
179
-
180
- ### `metafile.json`
181
-
182
- esbuild metafile'ı `.jskelet/metafile.json` dosyasına yazılır; dev panelindeki
183
- chunk analizi giriş/çıkış kırılımını buradan okur. Yazma başarısız olursa build
184
- düşmez — analiz verisi en iyi çabadır. **Çalışma zamanı bu dosyaya bağımlı
185
- değildir.**
186
-
187
- ## Fontlar
188
-
189
- `next/font/google` yerine self-host font dosyaları.
190
-
191
- Dosyalar `public/fonts/` altında **sabit isimlerle** durur (hash yok), çünkü
192
- `@font-face` içindeki `url()` yolları elle yazılıyor; hash'lemek her build'de
193
- stylesheet'i de değiştirmek zorunda bırakırdı.
194
-
195
- Dosya yoksa **bir kez** Google Fonts'tan indirilir ve **commit edilmesi
196
- beklenir**: build'in ağa bağımlı olması CI'da kırılgan. İndirme başarısız olursa
197
- uyarı basılır ve sayfa sistem font yığınına düşer — build durmaz.
198
-
199
- Yalnızca latin subset'i (`U+0000-00FF`) indirilir: diğerleri çoğu site için ölü
200
- ağırlık ve `unicode-range` olmadan hepsini indirmek font boyutunu katlar.
201
-
202
- Kullanımı stylesheet'te elle yazılır:
203
-
204
- ```css
205
- @font-face {
206
- font-family: "Inter";
207
- font-style: normal;
208
- font-weight: 400;
209
- font-display: swap;
210
- src: url("/fonts/inter-400.woff2") format("woff2");
211
- }
212
- ```
213
-
214
- `.woff2` uzantısı ve `/fonts/` öneki varsayılan `static` kurallarında olduğu için
215
- bu dosyalara otomatik olarak `immutable` cache yazılır.
216
-
217
- ## İkon sprite
218
-
219
- `@phosphor-icons/core` içindeki tek tek SVG'lerden, **yalnızca kaynakta
220
- kullanılan** ikonlar için `<symbol>` seti üretir. Tüm seti göndermek 1500+ ikon,
221
- yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak 10-30 sembolde
222
- tutuyor.
223
-
224
- - Sembol id'si: `<kebab-ad>-<weight>`, örn. `arrow-right-bold`.
225
- - Paket **uygulamanın** `node_modules`'ünden çözülür (ikon seti uygulamanın
226
- devDependency'si); kurulu değilse adım sessizce atlanır.
227
- - Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib`;
228
- `icons.scan` ile değiştirilebilir. Taranan uzantılar: `.ejs`, `.js`, `.mjs`.
229
- - Ağırlıklar: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. Tanınmayan
230
- bir ağırlık `regular` sayılır.
231
-
232
- ### Tarama neyi bulur
233
-
234
- | Kaynaktaki biçim | Bulunur mu |
235
- | --- | --- |
236
- | `icon({ name: "ArrowRight", weight: "bold" })` | ✓ ad + ağırlık |
237
- | `icon({ name: cond ? "A" : "B" })` | ✓ her iki sabit ad |
238
- | `data-icon="flag:fill"` ya da `"data-icon": "flag:fill"` | ✓ |
239
- | `icon: "XLogo"` / `iconName: "XLogo"` (yapılandırma listelerinde) | ✓ ad; ağırlıklar dolaylı çağrılardan toplananlar |
240
- | `icon({ name: item.icon })` | ✗ ad statik görünmez |
241
-
242
- Son satır için iki güvenlik ağı var: ad taşıyan yapılandırma alanları
243
- (`icon: "XLogo"`) ayrıca aranır, ve development'ta `icon()` sprite'taki
244
- sembolleri okuyup eksik olan için tek seferlik uyarı basar:
245
-
246
- ```
247
- [icon] missing from sprite: x-logo-regular — write the name as a literal or add
248
- it to the build/tasks/icons.mjs scan.
249
- ```
250
-
251
- Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
252
- dizini ekleyin, ya da adı bir yapılandırma alanında `icon: "XLogo"` biçiminde
253
- tutun.
254
-
255
- Phosphor'da bulunamayan adlar build sonunda özet olarak uyarılır:
256
- `N icons missing → …`
257
-
258
- ## Görsel optimizasyonu
259
-
260
- `next/image` optimizer'ının build zamanı karşılığı. `public/` altındaki elle
261
- konmuş png/jpg dosyaları için birkaç genişlikte webp üretir ve
262
- `.jskelet/images.json` manifest'ine yazar. `image()` bu manifest'e bakıp
263
- `srcset` + intrinsic `width`/`height` ekler; çağıran taraf hiçbir şey
264
- değiştirmez ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
265
-
266
- - Çıktılar hash'li olarak `public/assets/img/` altına düşer, yani `immutable`
267
- cache ve precompress kapsamına girerler.
268
- - **Kaynak dosyalar olduğu yerde kalır:** manifest'te olmayan bir görsel her
269
- zaman orijinaliyle servis edilir.
270
- - `assets` ve `fonts` dizinleri her zaman atlanır; ek dizinler `images.skip` ile.
271
- - Genişlikler kaynaktan büyük olanlar elenerek kullanılır ve kaynağın kendi
272
- genişliği (en fazla 1920) her zaman listeye girer. Retina ekranlarda bile
273
- 1920'nin üstü israf.
274
- - Varyant hash'i **kaynak + genişlikten** türetilir: aynı içerik her build'de
275
- aynı dosya adını verir, `immutable` cache bayatlamaz.
276
- - Manifest'e kodlayıcı imzası yazılır (`webp-q78-e4`). Kalite ayarı değişince
277
- imza da değişir ve tüm görseller yeniden kodlanır; aksi hâlde eski ayarla
278
- üretilmiş çıktılar sessizce kalırdı.
279
- - Kaynak değişmediyse ve çıktılar hâlâ yerindeyse yeniden kodlanmaz. Büyük bir
280
- `public/` dizininde bu, build süresini dakikalardan saniyelere indirir.
281
- - Bozuk/okunamayan tek bir görsel build'i düşürmez: uyarı basılır ve manifest'te
282
- yer almadığı için orijinal dosya servis edilmeye devam eder.
283
- - Manifest'te artık geçmeyen eski çıktılar silinir.
284
-
285
- Bu adım `sharp` gerektirir. Kurulu değilse adım sessizce atlanır ve `image()`
286
- orijinal dosyaya döner. Watch turunda hiç çalışmaz.
287
-
288
- ## Precompress
289
-
290
- Build çıktısı varlıkların brotli (kalite 11) ve gzip (seviye 9) kopyalarını
291
- üretir: `app.<hash>.css.br`, `app.<hash>.css.gz`, …
292
-
293
- - Yalnızca `public/assets/` kapsanır: oradaki dosyalar hash'li ve `immutable`,
294
- yani içerikleri hiç değişmiyor ve her istekte yeniden sıkıştırmak boşa CPU.
295
- Build'de bir kez kalite 11 ile sıkıştırmak hem sunucu yükünü sıfırlar hem de
296
- çalışma anında göze alınamayacak bir oran verir (istek anındaki kalite 5'e
297
- karşı).
298
- - `public/` altındaki elle konmuş dosyalar küçük ve seyrek istendiği için
299
- çalışma anındaki sıkıştırmaya bırakılır.
300
- - Sıkıştırılan uzantılar: `.css`, `.js`, `.mjs`, `.svg`, `.json`, `.xml`,
301
- `.txt`, `.map`. Zaten sıkışık formatlar (woff2, png, jpg, webp) atlanır.
302
- - 1 KB altındaki dosyalar atlanır: kazanç başlık maliyetini karşılamıyor.
303
- - Önceki turdan kalan `.br`/`.gz` kopyalar önce silinir, bayatlamasın.
304
- - Watch modunda çalışmaz: her değişiklikte kalite-11 brotli yavaş.
305
-
306
- Bu dosyaları `staticPrecompressed` middleware'i servis eder; kopya yoksa istek
307
- `express.static`e devredilir ([02-mimari.md](./02-mimari.md)).
308
-
309
- ## Opsiyonel peer bağımlılıkları
310
-
311
- | Paket | Gerekli olduğu adım | Yoksa ne olur |
312
- | --- | --- | --- |
313
- | `postcss` | CSS | CSS adımı **hata verir** (zorunlu import) |
314
- | `@tailwindcss/postcss` | CSS | CSS adımı **hata verir** |
315
- | `tailwindcss` | CSS (peer) | Tailwind direktifleri çözülemez |
316
- | `lightningcss` | CSS minifikasyonu | Tailwind çıktısı kullanılır, birkaç kB daha büyük |
317
- | `sharp` | Görsel optimizasyonu | Adım atlanır; `image()` orijinali kullanır |
318
- | `@phosphor-icons/core` | İkon sprite | Adım atlanır; `icon()` boş `<use>` üretir |
319
-
320
- CSS kullanmayacaksanız `paths.styles` dosyasını hiç oluşturmayın: adım uyarıyla
321
- atlanır ve postcss'e ihtiyaç kalmaz.
322
-
323
- Paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün kendisinden
324
- değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa kaynak
325
- dosyaları kendi dizininde çalışır ve düz bir `import "postcss"` framework'ün
326
- ağacına bakar — uygulamanınkine değil. Bu yüzden çözümleme uygulama kökünden
327
- başlatılır.
328
-
329
- ## `.gitignore` önerisi
330
-
331
- ```
332
- node_modules/
333
- .jskelet/
334
- public/assets/
335
- .env
336
- ```
337
-
338
- `public/fonts/` **commit edilmelidir** (build'in ağa bağımlı olmaması için),
339
- `public/assets/` edilmemelidir (her build'de yeniden üretilir).
340
-
341
- ## `jskelet start` ve eksik build
342
-
343
- `jskelet start` önce `.jskelet/manifest.json` dosyasına bakar; yoksa build'i
344
- kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op;
345
- amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
346
- karşılaşmaması.
347
-
348
- ## Teşhis: sık görülen durumlar
349
-
350
- - **Stil hiç yok.** Build çalışmamış (`hasAsset('app.css')` false) ya da
351
- `paths.styles` dosyası mevcut değil. Build çıktısındaki `CSS` satırını
352
- kontrol edin.
353
- - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
354
- dizinde yazılmışlar.
355
- - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
356
- uyarısına bakın.
357
- - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
358
- da build atlanmış) veya bir build hatası var.
359
- - **Dev'de sayfa aniden stilsiz kaldı.** Manifest ile diskteki dosya
360
- ayrışmıştır; `jskelet dev`i yeniden başlatmak yeterli.
361
-
362
- ## Sırada ne var
363
-
364
- - Watch akışı ve CSS hot-swap: [09-dev-araclari.md](./09-dev-araclari.md)
365
- - Prod build + start ve Docker: [10-dagitim.md](./10-dagitim.md)
366
- - `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
+ 1. Fonts config.fonts varsa
16
+ 2. Icon sprite config.icons !== false ise
17
+ 3. CSS styles giriş dosyası varsa
18
+ 4. Client JS client/entries/ varsa
19
+ 5. Images config.images !== false, watch değil ve sharp kurulu ise
20
+ 6. Manifest .jskelet/manifest.json
21
+ 7. Precompress watch değilse
22
+ ```
23
+
24
+ Sıra rastgele değil:
25
+
26
+ - **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
27
+ ama manifest anahtarı verir.
28
+ - **Precompress en sonda:** sıkıştırılacak her şey üretilmiş olmalı.
29
+ - **Görseller watch turunda hiç çalışmaz:** `sharp` ile yeniden kodlama pahalı.
30
+
31
+ Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir proje
32
+ font adımını hiç görmez; bu, "framework her projeye kendi varsayımlarını
33
+ dayatmaz" ilkesinin build tarafındaki karşılığı.
34
+
35
+ Terminal çıktısı hizalı adım satırları ve sonunda bir `output` bloğu verir: her
36
+ varlığın ham ve brotli boyutu, büyükten küçüğe.
37
+
38
+ ## Manifest ve hash'li varlıklar
39
+
40
+ Build çıktısı `public/assets/` altına **içerik hash'li** adlarla yazılır ve
41
+ mantıksal ad → public URL eşlemesi `.jskelet/manifest.json` dosyasına konur:
42
+
43
+ ```json
44
+ {
45
+ "app.css": "/assets/app.4f2a1b9c07.css",
46
+ "sprite.svg": "/assets/sprite.dc973997bd.svg",
47
+ "main.js": "/assets/js/main.9E1AB2C3.js",
48
+ "inter-400.woff2": "/fonts/inter-400.woff2"
49
+ }
50
+ ```
51
+
52
+ Hash sha256'nın ilk 10 hex karakteridir: çakışma için fazlasıyla yeterli ve
53
+ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
54
+ `Cache-Control: public, max-age=31536000, immutable` yazılabilir.
55
+
56
+ ### `asset(name)` ve `hasAsset(name)`
57
+
58
+ Şablonlara otomatik geçer; sunucu kodunda `import { asset, hasAsset } from "jskelet"`.
59
+
60
+ ```ejs
61
+ <% if (hasAsset('app.css')) { %>
62
+ <link rel="stylesheet" href="<%= asset('app.css') %>">
63
+ <% } %>
64
+ ```
65
+
66
+ - `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
67
+ - `hasAsset(name)` manifest'te olup olmadığını söyler.
68
+
69
+ Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
70
+ stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
71
+ yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
72
+ basılır: ``[assets] no manifest — run `jskelet build`.``
73
+
74
+ Manifest **dev'de her istekte** yeniden okunur (watch build hash'leri
75
+ değiştirir), prod'da bir kez.
76
+
77
+ ### Watch modunda manifest tutarlılığı
78
+
79
+ Watch turunda yeniden derlenen varlık yeni bir hash'e yazılıp eskisi silinir. Bu
80
+ yüzden manifest de güncellenmek zorunda (`patchManifest`): aksi hâlde HTML
81
+ silinmiş dosyayı isteyip 404 alır ve sayfa dev oturumunun kalanında stilsiz ya
82
+ da JS'siz kalır. CSS ve client görevlerinin ikisi de her turda kendi anahtarını
83
+ yamalar; diğer anahtarlar korunur.
84
+
85
+ ## CSS — Tailwind v4
86
+
87
+ Giriş dosyası `paths.styles` (varsayılan `styles/globals.css`). Dosya yoksa adım
88
+ uyarıyla atlanır.
89
+
90
+ Boru hattı: PostCSS + `@tailwindcss/postcss` → (varsa) lightningcss ile
91
+ minifikasyon → `writeAsset("app.css", …)`.
92
+
93
+ - **PostCSS boru hattı bir kez kurulur:** Tailwind'in kendi önbelleği plugin
94
+ örneğinde yaşıyor; her derlemede yeniden oluşturmak watch turlarını belirgin
95
+ şekilde yavaşlatıyor.
96
+ - **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
97
+ yalnızca birkaç kB daha büyük olur.
98
+ - Çıktı tek bir dosyadır ve layout onu render-blocking olarak yükler. Ayrı bir
99
+ "critical CSS" üretilmemesinin ölçüm gerekçesi
100
+ [02-mimari.md](./02-mimari.md)'de.
101
+
102
+ ### `@source` direktifleri zorunludur
103
+
104
+ Tailwind v4'ün sınıf taraması `globals.css` içindeki `@source` direktiflerine
105
+ bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar, bu
106
+ yüzden şablonlarda geçen varyantlar (`data-[active=false]:…` gibi) **sessizce
107
+ düşer**.
108
+
109
+ ```css
110
+ @import "tailwindcss" source(none);
111
+
112
+ @source "../views";
113
+ @source "../client";
114
+ @source "../routes";
115
+
116
+ .wrapper {
117
+ max-width: 48rem;
118
+ margin-inline: auto;
119
+ padding-inline: 1rem;
120
+ padding-block: 2rem;
121
+ }
122
+ ```
123
+
124
+ `source(none)` otomatik tespiti kapatır ve taramayı tamamen açık hâle getirir.
125
+ **Yeni bir üst dizin eklediğinizde `@source` satırını da ekleyin** — sınıfların
126
+ "bazen çalışmaması"nın en yaygın sebebi budur.
127
+
128
+ ### CSS watch kapsamı
129
+
130
+ Watch modunda üç hedef izlenir: stylesheet'in bulunduğu dizin, `views` ve
131
+ `client`. Şablon ve island dosyaları da izlenir çünkü Tailwind sınıfları oradan
132
+ geliyor; yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild
133
+ etmezdi. Değişiklikler 120 ms birleştirilir.
134
+
135
+ ## Client JS — esbuild
136
+
137
+ `client/entries/*.js` içindeki her `.js` dosyası bir entry'dir. Dizin yoksa ya da
138
+ boşsa adım atlanır.
139
+
140
+ esbuild ayarları:
141
+
142
+ | Ayar | Değer | Sebebi |
143
+ | --- | --- | --- |
144
+ | `bundle`, `splitting` | `true` | Ortak modüller paylaşılan chunk'a çıkar |
145
+ | `format` | `esm` | `type="module"` script'ler |
146
+ | `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 |
147
+ | `minify` | `true` | — |
148
+ | `sourcemap` | `true` | Tarayıcıda teşhis |
149
+ | `entryNames` | `[name].[hash]` | `immutable` cache |
150
+ | `chunkNames` | `chunks/[name].[hash]` | — |
151
+ | `legalComments` | `none` | — |
152
+
153
+ Çıktı `public/assets/js/` altına düşer ve her turda önce temizlenir.
154
+ `browserslist` okunmaz; hedef listesi kod içinde sabittir.
155
+
156
+ ### `@/` alias'ı
157
+
158
+ esbuild tarafında `@/` proje köküne çözülür ve uzantı tamamlama yapılır
159
+ (`.js`, `.mjs`, `.json`, `/index.js`). Node tarafındaki `alias-hooks.mjs` ile
160
+ aynı davranış, böylece `lib/` altındaki modüller hem sunucuda hem tarayıcıda
161
+ aynı import stilini kullanabilir.
162
+
163
+ ### `clientEnv` gömülmesi
164
+
165
+ Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de `process.env`
166
+ okur. `config.clientEnv` ile bildirilen anahtarlar ve `NODE_ENV` build zamanında
167
+ tek nesne olarak define edilir, yani listede olmayan bir anahtar okunduğunda
168
+ çökme yerine `undefined` döner. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
169
+
170
+ ### Manifest anahtarları
171
+
172
+ Yalnızca **gerçek entry'ler** manifest'e girer: dinamik import'lar da
173
+ `entryPoint` taşır ve filtrelenmezse her island ayrı bir manifest anahtarı
174
+ olurdu. Anahtar dosya adının kendisidir (`main.js`, `chart.js`), değer hash'li
175
+ URL.
176
+
177
+ Bu yüzden controller `entries: ["chart.js"]` yazarken hash'i bilmek zorunda
178
+ değildir ([05-islands.md](./05-islands.md)).
179
+
180
+ ### `metafile.json`
181
+
182
+ esbuild metafile'ı `.jskelet/metafile.json` dosyasına yazılır; dev panelindeki
183
+ chunk analizi giriş/çıkış kırılımını buradan okur. Yazma başarısız olursa build
184
+ düşmez — analiz verisi en iyi çabadır. **Çalışma zamanı bu dosyaya bağımlı
185
+ değildir.**
186
+
187
+ ## Fontlar
188
+
189
+ `next/font/google` yerine self-host font dosyaları.
190
+
191
+ Dosyalar `public/fonts/` altında **sabit isimlerle** durur (hash yok), çünkü
192
+ `@font-face` içindeki `url()` yolları elle yazılıyor; hash'lemek her build'de
193
+ stylesheet'i de değiştirmek zorunda bırakırdı.
194
+
195
+ Dosya yoksa **bir kez** Google Fonts'tan indirilir ve **commit edilmesi
196
+ beklenir**: build'in ağa bağımlı olması CI'da kırılgan. İndirme başarısız olursa
197
+ uyarı basılır ve sayfa sistem font yığınına düşer — build durmaz.
198
+
199
+ Yalnızca latin subset'i (`U+0000-00FF`) indirilir: diğerleri çoğu site için ölü
200
+ ağırlık ve `unicode-range` olmadan hepsini indirmek font boyutunu katlar.
201
+
202
+ Kullanımı stylesheet'te elle yazılır:
203
+
204
+ ```css
205
+ @font-face {
206
+ font-family: "Inter";
207
+ font-style: normal;
208
+ font-weight: 400;
209
+ font-display: swap;
210
+ src: url("/fonts/inter-400.woff2") format("woff2");
211
+ }
212
+ ```
213
+
214
+ `.woff2` uzantısı ve `/fonts/` öneki varsayılan `static` kurallarında olduğu için
215
+ bu dosyalara otomatik olarak `immutable` cache yazılır.
216
+
217
+ ## İkon sprite
218
+
219
+ `@phosphor-icons/core` içindeki tek tek SVG'lerden, **yalnızca kaynakta
220
+ kullanılan** ikonlar için `<symbol>` seti üretir. Tüm seti göndermek 1500+ ikon,
221
+ yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak 10-30 sembolde
222
+ tutuyor.
223
+
224
+ - Sembol id'si: `<kebab-ad>-<weight>`, örn. `arrow-right-bold`.
225
+ - Paket **uygulamanın** `node_modules`'ünden çözülür (ikon seti uygulamanın
226
+ devDependency'si); kurulu değilse adım sessizce atlanır.
227
+ - Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib`;
228
+ `icons.scan` ile değiştirilebilir. Taranan uzantılar: `.ejs`, `.js`, `.mjs`.
229
+ - Ağırlıklar: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. Tanınmayan
230
+ bir ağırlık `regular` sayılır.
231
+
232
+ ### Tarama neyi bulur
233
+
234
+ | Kaynaktaki biçim | Bulunur mu |
235
+ | --- | --- |
236
+ | `icon({ name: "ArrowRight", weight: "bold" })` | ✓ ad + ağırlık |
237
+ | `icon({ name: cond ? "A" : "B" })` | ✓ her iki sabit ad |
238
+ | `data-icon="flag:fill"` ya da `"data-icon": "flag:fill"` | ✓ |
239
+ | `icon: "XLogo"` / `iconName: "XLogo"` (yapılandırma listelerinde) | ✓ ad; ağırlıklar dolaylı çağrılardan toplananlar |
240
+ | `icon({ name: item.icon })` | ✗ ad statik görünmez |
241
+
242
+ Son satır için iki güvenlik ağı var: ad taşıyan yapılandırma alanları
243
+ (`icon: "XLogo"`) ayrıca aranır, ve development'ta `icon()` sprite'taki
244
+ sembolleri okuyup eksik olan için tek seferlik uyarı basar:
245
+
246
+ ```
247
+ [icon] missing from sprite: x-logo-regular — write the name as a literal or add
248
+ it to the build/tasks/icons.mjs scan.
249
+ ```
250
+
251
+ Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
252
+ dizini ekleyin, ya da adı bir yapılandırma alanında `icon: "XLogo"` biçiminde
253
+ tutun.
254
+
255
+ Phosphor'da bulunamayan adlar build sonunda özet olarak uyarılır:
256
+ `N icons missing → …`
257
+
258
+ ## Görsel optimizasyonu
259
+
260
+ `next/image` optimizer'ının build zamanı karşılığı. `public/` altındaki elle
261
+ konmuş png/jpg dosyaları için birkaç genişlikte webp üretir ve
262
+ `.jskelet/images.json` manifest'ine yazar. `image()` bu manifest'e bakıp
263
+ `srcset` + intrinsic `width`/`height` ekler; çağıran taraf hiçbir şey
264
+ değiştirmez ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
265
+
266
+ - Çıktılar hash'li olarak `public/assets/img/` altına düşer, yani `immutable`
267
+ cache ve precompress kapsamına girerler.
268
+ - **Kaynak dosyalar olduğu yerde kalır:** manifest'te olmayan bir görsel her
269
+ zaman orijinaliyle servis edilir.
270
+ - `assets` ve `fonts` dizinleri her zaman atlanır; ek dizinler `images.skip` ile.
271
+ - Genişlikler kaynaktan büyük olanlar elenerek kullanılır ve kaynağın kendi
272
+ genişliği (en fazla 1920) her zaman listeye girer. Retina ekranlarda bile
273
+ 1920'nin üstü israf.
274
+ - Varyant hash'i **kaynak + genişlikten** türetilir: aynı içerik her build'de
275
+ aynı dosya adını verir, `immutable` cache bayatlamaz.
276
+ - Manifest'e kodlayıcı imzası yazılır (`webp-q78-e4`). Kalite ayarı değişince
277
+ imza da değişir ve tüm görseller yeniden kodlanır; aksi hâlde eski ayarla
278
+ üretilmiş çıktılar sessizce kalırdı.
279
+ - Kaynak değişmediyse ve çıktılar hâlâ yerindeyse yeniden kodlanmaz. Büyük bir
280
+ `public/` dizininde bu, build süresini dakikalardan saniyelere indirir.
281
+ - Bozuk/okunamayan tek bir görsel build'i düşürmez: uyarı basılır ve manifest'te
282
+ yer almadığı için orijinal dosya servis edilmeye devam eder.
283
+ - Manifest'te artık geçmeyen eski çıktılar silinir.
284
+
285
+ Bu adım `sharp` gerektirir. Kurulu değilse adım sessizce atlanır ve `image()`
286
+ orijinal dosyaya döner. Watch turunda hiç çalışmaz.
287
+
288
+ ## Precompress
289
+
290
+ Build çıktısı varlıkların brotli (kalite 11) ve gzip (seviye 9) kopyalarını
291
+ üretir: `app.<hash>.css.br`, `app.<hash>.css.gz`, …
292
+
293
+ - Yalnızca `public/assets/` kapsanır: oradaki dosyalar hash'li ve `immutable`,
294
+ yani içerikleri hiç değişmiyor ve her istekte yeniden sıkıştırmak boşa CPU.
295
+ Build'de bir kez kalite 11 ile sıkıştırmak hem sunucu yükünü sıfırlar hem de
296
+ çalışma anında göze alınamayacak bir oran verir (istek anındaki kalite 5'e
297
+ karşı).
298
+ - `public/` altındaki elle konmuş dosyalar küçük ve seyrek istendiği için
299
+ çalışma anındaki sıkıştırmaya bırakılır.
300
+ - Sıkıştırılan uzantılar: `.css`, `.js`, `.mjs`, `.svg`, `.json`, `.xml`,
301
+ `.txt`, `.map`. Zaten sıkışık formatlar (woff2, png, jpg, webp) atlanır.
302
+ - 1 KB altındaki dosyalar atlanır: kazanç başlık maliyetini karşılamıyor.
303
+ - Önceki turdan kalan `.br`/`.gz` kopyalar önce silinir, bayatlamasın.
304
+ - Watch modunda çalışmaz: her değişiklikte kalite-11 brotli yavaş.
305
+
306
+ Bu dosyaları `staticPrecompressed` middleware'i servis eder; kopya yoksa istek
307
+ `express.static`e devredilir ([02-mimari.md](./02-mimari.md)).
308
+
309
+ ## Opsiyonel peer bağımlılıkları
310
+
311
+ | Paket | Gerekli olduğu adım | Yoksa ne olur |
312
+ | --- | --- | --- |
313
+ | `postcss` | CSS | CSS adımı **hata verir** (zorunlu import) |
314
+ | `@tailwindcss/postcss` | CSS | CSS adımı **hata verir** |
315
+ | `tailwindcss` | CSS (peer) | Tailwind direktifleri çözülemez |
316
+ | `lightningcss` | CSS minifikasyonu | Tailwind çıktısı kullanılır, birkaç kB daha büyük |
317
+ | `sharp` | Görsel optimizasyonu | Adım atlanır; `image()` orijinali kullanır |
318
+ | `@phosphor-icons/core` | İkon sprite | Adım atlanır; `icon()` boş `<use>` üretir |
319
+
320
+ CSS kullanmayacaksanız `paths.styles` dosyasını hiç oluşturmayın: adım uyarıyla
321
+ atlanır ve postcss'e ihtiyaç kalmaz.
322
+
323
+ Paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün kendisinden
324
+ değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa kaynak
325
+ dosyaları kendi dizininde çalışır ve düz bir `import "postcss"` framework'ün
326
+ ağacına bakar — uygulamanınkine değil. Bu yüzden çözümleme uygulama kökünden
327
+ başlatılır.
328
+
329
+ ## `.gitignore` önerisi
330
+
331
+ ```
332
+ node_modules/
333
+ .jskelet/
334
+ public/assets/
335
+ .env
336
+ ```
337
+
338
+ `public/fonts/` **commit edilmelidir** (build'in ağa bağımlı olmaması için),
339
+ `public/assets/` edilmemelidir (her build'de yeniden üretilir).
340
+
341
+ ## `jskelet start` ve eksik build
342
+
343
+ `jskelet start` önce `.jskelet/manifest.json` dosyasına bakar; yoksa build'i
344
+ kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op;
345
+ amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
346
+ karşılaşmaması.
347
+
348
+ ## Teşhis: sık görülen durumlar
349
+
350
+ - **Stil hiç yok.** Build çalışmamış (`hasAsset('app.css')` false) ya da
351
+ `paths.styles` dosyası mevcut değil. Build çıktısındaki `CSS` satırını
352
+ kontrol edin.
353
+ - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
354
+ dizinde yazılmışlar.
355
+ - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
356
+ uyarısına bakın.
357
+ - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
358
+ da build atlanmış) veya bir build hatası var.
359
+ - **Dev'de sayfa aniden stilsiz kaldı.** Manifest ile diskteki dosya
360
+ ayrışmıştır; `jskelet dev`i yeniden başlatmak yeterli.
361
+
362
+ ## Sırada ne var
363
+
364
+ - Watch akışı ve CSS hot-swap: [09-dev-araclari.md](./09-dev-araclari.md)
365
+ - Prod build + start ve Docker: [10-dagitim.md](./10-dagitim.md)
366
+ - `entries` ve island bundle'ının kullanımı: [05-islands.md](./05-islands.md)