jskelet 0.2.4 → 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 +8 -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 -1209
  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 -1239
  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 -670
  40. package/src/client/cache-panel/login.html +74 -74
  41. package/src/client/cache-panel/panel.css +756 -756
  42. package/src/client/cache-panel/panel.html +308 -308
  43. package/src/client/cache-panel/panel.js +915 -915
  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 -759
  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
@@ -1,291 +1,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 — 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 — 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
+ }