jskelet 0.6.3 → 0.6.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.
- package/AGENTS.md +136 -136
- package/CHANGELOG.md +633 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +700 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +351 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +708 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +355 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +965 -356
- package/src/server/og-raster.mjs +21 -0
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +51 -3
package/src/server/create-app.js
CHANGED
|
@@ -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
|
+
}
|