jskelet 0.6.2 → 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
@@ -1,366 +1,366 @@
1
- /**
2
- * Express uygulamasını kurar ve dinlemeye başlar.
3
- *
4
- * Middleware **sırası** bu dosyanın asıl değeri; her konumun bir sebebi var
5
- * ve yer değiştirmek sessiz bozulmalara yol açıyor:
6
- *
7
- * 1. rewrites(beforeFiles) — statik dosyalardan da önce çalışmalı, yoksa
8
- * `/assets/x.js` yolunu başka bir yere taşıyan kural işlemez.
9
- * 2. compression — static'ten önce; sonra gelirse statik dosyalar sıkışmaz.
10
- * 3. headers → devGate → redirects → trailingSlash — gate'in 404'ü
11
- * redirect'ten önce; trailingSlash config redirects'ten sonra, böylece
12
- * açık kurallar istenen yolu önce görür.
13
- * 3b. access log (açıksa) — tamamlanan yanıtların süresi; admin/prewarm
14
- * içeride elenir.
15
- * 3c. robots.txt — statikten önce, compression'ın içinde: kullanıcının
16
- * gövdesine framework Disallow bloğu eklenir. Sıkıştırılmış kopyanın
17
- * (Content-Encoding dolu) üzerine yazılmaz.
18
- * 4. staticPrecompressed → express.static — build'de üretilmiş `.br`/`.gz`
19
- * kopyalar varsa onlar servis edilir (kalite 11), yoksa istek altındaki
20
- * static'e düşer ve middleware anında sıkıştırır (kalite 5).
21
- * 4b. admin paneli (açıksa) — statikten sonra, route'lardan önce: kendi
22
- * gövde ayrıştırıcısını taşır ve uygulama yolunu gölgeleyemez.
23
- * 4c. image optimizer (images.remote) — uzak görselleri webp'ye çevirir;
24
- * body parser'dan önce, admin ile aynı katmanda.
25
- * 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
26
- * maliyeti ödenmesin.
27
- * 5b. csrf — body parser'lardan sonra: token form alanından okunuyor.
28
- * Rewrite'lardan önce, çünkü kontrol istemcinin gördüğü yola bakar.
29
- * 5c. auth handoff (açıksa) — CSRF'den *sonra*: mint POST origin koruması
30
- * görsün. Redeem GET güvenli metot; route render'ından önce kalır.
31
- * 6. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
32
- * 7. route'lar → 404 → hata yönetimi.
33
- */
34
- import path from "node:path";
35
- import process from "node:process";
36
- import express from "express";
37
- import { compression } from "./middleware/compression.js";
38
- import { robotsTxtMiddleware } from "./middleware/robots-txt.js";
39
- import { headersMiddleware } from "./middleware/headers.js";
40
- import { csrf } from "./middleware/csrf.js";
41
- import { staticPrecompressed } from "./middleware/static-precompressed.js";
42
- import { devGate } from "./middleware/dev-gate.js";
43
- import { redirects } from "./middleware/redirects.js";
44
- import { trailingSlash } from "./middleware/trailing-slash.js";
45
- import { configRewrites } from "./middleware/upstream-proxy.js";
46
- import { getConfig, loadConfig } from "../config/index.js";
47
- import { IMMUTABLE_CACHE } from "../config/defaults.js";
48
- import { registerRoutes } from "./router.js";
49
- import { renderNotFound } from "./render.js";
50
- import { renderStatusPage, statusFromError } from "./status-page.js";
51
- import { isPrewarmRequest, notePrewarmError, startPrewarm } from "./prewarm.js";
52
- import { trackUpstreamFetch } from "./upstream-tracking.js";
53
- import { configureUpstreamLimiter } from "./upstream-limiter.js";
54
- import { connectRedis, disconnectRedis } from "./redis.js";
55
- import { configureLogs, flushLogs, closeLogs } from "./logs/pipeline.js";
56
- import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
57
- import { ensurePortFree } from "./port-guard.js";
58
-
59
- /**
60
- * @param {{ root?: string, configFile?: string }} [options]
61
- * @returns {Promise<import('express').Express>}
62
- */
63
- export async function createApp(options = {}) {
64
- // Config middleware'lerden önce okunur; yoksa uyarı basar, akış durmaz.
65
- await loadConfig(options);
66
- const config = getConfig();
67
-
68
- // Upstream hatalarının izlenmesi route'lardan önce kurulmalı: sarmalayıcı
69
- // yalnızca render bağlamı içindeki `fetch` çağrılarına bakar, ama bağlamın
70
- // ilk kurulduğu istek de kapsanmalı.
71
- if (config.trackUpstream) trackUpstreamFetch();
72
-
73
- // Hız freni sarmalayıcının içinden okunuyor; ayarı ona vermek yeterli.
74
- // `cache().upstream.rate` verilmedikçe hiçbir istek beklemez.
75
- configureUpstreamLimiter(config.upstream);
76
-
77
- // Önbelleğin ikinci kademesi route'lardan önce kurulmalı: ilk istek de
78
- // paylaşımlı kopyayı görebilsin. Bağlanamazsa uyarı basılır ve uygulama
79
- // bellek içi önbellekle çalışmaya devam eder — middleware sırasına
80
- // dokunmayan, tamamen opsiyonel bir adım.
81
- await connectRedis(config);
82
-
83
- // Kalıcı log sink'leri (dosya / S3). Credential eksikse uyarı + no-op;
84
- // site düşmez. Access middleware gerektiğinde biraz aşağıda mount edilir.
85
- const { accessLog } = await configureLogs(config);
86
-
87
- const app = express();
88
-
89
- app.disable("x-powered-by");
90
- app.use((req, res, next) => {
91
- res.setHeader("X-Powered-By", config.brand.poweredBy);
92
- next();
93
- });
94
-
95
- app.set("etag", "strong");
96
- // Ters proxy arkasında doğru protokol ve istemci IP'si için. Doğrudan
97
- // internete açık bir sunucuda kapatılmalı: açıkken istemci kendi
98
- // `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit log
99
- // yanlış IP görür.
100
- app.set("trust proxy", config.security.trustProxy);
101
-
102
- app.use(configRewrites("beforeFiles"));
103
- app.use(compression());
104
- app.use(headersMiddleware());
105
- app.use(devGate());
106
- app.use(redirects());
107
- app.use(trailingSlash());
108
-
109
- // Access log: headers/redirects sonrası, statikten önce — böylece
110
- // tamamlanan her yanıt (304 dahil) süre alır; admin/prewarm içeride elenir.
111
- if (accessLog) {
112
- const { accessLogMiddleware } = await import("./logs/access-middleware.js");
113
- app.use(accessLogMiddleware());
114
- }
115
-
116
- // Kullanıcının robots.txt'i statik dosya da olabilir, route da. İkisi de
117
- // bu sarmalayıcıdan geçer. compression'dan sonra durur: ek, sıkıştırılmış
118
- // baytların değil düz metnin sonuna yazılır.
119
- app.use(robotsTxtMiddleware());
120
-
121
- app.use(staticPrecompressed(config.dirs.public));
122
- app.use(
123
- express.static(config.dirs.public, {
124
- index: false,
125
- redirect: false,
126
- // Hash'siz dosyalar (favicon, robots eki, elle konmuş görseller) içerik
127
- // değişse de aynı adla kalıyor; bir yıllık cache onları güncellenemez
128
- // hâle getiriyordu. Hash'li çıktı aşağıda ayrıca immutable işaretlenir.
129
- maxAge: "1h",
130
- setHeaders(res, filePath) {
131
- if (filePath.includes(`${path.sep}assets${path.sep}`)) {
132
- res.setHeader("Cache-Control", IMMUTABLE_CACHE);
133
- }
134
- },
135
- }),
136
- );
137
-
138
- // Dev overlay yalnızca development'ta; dinamik import sayesinde prod
139
- // sürecine hiçbir şey yüklenmez.
140
- if (process.env.NODE_ENV === "development") {
141
- const { mountDevtools } = await import("./dev/devtools.js");
142
- mountDevtools(app);
143
- }
144
-
145
- // Yönetim paneli ortama bakmaz, config'e bakar: açıkça etkinleştirilmedikçe
146
- // modül hiç yüklenmez ve yol da yoktur. Kendi gövde ayrıştırıcısını
147
- // taşıdığı için aşağıdaki parser'lardan önce durabiliyor; route'lardan
148
- // önce olması gerekiyor ki uygulama aynı yolu gölgeleyemesin.
149
- if (config.admin.enabled) {
150
- const { mountAdmin } = await import("./admin/mount.js");
151
- mountAdmin(app);
152
- }
153
-
154
- // Uzak görsel proxy: allowHosts doluysa mount. Statikten sonra, body
155
- // parser'dan önce — görsel GET'lerinde gövde ayrıştırma maliyeti ödenmesin.
156
- if (config.images && config.images !== false && config.images.remote) {
157
- const { mountImageOptimizer } = await import("./image-optimizer.js");
158
- await mountImageOptimizer(app);
159
- }
160
-
161
- app.use(express.urlencoded({ extended: false, limit: "64kb" }));
162
- app.use(express.json({ limit: "256kb" }));
163
-
164
- app.use(csrf());
165
-
166
- const handoff = config.auth?.crossSubdomainHandoff;
167
- if (
168
- handoff === true ||
169
- (handoff &&
170
- typeof handoff === "object" &&
171
- /** @type {{ enabled?: boolean }} */ (handoff).enabled !== false)
172
- ) {
173
- const { mountAuthHandoff } = await import("./auth/handoff.js");
174
- mountAuthHandoff(app);
175
- }
176
-
177
- app.use(configRewrites("afterFiles"));
178
-
179
- await registerRoutes(app);
180
-
181
- app.use(async (req, res, next) => {
182
- try {
183
- // headersMiddleware `/assets/*` için immutable basmış olabilir; eksik bir
184
- // hash'li dosyanın 404'ü CDN'de bir yıl zehirlenmesin.
185
- res.status(404).setHeader("Cache-Control", "no-store");
186
- res.type("html").send(await renderNotFound());
187
- } catch (error) {
188
- next(error);
189
- }
190
- });
191
-
192
- // Hata yönetimi: notFound/redirect kontrol akışı burada da yakalanır,
193
- // çünkü bir controller dışında (ör. middleware içinde) fırlatılabilir.
194
- app.use(async (error, req, res, next) => {
195
- if (res.headersSent) {
196
- next(error);
197
- return;
198
- }
199
-
200
- if (isRedirectError(error)) {
201
- res.redirect(error.statusCode, error.location);
202
- return;
203
- }
204
-
205
- if (isNotFoundError(error)) {
206
- res.status(404).setHeader("Cache-Control", "no-store");
207
- res.type("html").send(await renderNotFound());
208
- return;
209
- }
210
-
211
- const status = statusFromError(error);
212
- // Isıtma turunun hataları tek tek loglanmaz; tur bitince özet olarak
213
- // basılır. Yüzlerce yolu tarayan bir tur, upstream bir an tıksırdığında
214
- // logu yığın izleriyle dolduruyordu.
215
- if (isPrewarmRequest(req)) notePrewarmError(status, error);
216
- else console.error(`[${status}] ${req.method} ${req.originalUrl}`, error);
217
-
218
- // Hata sayfası hiçbir katmanda saklanmamalı: geçici bir upstream arızası
219
- // CDN'de dakikalarca yaşayan bir 500 sayfasına dönüşmesin.
220
- res.setHeader("Cache-Control", "no-store");
221
- res
222
- .status(status)
223
- .type("html")
224
- .send(await renderStatusPage(status, { error }));
225
- });
226
-
227
- return app;
228
- }
229
-
230
- /**
231
- * Uygulamayı kurup dinlemeye başlar. CLI `jskelet start` bunu çağırır;
232
- * gömülü kullanımda `createApp()` tercih edilir.
233
- *
234
- * Port doluysa başlamaz. `murder: true` veya argv'de `--murder` varsa
235
- * dinleyen süreç öldürülüp bağlama denenir.
236
- *
237
- * @param {{ root?: string, configFile?: string, port?: number, host?: string, murder?: boolean }} [options]
238
- * @returns {Promise<import('http').Server>}
239
- */
240
- export async function startServer(options = {}) {
241
- const port = Number(options.port ?? process.env.PORT ?? 3000);
242
- const murder =
243
- options.murder === true || process.argv.includes("--murder");
244
-
245
- // createApp pahalı; dolu portta uygulama kurmadan önce net hata ver.
246
- await ensurePortFree(port, { murder });
247
-
248
- const app = await createApp(options);
249
-
250
- // Varsayılan `::`, `0.0.0.0` değil: ikisi de "tüm arayüzler" demek, ama
251
- // yalnızca IPv6 soketi çift yığın çalışır ve `localhost`un `::1`e çözüldüğü
252
- // durumu da kapsar. Tarayıcılar `localhost` için önce `::1` deniyor; sıradan
253
- // isteklerde IPv4'e düşüyorlar ama WebSocket el sıkışması bu geri düşüşü
254
- // yapmadan "failed" veriyordu. IPv6'sı olmayan bir makinede bağlama hata
255
- // verir; aşağıda IPv4'e dönülür.
256
- const host = options.host ?? process.env.HOST ?? null;
257
-
258
- // Tek bir istek hatası süreci düşürmesin; logla ve ayakta kal. Bir haber
259
- // sitesinde tek sayfanın hatası tüm siteyi indirmemeli.
260
- process.on("unhandledRejection", (reason) => {
261
- console.error("[unhandledRejection]", reason);
262
- });
263
- process.on("uncaughtException", (error) => {
264
- console.error("[uncaughtException]", error);
265
- });
266
-
267
- // Dev panelinin canlı kanalı: el sıkışma `upgrade` olayında geçtiği için
268
- // middleware zincirine değil, doğrudan sunucuya bağlanır. Modül `listen`den
269
- // önce yüklenir; dinleme başladıktan sonra beklenen bir `await` kalırsa ilk
270
- // upgrade isteği dinleyici yokken gelip reddedilebiliyor.
271
- const attachDevSocket =
272
- process.env.NODE_ENV === "development"
273
- ? (await import("./dev/devtools.js")).attachDevSocket
274
- : null;
275
-
276
- return new Promise((resolve, reject) => {
277
- /** @param {string} address */
278
- const listen = (address) => {
279
- const server = app.listen(port, address, () => {
280
- // Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
281
- // buradan anlar ve özet satırını ona göre basar.
282
- console.log(
283
- `jskelet → http://localhost:${port} (${process.env.NODE_ENV ?? "production"})`,
284
- );
285
- startPrewarm({ port });
286
- attachShutdown(server);
287
- resolve(server);
288
- });
289
-
290
- server.on("error", (error) => {
291
- // IPv6 desteklenmiyorsa yalnızca varsayılan adres için IPv4'e dönülür;
292
- // kullanıcı bir adres verdiyse sessizce başkasını dinlemek yanlış olur.
293
- if (!host && isAddressUnsupported(error)) {
294
- listen("0.0.0.0");
295
- return;
296
- }
297
- if (error.code === "EADDRINUSE") {
298
- const hint = murder
299
- ? `port ${port} is still in use after --murder`
300
- : `port ${port} is already in use. Pass --murder to kill it and start, or set PORT to another value.`;
301
- const wrapped = new Error(hint);
302
- /** @type {NodeJS.ErrnoException} */ (wrapped).code = "EADDRINUSE";
303
- reject(wrapped);
304
- return;
305
- }
306
- reject(error);
307
- });
308
-
309
- attachDevSocket?.(server);
310
- };
311
-
312
- listen(host ?? "::");
313
- });
314
- }
315
-
316
- /**
317
- * `SIGTERM`/`SIGINT` sonrası düzenli kapanış.
318
- *
319
- * Kapatılması gereken dış kaynaklar Redis ve log sink buffer'ları. Redis
320
- * `quit` uçuştaki komutların bitmesini bekliyor; sert `disconnect` yarıda
321
- * kalan bir `SET` bırakabiliyor. S3 sink kapanışta kalan batch'i PutObject
322
- * ile gönderir.
323
- *
324
- * Açık HTTP bağlantıları **beklenmez**. `close()` tek başına yalnızca yeni
325
- * bağlantıyı reddediyor; keep-alive bir istemci ya da dev panelinin
326
- * WebSocket'i sunucuyu süresiz ayakta tutuyor ve Ctrl+C yanıt vermiyormuş gibi
327
- * görünüyordu. Sinyal geldiğinde ters proxy zaten trafik göndermiyor.
328
- *
329
- * Yine de bir zamanlayıcı var: Redis kapanışı askıda kalırsa süreç `SIGKILL`
330
- * beklemek zorunda kalmasın.
331
- *
332
- * @param {import('http').Server} server
333
- */
334
- function attachShutdown(server) {
335
- let closing = false;
336
-
337
- const shutdown = () => {
338
- // İkinci sinyal beklemeyi kısa kessin: kullanıcı Ctrl+C'ye tekrar bastıysa
339
- // gerçekten çıkmak istiyor.
340
- if (closing) process.exit(0);
341
- closing = true;
342
-
343
- const timer = setTimeout(() => process.exit(0), 3000);
344
- timer.unref();
345
-
346
- server.close();
347
- server.closeAllConnections?.();
348
-
349
- void Promise.all([disconnectRedis(), flushLogs().then(() => closeLogs())])
350
- .finally(() => {
351
- clearTimeout(timer);
352
- process.exit(0);
353
- });
354
- };
355
-
356
- process.once("SIGTERM", shutdown);
357
- process.once("SIGINT", shutdown);
358
- }
359
-
360
- /**
361
- * @param {NodeJS.ErrnoException} error
362
- * @returns {boolean} adres ailesi bu makinede kullanılamıyor mu
363
- */
364
- function isAddressUnsupported(error) {
365
- return error.code === "EAFNOSUPPORT" || error.code === "EADDRNOTAVAIL";
366
- }
1
+ /**
2
+ * Express uygulamasını kurar ve dinlemeye başlar.
3
+ *
4
+ * Middleware **sırası** bu dosyanın asıl değeri; her konumun bir sebebi var
5
+ * ve yer değiştirmek sessiz bozulmalara yol açıyor:
6
+ *
7
+ * 1. rewrites(beforeFiles) — statik dosyalardan da önce çalışmalı, yoksa
8
+ * `/assets/x.js` yolunu başka bir yere taşıyan kural işlemez.
9
+ * 2. compression — static'ten önce; sonra gelirse statik dosyalar sıkışmaz.
10
+ * 3. headers → devGate → redirects → trailingSlash — gate'in 404'ü
11
+ * redirect'ten önce; trailingSlash config redirects'ten sonra, böylece
12
+ * açık kurallar istenen yolu önce görür.
13
+ * 3b. access log (açıksa) — tamamlanan yanıtların süresi; admin/prewarm
14
+ * içeride elenir.
15
+ * 3c. robots.txt — statikten önce, compression'ın içinde: kullanıcının
16
+ * gövdesine framework Disallow bloğu eklenir. Sıkıştırılmış kopyanın
17
+ * (Content-Encoding dolu) üzerine yazılmaz.
18
+ * 4. staticPrecompressed → express.static — build'de üretilmiş `.br`/`.gz`
19
+ * kopyalar varsa onlar servis edilir (kalite 11), yoksa istek altındaki
20
+ * static'e düşer ve middleware anında sıkıştırır (kalite 5).
21
+ * 4b. admin paneli (açıksa) — statikten sonra, route'lardan önce: kendi
22
+ * gövde ayrıştırıcısını taşır ve uygulama yolunu gölgeleyemez.
23
+ * 4c. image optimizer (images.remote) — uzak görselleri webp'ye çevirir;
24
+ * body parser'dan önce, admin ile aynı katmanda.
25
+ * 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
26
+ * maliyeti ödenmesin.
27
+ * 5b. csrf — body parser'lardan sonra: token form alanından okunuyor.
28
+ * Rewrite'lardan önce, çünkü kontrol istemcinin gördüğü yola bakar.
29
+ * 5c. auth handoff (açıksa) — CSRF'den *sonra*: mint POST origin koruması
30
+ * görsün. Redeem GET güvenli metot; route render'ından önce kalır.
31
+ * 6. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
32
+ * 7. route'lar → 404 → hata yönetimi.
33
+ */
34
+ import path from "node:path";
35
+ import process from "node:process";
36
+ import express from "express";
37
+ import { compression } from "./middleware/compression.js";
38
+ import { robotsTxtMiddleware } from "./middleware/robots-txt.js";
39
+ import { headersMiddleware } from "./middleware/headers.js";
40
+ import { csrf } from "./middleware/csrf.js";
41
+ import { staticPrecompressed } from "./middleware/static-precompressed.js";
42
+ import { devGate } from "./middleware/dev-gate.js";
43
+ import { redirects } from "./middleware/redirects.js";
44
+ import { trailingSlash } from "./middleware/trailing-slash.js";
45
+ import { configRewrites } from "./middleware/upstream-proxy.js";
46
+ import { getConfig, loadConfig } from "../config/index.js";
47
+ import { IMMUTABLE_CACHE } from "../config/defaults.js";
48
+ import { registerRoutes } from "./router.js";
49
+ import { renderNotFound } from "./render.js";
50
+ import { renderStatusPage, statusFromError } from "./status-page.js";
51
+ import { isPrewarmRequest, notePrewarmError, startPrewarm } from "./prewarm.js";
52
+ import { trackUpstreamFetch } from "./upstream-tracking.js";
53
+ import { configureUpstreamLimiter } from "./upstream-limiter.js";
54
+ import { connectRedis, disconnectRedis } from "./redis.js";
55
+ import { configureLogs, flushLogs, closeLogs } from "./logs/pipeline.js";
56
+ import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
57
+ import { ensurePortFree } from "./port-guard.js";
58
+
59
+ /**
60
+ * @param {{ root?: string, configFile?: string }} [options]
61
+ * @returns {Promise<import('express').Express>}
62
+ */
63
+ export async function createApp(options = {}) {
64
+ // Config middleware'lerden önce okunur; yoksa uyarı basar, akış durmaz.
65
+ await loadConfig(options);
66
+ const config = getConfig();
67
+
68
+ // Upstream hatalarının izlenmesi route'lardan önce kurulmalı: sarmalayıcı
69
+ // yalnızca render bağlamı içindeki `fetch` çağrılarına bakar, ama bağlamın
70
+ // ilk kurulduğu istek de kapsanmalı.
71
+ if (config.trackUpstream) trackUpstreamFetch();
72
+
73
+ // Hız freni sarmalayıcının içinden okunuyor; ayarı ona vermek yeterli.
74
+ // `cache().upstream.rate` verilmedikçe hiçbir istek beklemez.
75
+ configureUpstreamLimiter(config.upstream);
76
+
77
+ // Önbelleğin ikinci kademesi route'lardan önce kurulmalı: ilk istek de
78
+ // paylaşımlı kopyayı görebilsin. Bağlanamazsa uyarı basılır ve uygulama
79
+ // bellek içi önbellekle çalışmaya devam eder — middleware sırasına
80
+ // dokunmayan, tamamen opsiyonel bir adım.
81
+ await connectRedis(config);
82
+
83
+ // Kalıcı log sink'leri (dosya / S3). Credential eksikse uyarı + no-op;
84
+ // site düşmez. Access middleware gerektiğinde biraz aşağıda mount edilir.
85
+ const { accessLog } = await configureLogs(config);
86
+
87
+ const app = express();
88
+
89
+ app.disable("x-powered-by");
90
+ app.use((req, res, next) => {
91
+ res.setHeader("X-Powered-By", config.brand.poweredBy);
92
+ next();
93
+ });
94
+
95
+ app.set("etag", "strong");
96
+ // Ters proxy arkasında doğru protokol ve istemci IP'si için. Doğrudan
97
+ // internete açık bir sunucuda kapatılmalı: açıkken istemci kendi
98
+ // `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit log
99
+ // yanlış IP görür.
100
+ app.set("trust proxy", config.security.trustProxy);
101
+
102
+ app.use(configRewrites("beforeFiles"));
103
+ app.use(compression());
104
+ app.use(headersMiddleware());
105
+ app.use(devGate());
106
+ app.use(redirects());
107
+ app.use(trailingSlash());
108
+
109
+ // Access log: headers/redirects sonrası, statikten önce — böylece
110
+ // tamamlanan her yanıt (304 dahil) süre alır; admin/prewarm içeride elenir.
111
+ if (accessLog) {
112
+ const { accessLogMiddleware } = await import("./logs/access-middleware.js");
113
+ app.use(accessLogMiddleware());
114
+ }
115
+
116
+ // Kullanıcının robots.txt'i statik dosya da olabilir, route da. İkisi de
117
+ // bu sarmalayıcıdan geçer. compression'dan sonra durur: ek, sıkıştırılmış
118
+ // baytların değil düz metnin sonuna yazılır.
119
+ app.use(robotsTxtMiddleware());
120
+
121
+ app.use(staticPrecompressed(config.dirs.public));
122
+ app.use(
123
+ express.static(config.dirs.public, {
124
+ index: false,
125
+ redirect: false,
126
+ // Hash'siz dosyalar (favicon, robots eki, elle konmuş görseller) içerik
127
+ // değişse de aynı adla kalıyor; bir yıllık cache onları güncellenemez
128
+ // hâle getiriyordu. Hash'li çıktı aşağıda ayrıca immutable işaretlenir.
129
+ maxAge: "1h",
130
+ setHeaders(res, filePath) {
131
+ if (filePath.includes(`${path.sep}assets${path.sep}`)) {
132
+ res.setHeader("Cache-Control", IMMUTABLE_CACHE);
133
+ }
134
+ },
135
+ }),
136
+ );
137
+
138
+ // Dev overlay yalnızca development'ta; dinamik import sayesinde prod
139
+ // sürecine hiçbir şey yüklenmez.
140
+ if (process.env.NODE_ENV === "development") {
141
+ const { mountDevtools } = await import("./dev/devtools.js");
142
+ mountDevtools(app);
143
+ }
144
+
145
+ // Yönetim paneli ortama bakmaz, config'e bakar: açıkça etkinleştirilmedikçe
146
+ // modül hiç yüklenmez ve yol da yoktur. Kendi gövde ayrıştırıcısını
147
+ // taşıdığı için aşağıdaki parser'lardan önce durabiliyor; route'lardan
148
+ // önce olması gerekiyor ki uygulama aynı yolu gölgeleyemesin.
149
+ if (config.admin.enabled) {
150
+ const { mountAdmin } = await import("./admin/mount.js");
151
+ mountAdmin(app);
152
+ }
153
+
154
+ // Uzak görsel proxy: allowHosts doluysa mount. Statikten sonra, body
155
+ // parser'dan önce — görsel GET'lerinde gövde ayrıştırma maliyeti ödenmesin.
156
+ if (config.images && config.images !== false && config.images.remote) {
157
+ const { mountImageOptimizer } = await import("./image-optimizer.js");
158
+ await mountImageOptimizer(app);
159
+ }
160
+
161
+ app.use(express.urlencoded({ extended: false, limit: "64kb" }));
162
+ app.use(express.json({ limit: "256kb" }));
163
+
164
+ app.use(csrf());
165
+
166
+ const handoff = config.auth?.crossSubdomainHandoff;
167
+ if (
168
+ handoff === true ||
169
+ (handoff &&
170
+ typeof handoff === "object" &&
171
+ /** @type {{ enabled?: boolean }} */ (handoff).enabled !== false)
172
+ ) {
173
+ const { mountAuthHandoff } = await import("./auth/handoff.js");
174
+ mountAuthHandoff(app);
175
+ }
176
+
177
+ app.use(configRewrites("afterFiles"));
178
+
179
+ await registerRoutes(app);
180
+
181
+ app.use(async (req, res, next) => {
182
+ try {
183
+ // headersMiddleware `/assets/*` için immutable basmış olabilir; eksik bir
184
+ // hash'li dosyanın 404'ü CDN'de bir yıl zehirlenmesin.
185
+ res.status(404).setHeader("Cache-Control", "no-store");
186
+ res.type("html").send(await renderNotFound());
187
+ } catch (error) {
188
+ next(error);
189
+ }
190
+ });
191
+
192
+ // Hata yönetimi: notFound/redirect kontrol akışı burada da yakalanır,
193
+ // çünkü bir controller dışında (ör. middleware içinde) fırlatılabilir.
194
+ app.use(async (error, req, res, next) => {
195
+ if (res.headersSent) {
196
+ next(error);
197
+ return;
198
+ }
199
+
200
+ if (isRedirectError(error)) {
201
+ res.redirect(error.statusCode, error.location);
202
+ return;
203
+ }
204
+
205
+ if (isNotFoundError(error)) {
206
+ res.status(404).setHeader("Cache-Control", "no-store");
207
+ res.type("html").send(await renderNotFound());
208
+ return;
209
+ }
210
+
211
+ const status = statusFromError(error);
212
+ // Isıtma turunun hataları tek tek loglanmaz; tur bitince özet olarak
213
+ // basılır. Yüzlerce yolu tarayan bir tur, upstream bir an tıksırdığında
214
+ // logu yığın izleriyle dolduruyordu.
215
+ if (isPrewarmRequest(req)) notePrewarmError(status, error);
216
+ else console.error(`[${status}] ${req.method} ${req.originalUrl}`, error);
217
+
218
+ // Hata sayfası hiçbir katmanda saklanmamalı: geçici bir upstream arızası
219
+ // CDN'de dakikalarca yaşayan bir 500 sayfasına dönüşmesin.
220
+ res.setHeader("Cache-Control", "no-store");
221
+ res
222
+ .status(status)
223
+ .type("html")
224
+ .send(await renderStatusPage(status, { error }));
225
+ });
226
+
227
+ return app;
228
+ }
229
+
230
+ /**
231
+ * Uygulamayı kurup dinlemeye başlar. CLI `jskelet start` bunu çağırır;
232
+ * gömülü kullanımda `createApp()` tercih edilir.
233
+ *
234
+ * Port doluysa başlamaz. `murder: true` veya argv'de `--murder` varsa
235
+ * dinleyen süreç öldürülüp bağlama denenir.
236
+ *
237
+ * @param {{ root?: string, configFile?: string, port?: number, host?: string, murder?: boolean }} [options]
238
+ * @returns {Promise<import('http').Server>}
239
+ */
240
+ export async function startServer(options = {}) {
241
+ const port = Number(options.port ?? process.env.PORT ?? 3000);
242
+ const murder =
243
+ options.murder === true || process.argv.includes("--murder");
244
+
245
+ // createApp pahalı; dolu portta uygulama kurmadan önce net hata ver.
246
+ await ensurePortFree(port, { murder });
247
+
248
+ const app = await createApp(options);
249
+
250
+ // Varsayılan `::`, `0.0.0.0` değil: ikisi de "tüm arayüzler" demek, ama
251
+ // yalnızca IPv6 soketi çift yığın çalışır ve `localhost`un `::1`e çözüldüğü
252
+ // durumu da kapsar. Tarayıcılar `localhost` için önce `::1` deniyor; sıradan
253
+ // isteklerde IPv4'e düşüyorlar ama WebSocket el sıkışması bu geri düşüşü
254
+ // yapmadan "failed" veriyordu. IPv6'sı olmayan bir makinede bağlama hata
255
+ // verir; aşağıda IPv4'e dönülür.
256
+ const host = options.host ?? process.env.HOST ?? null;
257
+
258
+ // Tek bir istek hatası süreci düşürmesin; logla ve ayakta kal. Bir haber
259
+ // sitesinde tek sayfanın hatası tüm siteyi indirmemeli.
260
+ process.on("unhandledRejection", (reason) => {
261
+ console.error("[unhandledRejection]", reason);
262
+ });
263
+ process.on("uncaughtException", (error) => {
264
+ console.error("[uncaughtException]", error);
265
+ });
266
+
267
+ // Dev panelinin canlı kanalı: el sıkışma `upgrade` olayında geçtiği için
268
+ // middleware zincirine değil, doğrudan sunucuya bağlanır. Modül `listen`den
269
+ // önce yüklenir; dinleme başladıktan sonra beklenen bir `await` kalırsa ilk
270
+ // upgrade isteği dinleyici yokken gelip reddedilebiliyor.
271
+ const attachDevSocket =
272
+ process.env.NODE_ENV === "development"
273
+ ? (await import("./dev/devtools.js")).attachDevSocket
274
+ : null;
275
+
276
+ return new Promise((resolve, reject) => {
277
+ /** @param {string} address */
278
+ const listen = (address) => {
279
+ const server = app.listen(port, address, () => {
280
+ // Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
281
+ // buradan anlar ve özet satırını ona göre basar.
282
+ console.log(
283
+ `jskelet → http://localhost:${port} (${process.env.NODE_ENV ?? "production"})`,
284
+ );
285
+ startPrewarm({ port });
286
+ attachShutdown(server);
287
+ resolve(server);
288
+ });
289
+
290
+ server.on("error", (error) => {
291
+ // IPv6 desteklenmiyorsa yalnızca varsayılan adres için IPv4'e dönülür;
292
+ // kullanıcı bir adres verdiyse sessizce başkasını dinlemek yanlış olur.
293
+ if (!host && isAddressUnsupported(error)) {
294
+ listen("0.0.0.0");
295
+ return;
296
+ }
297
+ if (error.code === "EADDRINUSE") {
298
+ const hint = murder
299
+ ? `port ${port} is still in use after --murder`
300
+ : `port ${port} is already in use. Pass --murder to kill it and start, or set PORT to another value.`;
301
+ const wrapped = new Error(hint);
302
+ /** @type {NodeJS.ErrnoException} */ (wrapped).code = "EADDRINUSE";
303
+ reject(wrapped);
304
+ return;
305
+ }
306
+ reject(error);
307
+ });
308
+
309
+ attachDevSocket?.(server);
310
+ };
311
+
312
+ listen(host ?? "::");
313
+ });
314
+ }
315
+
316
+ /**
317
+ * `SIGTERM`/`SIGINT` sonrası düzenli kapanış.
318
+ *
319
+ * Kapatılması gereken dış kaynaklar Redis ve log sink buffer'ları. Redis
320
+ * `quit` uçuştaki komutların bitmesini bekliyor; sert `disconnect` yarıda
321
+ * kalan bir `SET` bırakabiliyor. S3 sink kapanışta kalan batch'i PutObject
322
+ * ile gönderir.
323
+ *
324
+ * Açık HTTP bağlantıları **beklenmez**. `close()` tek başına yalnızca yeni
325
+ * bağlantıyı reddediyor; keep-alive bir istemci ya da dev panelinin
326
+ * WebSocket'i sunucuyu süresiz ayakta tutuyor ve Ctrl+C yanıt vermiyormuş gibi
327
+ * görünüyordu. Sinyal geldiğinde ters proxy zaten trafik göndermiyor.
328
+ *
329
+ * Yine de bir zamanlayıcı var: Redis kapanışı askıda kalırsa süreç `SIGKILL`
330
+ * beklemek zorunda kalmasın.
331
+ *
332
+ * @param {import('http').Server} server
333
+ */
334
+ function attachShutdown(server) {
335
+ let closing = false;
336
+
337
+ const shutdown = () => {
338
+ // İkinci sinyal beklemeyi kısa kessin: kullanıcı Ctrl+C'ye tekrar bastıysa
339
+ // gerçekten çıkmak istiyor.
340
+ if (closing) process.exit(0);
341
+ closing = true;
342
+
343
+ const timer = setTimeout(() => process.exit(0), 3000);
344
+ timer.unref();
345
+
346
+ server.close();
347
+ server.closeAllConnections?.();
348
+
349
+ void Promise.all([disconnectRedis(), flushLogs().then(() => closeLogs())])
350
+ .finally(() => {
351
+ clearTimeout(timer);
352
+ process.exit(0);
353
+ });
354
+ };
355
+
356
+ process.once("SIGTERM", shutdown);
357
+ process.once("SIGINT", shutdown);
358
+ }
359
+
360
+ /**
361
+ * @param {NodeJS.ErrnoException} error
362
+ * @returns {boolean} adres ailesi bu makinede kullanılamıyor mu
363
+ */
364
+ function isAddressUnsupported(error) {
365
+ return error.code === "EAFNOSUPPORT" || error.code === "EADDRNOTAVAIL";
366
+ }