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.
- package/AGENTS.md +132 -132
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +103 -103
- package/docs/01-baslangic.md +285 -285
- package/docs/02-mimari.md +287 -287
- package/docs/03-routing.md +480 -480
- package/docs/04-render-ve-sablonlar.md +490 -490
- package/docs/05-islands.md +482 -482
- package/docs/06-cache.md +1209 -1209
- package/docs/08-build.md +366 -366
- package/docs/09-dev-araclari.md +335 -335
- package/docs/10-dagitim.md +329 -329
- package/docs/12-panel-ve-oturum.md +384 -384
- package/docs/README.md +105 -105
- package/docs/en/01-getting-started.md +292 -292
- package/docs/en/02-architecture.md +305 -305
- package/docs/en/03-routing.md +497 -497
- package/docs/en/04-rendering.md +504 -504
- package/docs/en/05-islands.md +492 -492
- package/docs/en/06-caching.md +1239 -1239
- package/docs/en/07-configuration.md +986 -986
- package/docs/en/08-build.md +383 -383
- package/docs/en/09-dev-tools.md +342 -342
- package/docs/en/10-deployment.md +332 -332
- package/docs/en/11-migration.md +359 -359
- package/docs/en/12-dashboards-and-sessions.md +392 -392
- package/docs/en/README.md +112 -112
- package/package.json +102 -102
- package/src/build/ensure-build.mjs +15 -15
- package/src/build/paths.mjs +143 -143
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +268 -268
- package/src/build/tasks/css.mjs +124 -124
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +224 -224
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/client/cache-panel/i18n.js +670 -670
- package/src/client/cache-panel/login.html +74 -74
- package/src/client/cache-panel/panel.css +756 -756
- package/src/client/cache-panel/panel.html +308 -308
- package/src/client/cache-panel/panel.js +915 -915
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +725 -725
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +35 -35
- package/src/client/registry.js +297 -297
- package/src/client/safe-image.js +91 -91
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/config/pattern.js +107 -107
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies.js +257 -257
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +162 -162
- package/src/index.js +83 -83
- package/src/init.mjs +221 -221
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/assets.js +147 -147
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-panel.js +759 -759
- package/src/server/cloudflare.js +607 -595
- package/src/server/create-app.js +291 -291
- package/src/server/data-cache.js +462 -462
- package/src/server/dev/report.js +369 -369
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/html-cache.js +817 -817
- 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 +62 -62
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/static-precompressed.js +100 -100
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/prewarm.js +601 -601
- package/src/server/redis.js +569 -569
- package/src/server/router.js +128 -128
- package/src/server/status-page.js +164 -164
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/start.mjs +7 -7
- package/src/templates/layout.ejs +44 -44
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +85 -85
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +245 -245
package/src/server/create-app.js
CHANGED
|
@@ -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
|
+
}
|