jskelet 0.2.5 → 0.3.0

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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  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 +293 -287
  7. package/docs/03-routing.md +486 -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 +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -1,291 +1,295 @@
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 — gate'in 404'ü redirect'ten önce.
11
- * 4. staticPrecompressed → express.static — build'de üretilmiş `.br`/`.gz`
12
- * kopyalar varsa onlar servis edilir (kalite 11), yoksa istek altındaki
13
- * static'e düşer ve middleware anında sıkıştırır (kalite 5).
14
- * 4b. cache paneli (açıksa) — statikten sonra, route'lardan önce: kendi
15
- * gövde ayrıştırıcısını taşır ve uygulama yolunu gölgeleyemez.
16
- * 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
17
- * maliyeti ödenmesin.
18
- * 6. csrf — body parser'lardan sonra olmalı: token form alanından okunuyor.
19
- * Rewrite'lardan önce, çünkü kontrol istemcinin gördüğü yola bakar.
20
- * 7. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
21
- * 8. route'lar → 404 → hata yönetimi.
22
- */
23
- import path from "node:path";
24
- import process from "node:process";
25
- import express from "express";
26
- import { compression } from "./middleware/compression.js";
27
- import { headersMiddleware } from "./middleware/headers.js";
28
- import { csrf } from "./middleware/csrf.js";
29
- import { staticPrecompressed } from "./middleware/static-precompressed.js";
30
- import { devGate } from "./middleware/dev-gate.js";
31
- import { redirects } from "./middleware/redirects.js";
32
- import { configRewrites } from "./middleware/upstream-proxy.js";
33
- import { getConfig, loadConfig } from "../config/index.js";
34
- import { IMMUTABLE_CACHE } from "../config/defaults.js";
35
- import { registerRoutes } from "./router.js";
36
- import { renderNotFound } from "./render.js";
37
- import { renderStatusPage, statusFromError } from "./status-page.js";
38
- import { isPrewarmRequest, notePrewarmError, startPrewarm } from "./prewarm.js";
39
- import { trackUpstreamFetch } from "./upstream-tracking.js";
40
- import { configureUpstreamLimiter } from "./upstream-limiter.js";
41
- import { connectRedis, disconnectRedis } from "./redis.js";
42
- import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
43
-
44
- /**
45
- * @param {{ root?: string, configFile?: string }} [options]
46
- * @returns {Promise<import('express').Express>}
47
- */
48
- export async function createApp(options = {}) {
49
- // Config middleware'lerden önce okunur; yoksa uyarı basar, akış durmaz.
50
- await loadConfig(options);
51
- const config = getConfig();
52
-
53
- // Upstream hatalarının izlenmesi route'lardan önce kurulmalı: sarmalayıcı
54
- // yalnızca render bağlamı içindeki `fetch` çağrılarına bakar, ama bağlamın
55
- // ilk kurulduğu istek de kapsanmalı.
56
- if (config.trackUpstream) trackUpstreamFetch();
57
-
58
- // Hız freni sarmalayıcının içinden okunuyor; ayarı ona vermek yeterli.
59
- // `cache().upstream.rate` verilmedikçe hiçbir istek beklemez.
60
- configureUpstreamLimiter(config.upstream);
61
-
62
- // Önbelleğin ikinci kademesi route'lardan önce kurulmalı: ilk istek de
63
- // paylaşımlı kopyayı görebilsin. Bağlanamazsa uyarı basılır ve uygulama
64
- // bellek içi önbellekle çalışmaya devam eder — middleware sırasına
65
- // dokunmayan, tamamen opsiyonel bir adım.
66
- await connectRedis(config);
67
-
68
- const app = express();
69
-
70
- app.disable("x-powered-by");
71
- app.use((req, res, next) => {
72
- res.setHeader("X-Powered-By", config.brand.poweredBy);
73
- next();
74
- });
75
-
76
- app.set("etag", "strong");
77
- // Ters proxy arkasında doğru protokol ve istemci IP'si için. Doğrudan
78
- // internete açık bir sunucuda kapatılmalı: açıkken istemci kendi
79
- // `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit log
80
- // yanlış IP görür.
81
- app.set("trust proxy", config.security.trustProxy);
82
-
83
- app.use(configRewrites("beforeFiles"));
84
- app.use(compression());
85
- app.use(headersMiddleware());
86
- app.use(devGate());
87
- app.use(redirects());
88
-
89
- app.use(staticPrecompressed(config.dirs.public));
90
- app.use(
91
- express.static(config.dirs.public, {
92
- index: false,
93
- redirect: false,
94
- // Hash'siz dosyalar (favicon, robots eki, elle konmuş görseller) içerik
95
- // değişse de aynı adla kalıyor; bir yıllık cache onları güncellenemez
96
- // hâle getiriyordu. Hash'li çıktı aşağıda ayrıca immutable işaretlenir.
97
- maxAge: "1h",
98
- setHeaders(res, filePath) {
99
- if (filePath.includes(`${path.sep}assets${path.sep}`)) {
100
- res.setHeader("Cache-Control", IMMUTABLE_CACHE);
101
- }
102
- },
103
- }),
104
- );
105
-
106
- // Dev overlay yalnızca development'ta; dinamik import sayesinde prod
107
- // sürecine hiçbir şey yüklenmez.
108
- if (process.env.NODE_ENV === "development") {
109
- const { mountDevtools } = await import("./dev/devtools.js");
110
- mountDevtools(app);
111
- }
112
-
113
- // Önbellek paneli ortama bakmaz, config'e bakar: açıkça etkinleştirilmedikçe
114
- // modül hiç yüklenmez ve yol da yoktur. Kendi gövde ayrıştırıcısını
115
- // taşıdığı için aşağıdaki parser'lardan önce durabiliyor; route'lardan
116
- // önce olması gerekiyor ki uygulama aynı yolu gölgeleyemesin.
117
- if (config.cachePanel.enabled) {
118
- const { mountCachePanel } = await import("./cache-panel.js");
119
- mountCachePanel(app);
120
- }
121
-
122
- app.use(express.urlencoded({ extended: false, limit: "64kb" }));
123
- app.use(express.json({ limit: "256kb" }));
124
-
125
- app.use(csrf());
126
-
127
- app.use(configRewrites("afterFiles"));
128
-
129
- await registerRoutes(app);
130
-
131
- app.use(async (req, res, next) => {
132
- try {
133
- res.status(404).type("html").send(await renderNotFound());
134
- } catch (error) {
135
- next(error);
136
- }
137
- });
138
-
139
- // Hata yönetimi: notFound/redirect kontrol akışı burada da yakalanır,
140
- // çünkü bir controller dışında (ör. middleware içinde) fırlatılabilir.
141
- app.use(async (error, req, res, next) => {
142
- if (res.headersSent) {
143
- next(error);
144
- return;
145
- }
146
-
147
- if (isRedirectError(error)) {
148
- res.redirect(error.statusCode, error.location);
149
- return;
150
- }
151
-
152
- if (isNotFoundError(error)) {
153
- res.status(404).type("html").send(await renderNotFound());
154
- return;
155
- }
156
-
157
- const status = statusFromError(error);
158
- // Isıtma turunun hataları tek tek loglanmaz; tur bitince özet olarak
159
- // basılır. Yüzlerce yolu tarayan bir tur, upstream bir an tıksırdığında
160
- // logu yığın izleriyle dolduruyordu.
161
- if (isPrewarmRequest(req)) notePrewarmError(status, error);
162
- else console.error(`[${status}] ${req.method} ${req.originalUrl}`, error);
163
-
164
- // Hata sayfası hiçbir katmanda saklanmamalı: geçici bir upstream arızası
165
- // CDN'de dakikalarca yaşayan bir 500 sayfasına dönüşmesin.
166
- res.setHeader("Cache-Control", "no-store");
167
- res
168
- .status(status)
169
- .type("html")
170
- .send(await renderStatusPage(status, { error }));
171
- });
172
-
173
- return app;
174
- }
175
-
176
- /**
177
- * Uygulamayı kurup dinlemeye başlar. CLI `jskelet start` bunu çağırır;
178
- * gömülü kullanımda `createApp()` tercih edilir.
179
- *
180
- * @param {{ root?: string, configFile?: string, port?: number, host?: string }} [options]
181
- * @returns {Promise<import('http').Server>}
182
- */
183
- export async function startServer(options = {}) {
184
- const app = await createApp(options);
185
- const port = Number(options.port ?? process.env.PORT ?? 3000);
186
-
187
- // Varsayılan `::`, `0.0.0.0` değil: ikisi de "tüm arayüzler" demek, ama
188
- // yalnızca IPv6 soketi çift yığın çalışır ve `localhost`un `::1`e çözüldüğü
189
- // durumu da kapsar. Tarayıcılar `localhost` için önce `::1` deniyor; sıradan
190
- // isteklerde IPv4'e düşüyorlar ama WebSocket el sıkışması bu geri düşüşü
191
- // yapmadan "failed" veriyordu. IPv6'sı olmayan bir makinede bağlama hata
192
- // verir; aşağıda IPv4'e dönülür.
193
- const host = options.host ?? process.env.HOST ?? null;
194
-
195
- // Tek bir istek hatası süreci düşürmesin; logla ve ayakta kal. Bir haber
196
- // sitesinde tek sayfanın hatası tüm siteyi indirmemeli.
197
- process.on("unhandledRejection", (reason) => {
198
- console.error("[unhandledRejection]", reason);
199
- });
200
- process.on("uncaughtException", (error) => {
201
- console.error("[uncaughtException]", error);
202
- });
203
-
204
- // Dev panelinin canlı kanalı: el sıkışma `upgrade` olayında geçtiği için
205
- // middleware zincirine değil, doğrudan sunucuya bağlanır. Modül `listen`den
206
- // önce yüklenir; dinleme başladıktan sonra beklenen bir `await` kalırsa ilk
207
- // upgrade isteği dinleyici yokken gelip reddedilebiliyor.
208
- const attachDevSocket =
209
- process.env.NODE_ENV === "development"
210
- ? (await import("./dev/devtools.js")).attachDevSocket
211
- : null;
212
-
213
- return new Promise((resolve, reject) => {
214
- /** @param {string} address */
215
- const listen = (address) => {
216
- const server = app.listen(port, address, () => {
217
- // Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
218
- // buradan anlar ve özet satırını ona göre basar.
219
- console.log(
220
- `jskelet → http://localhost:${port} (${process.env.NODE_ENV ?? "production"})`,
221
- );
222
- startPrewarm({ port });
223
- attachShutdown(server);
224
- resolve(server);
225
- });
226
-
227
- server.on("error", (error) => {
228
- // IPv6 desteklenmiyorsa yalnızca varsayılan adres için IPv4'e dönülür;
229
- // kullanıcı bir adres verdiyse sessizce başkasını dinlemek yanlış olur.
230
- if (!host && isAddressUnsupported(error)) {
231
- listen("0.0.0.0");
232
- return;
233
- }
234
- reject(error);
235
- });
236
-
237
- attachDevSocket?.(server);
238
- };
239
-
240
- listen(host ?? "::");
241
- });
242
- }
243
-
244
- /**
245
- * `SIGTERM`/`SIGINT` sonrası düzenli kapanış.
246
- *
247
- * Kapatılması gereken tek dış bağlantı Redis ve `quit` uçuştaki komutların
248
- * bitmesini bekliyor; sert `disconnect` yarıda kalan bir `SET` bırakabiliyor.
249
- *
250
- * Açık HTTP bağlantıları **beklenmez**. `close()` tek başına yalnızca yeni
251
- * bağlantıyı reddediyor; keep-alive bir istemci ya da dev panelinin
252
- * WebSocket'i sunucuyu süresiz ayakta tutuyor ve Ctrl+C yanıt vermiyormuş gibi
253
- * görünüyordu. Sinyal geldiğinde ters proxy zaten trafik göndermiyor.
254
- *
255
- * Yine de bir zamanlayıcı var: Redis kapanışı askıda kalırsa süreç `SIGKILL`
256
- * beklemek zorunda kalmasın.
257
- *
258
- * @param {import('http').Server} server
259
- */
260
- function attachShutdown(server) {
261
- let closing = false;
262
-
263
- const shutdown = () => {
264
- // İkinci sinyal beklemeyi kısa kessin: kullanıcı Ctrl+C'ye tekrar bastıysa
265
- // gerçekten çıkmak istiyor.
266
- if (closing) process.exit(0);
267
- closing = true;
268
-
269
- const timer = setTimeout(() => process.exit(0), 3000);
270
- timer.unref();
271
-
272
- server.close();
273
- server.closeAllConnections?.();
274
-
275
- void disconnectRedis().finally(() => {
276
- clearTimeout(timer);
277
- process.exit(0);
278
- });
279
- };
280
-
281
- process.once("SIGTERM", shutdown);
282
- process.once("SIGINT", shutdown);
283
- }
284
-
285
- /**
286
- * @param {NodeJS.ErrnoException} error
287
- * @returns {boolean} adres ailesi bu makinede kullanılamıyor mu
288
- */
289
- function isAddressUnsupported(error) {
290
- return error.code === "EAFNOSUPPORT" || error.code === "EADDRNOTAVAIL";
291
- }
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
+ * 4. staticPrecompressed → express.static — build'de üretilmiş `.br`/`.gz`
14
+ * kopyalar varsa onlar servis edilir (kalite 11), yoksa istek altındaki
15
+ * static'e düşer ve middleware anında sıkıştırır (kalite 5).
16
+ * 4b. admin paneli (açıksa) — statikten sonra, route'lardan önce: kendi
17
+ * gövde ayrıştırıcısını taşır ve uygulama yolunu gölgeleyemez.
18
+ * 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
19
+ * maliyeti ödenmesin.
20
+ * 6. csrf — body parser'lardan sonra olmalı: token form alanından okunuyor.
21
+ * Rewrite'lardan önce, çünkü kontrol istemcinin gördüğü yola bakar.
22
+ * 7. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
23
+ * 8. route'lar → 404 → hata yönetimi.
24
+ */
25
+ import path from "node:path";
26
+ import process from "node:process";
27
+ import express from "express";
28
+ import { compression } from "./middleware/compression.js";
29
+ import { headersMiddleware } from "./middleware/headers.js";
30
+ import { csrf } from "./middleware/csrf.js";
31
+ import { staticPrecompressed } from "./middleware/static-precompressed.js";
32
+ import { devGate } from "./middleware/dev-gate.js";
33
+ import { redirects } from "./middleware/redirects.js";
34
+ import { trailingSlash } from "./middleware/trailing-slash.js";
35
+ import { configRewrites } from "./middleware/upstream-proxy.js";
36
+ import { getConfig, loadConfig } from "../config/index.js";
37
+ import { IMMUTABLE_CACHE } from "../config/defaults.js";
38
+ import { registerRoutes } from "./router.js";
39
+ import { renderNotFound } from "./render.js";
40
+ import { renderStatusPage, statusFromError } from "./status-page.js";
41
+ import { isPrewarmRequest, notePrewarmError, startPrewarm } from "./prewarm.js";
42
+ import { trackUpstreamFetch } from "./upstream-tracking.js";
43
+ import { configureUpstreamLimiter } from "./upstream-limiter.js";
44
+ import { connectRedis, disconnectRedis } from "./redis.js";
45
+ import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
46
+
47
+ /**
48
+ * @param {{ root?: string, configFile?: string }} [options]
49
+ * @returns {Promise<import('express').Express>}
50
+ */
51
+ export async function createApp(options = {}) {
52
+ // Config middleware'lerden önce okunur; yoksa uyarı basar, akış durmaz.
53
+ await loadConfig(options);
54
+ const config = getConfig();
55
+
56
+ // Upstream hatalarının izlenmesi route'lardan önce kurulmalı: sarmalayıcı
57
+ // yalnızca render bağlamı içindeki `fetch` çağrılarına bakar, ama bağlamın
58
+ // ilk kurulduğu istek de kapsanmalı.
59
+ if (config.trackUpstream) trackUpstreamFetch();
60
+
61
+ // Hız freni sarmalayıcının içinden okunuyor; ayarı ona vermek yeterli.
62
+ // `cache().upstream.rate` verilmedikçe hiçbir istek beklemez.
63
+ configureUpstreamLimiter(config.upstream);
64
+
65
+ // Önbelleğin ikinci kademesi route'lardan önce kurulmalı: ilk istek de
66
+ // paylaşımlı kopyayı görebilsin. Bağlanamazsa uyarı basılır ve uygulama
67
+ // bellek içi önbellekle çalışmaya devam eder — middleware sırasına
68
+ // dokunmayan, tamamen opsiyonel bir adım.
69
+ await connectRedis(config);
70
+
71
+ const app = express();
72
+
73
+ app.disable("x-powered-by");
74
+ app.use((req, res, next) => {
75
+ res.setHeader("X-Powered-By", config.brand.poweredBy);
76
+ next();
77
+ });
78
+
79
+ app.set("etag", "strong");
80
+ // Ters proxy arkasında doğru protokol ve istemci IP'si için. Doğrudan
81
+ // internete açık bir sunucuda kapatılmalı: açıkken istemci kendi
82
+ // `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit log
83
+ // yanlış IP görür.
84
+ app.set("trust proxy", config.security.trustProxy);
85
+
86
+ app.use(configRewrites("beforeFiles"));
87
+ app.use(compression());
88
+ app.use(headersMiddleware());
89
+ app.use(devGate());
90
+ app.use(redirects());
91
+ app.use(trailingSlash());
92
+
93
+ app.use(staticPrecompressed(config.dirs.public));
94
+ app.use(
95
+ express.static(config.dirs.public, {
96
+ index: false,
97
+ redirect: false,
98
+ // Hash'siz dosyalar (favicon, robots eki, elle konmuş görseller) içerik
99
+ // değişse de aynı adla kalıyor; bir yıllık cache onları güncellenemez
100
+ // hâle getiriyordu. Hash'li çıktı aşağıda ayrıca immutable işaretlenir.
101
+ maxAge: "1h",
102
+ setHeaders(res, filePath) {
103
+ if (filePath.includes(`${path.sep}assets${path.sep}`)) {
104
+ res.setHeader("Cache-Control", IMMUTABLE_CACHE);
105
+ }
106
+ },
107
+ }),
108
+ );
109
+
110
+ // Dev overlay yalnızca development'ta; dinamik import sayesinde prod
111
+ // sürecine hiçbir şey yüklenmez.
112
+ if (process.env.NODE_ENV === "development") {
113
+ const { mountDevtools } = await import("./dev/devtools.js");
114
+ mountDevtools(app);
115
+ }
116
+
117
+ // Yönetim paneli ortama bakmaz, config'e bakar: açıkça etkinleştirilmedikçe
118
+ // modül hiç yüklenmez ve yol da yoktur. Kendi gövde ayrıştırıcısını
119
+ // taşıdığı için aşağıdaki parser'lardan önce durabiliyor; route'lardan
120
+ // önce olması gerekiyor ki uygulama aynı yolu gölgeleyemesin.
121
+ if (config.admin.enabled) {
122
+ const { mountAdmin } = await import("./admin/mount.js");
123
+ mountAdmin(app);
124
+ }
125
+
126
+ app.use(express.urlencoded({ extended: false, limit: "64kb" }));
127
+ app.use(express.json({ limit: "256kb" }));
128
+
129
+ app.use(csrf());
130
+
131
+ app.use(configRewrites("afterFiles"));
132
+
133
+ await registerRoutes(app);
134
+
135
+ app.use(async (req, res, next) => {
136
+ try {
137
+ res.status(404).type("html").send(await renderNotFound());
138
+ } catch (error) {
139
+ next(error);
140
+ }
141
+ });
142
+
143
+ // Hata yönetimi: notFound/redirect kontrol akışı burada da yakalanır,
144
+ // çünkü bir controller dışında (ör. middleware içinde) fırlatılabilir.
145
+ app.use(async (error, req, res, next) => {
146
+ if (res.headersSent) {
147
+ next(error);
148
+ return;
149
+ }
150
+
151
+ if (isRedirectError(error)) {
152
+ res.redirect(error.statusCode, error.location);
153
+ return;
154
+ }
155
+
156
+ if (isNotFoundError(error)) {
157
+ res.status(404).type("html").send(await renderNotFound());
158
+ return;
159
+ }
160
+
161
+ const status = statusFromError(error);
162
+ // Isıtma turunun hataları tek tek loglanmaz; tur bitince özet olarak
163
+ // basılır. Yüzlerce yolu tarayan bir tur, upstream bir an tıksırdığında
164
+ // logu yığın izleriyle dolduruyordu.
165
+ if (isPrewarmRequest(req)) notePrewarmError(status, error);
166
+ else console.error(`[${status}] ${req.method} ${req.originalUrl}`, error);
167
+
168
+ // Hata sayfası hiçbir katmanda saklanmamalı: geçici bir upstream arızası
169
+ // CDN'de dakikalarca yaşayan bir 500 sayfasına dönüşmesin.
170
+ res.setHeader("Cache-Control", "no-store");
171
+ res
172
+ .status(status)
173
+ .type("html")
174
+ .send(await renderStatusPage(status, { error }));
175
+ });
176
+
177
+ return app;
178
+ }
179
+
180
+ /**
181
+ * Uygulamayı kurup dinlemeye başlar. CLI `jskelet start` bunu çağırır;
182
+ * gömülü kullanımda `createApp()` tercih edilir.
183
+ *
184
+ * @param {{ root?: string, configFile?: string, port?: number, host?: string }} [options]
185
+ * @returns {Promise<import('http').Server>}
186
+ */
187
+ export async function startServer(options = {}) {
188
+ const app = await createApp(options);
189
+ const port = Number(options.port ?? process.env.PORT ?? 3000);
190
+
191
+ // Varsayılan `::`, `0.0.0.0` değil: ikisi de "tüm arayüzler" demek, ama
192
+ // yalnızca IPv6 soketi çift yığın çalışır ve `localhost`un `::1`e çözüldüğü
193
+ // durumu da kapsar. Tarayıcılar `localhost` için önce `::1` deniyor; sıradan
194
+ // isteklerde IPv4'e düşüyorlar ama WebSocket el sıkışması bu geri düşüşü
195
+ // yapmadan "failed" veriyordu. IPv6'sı olmayan bir makinede bağlama hata
196
+ // verir; aşağıda IPv4'e dönülür.
197
+ const host = options.host ?? process.env.HOST ?? null;
198
+
199
+ // Tek bir istek hatası süreci düşürmesin; logla ve ayakta kal. Bir haber
200
+ // sitesinde tek sayfanın hatası tüm siteyi indirmemeli.
201
+ process.on("unhandledRejection", (reason) => {
202
+ console.error("[unhandledRejection]", reason);
203
+ });
204
+ process.on("uncaughtException", (error) => {
205
+ console.error("[uncaughtException]", error);
206
+ });
207
+
208
+ // Dev panelinin canlı kanalı: el sıkışma `upgrade` olayında geçtiği için
209
+ // middleware zincirine değil, doğrudan sunucuya bağlanır. Modül `listen`den
210
+ // önce yüklenir; dinleme başladıktan sonra beklenen bir `await` kalırsa ilk
211
+ // upgrade isteği dinleyici yokken gelip reddedilebiliyor.
212
+ const attachDevSocket =
213
+ process.env.NODE_ENV === "development"
214
+ ? (await import("./dev/devtools.js")).attachDevSocket
215
+ : null;
216
+
217
+ return new Promise((resolve, reject) => {
218
+ /** @param {string} address */
219
+ const listen = (address) => {
220
+ const server = app.listen(port, address, () => {
221
+ // Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
222
+ // buradan anlar ve özet satırını ona göre basar.
223
+ console.log(
224
+ `jskelet → http://localhost:${port} (${process.env.NODE_ENV ?? "production"})`,
225
+ );
226
+ startPrewarm({ port });
227
+ attachShutdown(server);
228
+ resolve(server);
229
+ });
230
+
231
+ server.on("error", (error) => {
232
+ // IPv6 desteklenmiyorsa yalnızca varsayılan adres için IPv4'e dönülür;
233
+ // kullanıcı bir adres verdiyse sessizce başkasını dinlemek yanlış olur.
234
+ if (!host && isAddressUnsupported(error)) {
235
+ listen("0.0.0.0");
236
+ return;
237
+ }
238
+ reject(error);
239
+ });
240
+
241
+ attachDevSocket?.(server);
242
+ };
243
+
244
+ listen(host ?? "::");
245
+ });
246
+ }
247
+
248
+ /**
249
+ * `SIGTERM`/`SIGINT` sonrası düzenli kapanış.
250
+ *
251
+ * Kapatılması gereken tek dış bağlantı Redis ve `quit` uçuştaki komutların
252
+ * bitmesini bekliyor; sert `disconnect` yarıda kalan bir `SET` bırakabiliyor.
253
+ *
254
+ * Açık HTTP bağlantıları **beklenmez**. `close()` tek başına yalnızca yeni
255
+ * bağlantıyı reddediyor; keep-alive bir istemci ya da dev panelinin
256
+ * WebSocket'i sunucuyu süresiz ayakta tutuyor ve Ctrl+C yanıt vermiyormuş gibi
257
+ * görünüyordu. Sinyal geldiğinde ters proxy zaten trafik göndermiyor.
258
+ *
259
+ * Yine de bir zamanlayıcı var: Redis kapanışı askıda kalırsa süreç `SIGKILL`
260
+ * beklemek zorunda kalmasın.
261
+ *
262
+ * @param {import('http').Server} server
263
+ */
264
+ function attachShutdown(server) {
265
+ let closing = false;
266
+
267
+ const shutdown = () => {
268
+ // İkinci sinyal beklemeyi kısa kessin: kullanıcı Ctrl+C'ye tekrar bastıysa
269
+ // gerçekten çıkmak istiyor.
270
+ if (closing) process.exit(0);
271
+ closing = true;
272
+
273
+ const timer = setTimeout(() => process.exit(0), 3000);
274
+ timer.unref();
275
+
276
+ server.close();
277
+ server.closeAllConnections?.();
278
+
279
+ void disconnectRedis().finally(() => {
280
+ clearTimeout(timer);
281
+ process.exit(0);
282
+ });
283
+ };
284
+
285
+ process.once("SIGTERM", shutdown);
286
+ process.once("SIGINT", shutdown);
287
+ }
288
+
289
+ /**
290
+ * @param {NodeJS.ErrnoException} error
291
+ * @returns {boolean} adres ailesi bu makinede kullanılamıyor mu
292
+ */
293
+ function isAddressUnsupported(error) {
294
+ return error.code === "EAFNOSUPPORT" || error.code === "EADDRNOTAVAIL";
295
+ }