jskelet 0.1.1

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 (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. package/src/views/helpers/tags.js +193 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Build çıktısı manifest'ine erişim: mantıksal isim → hash'li public URL.
3
+ *
4
+ * Build çalışmadıysa uygulama yine ayağa kalkar. `asset()` `/assets/<isim>`
5
+ * döner ve `hasAsset()` false olur; layout stylesheet/script etiketlerini hiç
6
+ * basmaz. Böylece `jskelet build` unutulduğunda hata yerine stilsiz ama
7
+ * çalışan bir sayfa görürsünüz.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+ import { getConfig } from "../config/index.js";
13
+
14
+ const isDev = process.env.NODE_ENV === "development";
15
+
16
+ /** @type {{ manifest: Record<string, string> } | null} */
17
+ let cached = null;
18
+ let warned = false;
19
+
20
+ function load() {
21
+ const { generated } = getConfig().dirs;
22
+
23
+ /** @type {Record<string, string>} */
24
+ let manifest = {};
25
+
26
+ try {
27
+ manifest = JSON.parse(
28
+ fs.readFileSync(path.join(generated, "manifest.json"), "utf8"),
29
+ );
30
+ } catch {
31
+ if (!warned) {
32
+ warned = true;
33
+ console.warn("[assets] manifest yok — `jskelet build` çalıştırın.");
34
+ }
35
+ }
36
+
37
+ return { manifest };
38
+ }
39
+
40
+ function state() {
41
+ // Dev'de her istekte yeniden okunur (watch build hash'leri değiştirir);
42
+ // prod'da bir kez.
43
+ if (isDev || !cached) cached = load();
44
+ return cached;
45
+ }
46
+
47
+ /**
48
+ * @param {string} name Örn. "app.css", "main.js", "sprite.svg"
49
+ * @returns {string}
50
+ */
51
+ export function asset(name) {
52
+ return state().manifest[name] ?? `/assets/${name}`;
53
+ }
54
+
55
+ /**
56
+ * @param {string} name
57
+ * @returns {boolean}
58
+ */
59
+ export function hasAsset(name) {
60
+ return Boolean(state().manifest[name]);
61
+ }
62
+
63
+ /**
64
+ * @typedef {{ width: number, height: number,
65
+ * variants: { width: number, url: string }[] }} OptimizedImage
66
+ */
67
+
68
+ /** @type {Record<string, OptimizedImage> | null} */
69
+ let images = null;
70
+
71
+ /**
72
+ * `build/tasks/images.mjs` çıktısı: kaynak yolundan webp varyantlarına.
73
+ * Build çalışmadıysa boş kalır ve görseller orijinalleriyle servis edilir.
74
+ *
75
+ * @param {string} [src] Örn. "/hero.png"
76
+ * @returns {OptimizedImage | undefined}
77
+ */
78
+ export function optimizedImage(src) {
79
+ if (isDev || !images) {
80
+ try {
81
+ const file = path.join(getConfig().dirs.generated, "images.json");
82
+ images = JSON.parse(fs.readFileSync(file, "utf8")).images ?? {};
83
+ } catch {
84
+ images = {};
85
+ }
86
+ }
87
+
88
+ return src ? images[src] : undefined;
89
+ }
90
+
91
+ /** @type {{ url: string, ids: Set<string> } | null} */
92
+ let spriteIds = null;
93
+
94
+ /**
95
+ * Sprite'taki sembol kimlikleri. Sprite yalnızca kaynakta **statik olarak**
96
+ * görülen ikon adlarını içerir; adı çalışma anında hesaplanan bir `icon()`
97
+ * çağrısı eksik sembole işaret ederse ekranda sessizce boşluk kalır. Dev'de
98
+ * `icon()` bu kümeye bakıp uyarır.
99
+ *
100
+ * @returns {Set<string>}
101
+ */
102
+ export function getSpriteIds() {
103
+ const url = asset("sprite.svg");
104
+ if (spriteIds?.url === url) return spriteIds.ids;
105
+
106
+ /** @type {Set<string>} */
107
+ const ids = new Set();
108
+
109
+ try {
110
+ const file = path.join(getConfig().dirs.public, url.replace(/^\//, ""));
111
+ const svg = fs.readFileSync(file, "utf8");
112
+ for (const match of svg.matchAll(/<symbol id="([^"]+)"/g)) ids.add(match[1]);
113
+ } catch {
114
+ // Sprite okunamazsa uyarı üretilmez; asıl akış etkilenmez.
115
+ }
116
+
117
+ spriteIds = { url, ids };
118
+ return ids;
119
+ }
@@ -0,0 +1,167 @@
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
+ * 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
15
+ * maliyeti ödenmesin.
16
+ * 6. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
17
+ * 7. route'lar → 404 → hata yönetimi.
18
+ */
19
+ import path from "node:path";
20
+ import process from "node:process";
21
+ import express from "express";
22
+ import { compression } from "./middleware/compression.js";
23
+ import { headersMiddleware } from "./middleware/headers.js";
24
+ import { staticPrecompressed } from "./middleware/static-precompressed.js";
25
+ import { devGate } from "./middleware/dev-gate.js";
26
+ import { redirects } from "./middleware/redirects.js";
27
+ import { configRewrites } from "./middleware/upstream-proxy.js";
28
+ import { getConfig, loadConfig } from "../config/index.js";
29
+ import { IMMUTABLE_CACHE } from "../config/defaults.js";
30
+ import { registerRoutes } from "./router.js";
31
+ import { renderNotFound } from "./render.js";
32
+ import { renderStatusPage, statusFromError } from "./status-page.js";
33
+ import { startPrewarm } from "./prewarm.js";
34
+ import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
35
+
36
+ /**
37
+ * @param {{ root?: string, configFile?: string }} [options]
38
+ * @returns {Promise<import('express').Express>}
39
+ */
40
+ export async function createApp(options = {}) {
41
+ // Config middleware'lerden önce okunur; yoksa uyarı basar, akış durmaz.
42
+ await loadConfig(options);
43
+ const config = getConfig();
44
+
45
+ const app = express();
46
+
47
+ app.disable("x-powered-by");
48
+ app.use((req, res, next) => {
49
+ res.setHeader("X-Powered-By", config.brand.poweredBy);
50
+ next();
51
+ });
52
+
53
+ app.set("etag", "strong");
54
+ // Ters proxy arkasında doğru protokol ve istemci IP'si için.
55
+ app.set("trust proxy", true);
56
+
57
+ app.use(configRewrites("beforeFiles"));
58
+ app.use(compression());
59
+ app.use(headersMiddleware());
60
+ app.use(devGate());
61
+ app.use(redirects());
62
+
63
+ app.use(staticPrecompressed(config.dirs.public));
64
+ app.use(
65
+ express.static(config.dirs.public, {
66
+ index: false,
67
+ redirect: false,
68
+ // Hash'siz dosyalar (favicon, robots eki, elle konmuş görseller) içerik
69
+ // değişse de aynı adla kalıyor; bir yıllık cache onları güncellenemez
70
+ // hâle getiriyordu. Hash'li çıktı aşağıda ayrıca immutable işaretlenir.
71
+ maxAge: "1h",
72
+ setHeaders(res, filePath) {
73
+ if (filePath.includes(`${path.sep}assets${path.sep}`)) {
74
+ res.setHeader("Cache-Control", IMMUTABLE_CACHE);
75
+ }
76
+ },
77
+ }),
78
+ );
79
+
80
+ // Dev overlay yalnızca development'ta; dinamik import sayesinde prod
81
+ // sürecine hiçbir şey yüklenmez.
82
+ if (process.env.NODE_ENV === "development") {
83
+ const { mountDevtools } = await import("./dev/devtools.js");
84
+ mountDevtools(app);
85
+ }
86
+
87
+ app.use(express.urlencoded({ extended: false, limit: "64kb" }));
88
+ app.use(express.json({ limit: "256kb" }));
89
+
90
+ app.use(configRewrites("afterFiles"));
91
+
92
+ await registerRoutes(app);
93
+
94
+ app.use(async (req, res, next) => {
95
+ try {
96
+ res.status(404).type("html").send(await renderNotFound());
97
+ } catch (error) {
98
+ next(error);
99
+ }
100
+ });
101
+
102
+ // Hata yönetimi: notFound/redirect kontrol akışı burada da yakalanır,
103
+ // çünkü bir controller dışında (ör. middleware içinde) fırlatılabilir.
104
+ app.use(async (error, req, res, next) => {
105
+ if (res.headersSent) {
106
+ next(error);
107
+ return;
108
+ }
109
+
110
+ if (isRedirectError(error)) {
111
+ res.redirect(error.statusCode, error.location);
112
+ return;
113
+ }
114
+
115
+ if (isNotFoundError(error)) {
116
+ res.status(404).type("html").send(await renderNotFound());
117
+ return;
118
+ }
119
+
120
+ const status = statusFromError(error);
121
+ console.error(`[${status}] ${req.method} ${req.originalUrl}`, error);
122
+
123
+ // Hata sayfası hiçbir katmanda saklanmamalı: geçici bir upstream arızası
124
+ // CDN'de dakikalarca yaşayan bir 500 sayfasına dönüşmesin.
125
+ res.setHeader("Cache-Control", "no-store");
126
+ res
127
+ .status(status)
128
+ .type("html")
129
+ .send(await renderStatusPage(status, { error }));
130
+ });
131
+
132
+ return app;
133
+ }
134
+
135
+ /**
136
+ * Uygulamayı kurup dinlemeye başlar. CLI `jskelet start` bunu çağırır;
137
+ * gömülü kullanımda `createApp()` tercih edilir.
138
+ *
139
+ * @param {{ root?: string, configFile?: string, port?: number, host?: string }} [options]
140
+ * @returns {Promise<import('http').Server>}
141
+ */
142
+ export async function startServer(options = {}) {
143
+ const app = await createApp(options);
144
+ const port = Number(options.port ?? process.env.PORT ?? 3000);
145
+ const host = options.host ?? process.env.HOST ?? "0.0.0.0";
146
+
147
+ // Tek bir istek hatası süreci düşürmesin; logla ve ayakta kal. Bir haber
148
+ // sitesinde tek sayfanın hatası tüm siteyi indirmemeli.
149
+ process.on("unhandledRejection", (reason) => {
150
+ console.error("[unhandledRejection]", reason);
151
+ });
152
+ process.on("uncaughtException", (error) => {
153
+ console.error("[uncaughtException]", error);
154
+ });
155
+
156
+ return new Promise((resolve) => {
157
+ const server = app.listen(port, host, () => {
158
+ // Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
159
+ // buradan anlar ve özet satırını ona göre basar.
160
+ console.log(
161
+ `jskelet → http://localhost:${port} (${process.env.NODE_ENV ?? "production"})`,
162
+ );
163
+ startPrewarm({ port });
164
+ resolve(server);
165
+ });
166
+ });
167
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Dev overlay'in sunucu tarafı: istek/render ölçümleri, hata günlüğü ve
3
+ * overlay modülünün servisi.
4
+ *
5
+ * Yalnızca `NODE_ENV=development` iken `create-app.js` tarafından mount
6
+ * edilir; build çıktısına hiçbir şey eklemez (overlay dosyası esbuild
7
+ * entry'si değil, doğrudan diskten servis edilir).
8
+ */
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { createHash } from "node:crypto";
13
+ import express from "express";
14
+ import * as log from "../../log.mjs";
15
+ import { FRAMEWORK_ROOT, getConfig } from "../../config/index.js";
16
+ import { prewarm, prewarmProgress } from "../prewarm.js";
17
+ import { clearHtmlCache } from "../html-cache.js";
18
+ import {
19
+ buildReport,
20
+ clearPageReports,
21
+ recordPageReport,
22
+ trackServerFetch,
23
+ } from "./report.js";
24
+
25
+ /** Overlay dosyaları framework paketinden servis edilir, uygulamadan değil. */
26
+ const DEVTOOLS_DIR = path.join(FRAMEWORK_ROOT, "src", "client", "devtools");
27
+ const OVERLAY_FILE = path.join(DEVTOOLS_DIR, "overlay.js");
28
+ const LOGO_FILE = path.join(FRAMEWORK_ROOT, "src", "logo.png");
29
+
30
+ /** @type {ReturnType<typeof getConfig>["brand"]} */
31
+ let brand;
32
+
33
+ /** Her sürecin kendine özgü kimliği; overlay restart'ı buradan anlar. */
34
+ const BOOT_ID = `${Date.now()}-${process.pid}`;
35
+
36
+ const MAX_ENTRIES = 50;
37
+
38
+ // Kayıtlar süreç belleğinde durursa `node --watch` her yeniden başlatmada
39
+ // geçmişi siler ve overlay boşalır. Bu dosya restart'lar arasında istek/hata
40
+ // geçmişini taşır.
41
+ //
42
+ // Proje ağacına yazılmaz: her yazma `node --watch`'u tetikleyip sunucuyu
43
+ // yeniden başlatıyordu ve bu kendini besleyen bir döngü kuruyordu
44
+ // (restart → açılış uyarısı → yazma → restart).
45
+ const STATE_FILE = path.join(
46
+ os.tmpdir(),
47
+ `jskelet-devtools-${createHash("sha1")
48
+ .update(getConfig().root)
49
+ .digest("hex")
50
+ .slice(0, 10)}.json`,
51
+ );
52
+
53
+ /** @type {{ id: number, method: string, url: string, status: number, ms: number, cache: string | null, at: number }[]} */
54
+ let requests = [];
55
+
56
+ /** @type {{ id: number, level: string, message: string, stack: string | null, url: string | null, at: number }[]} */
57
+ let errors = [];
58
+
59
+ let nextId = 1;
60
+
61
+ /** Süreç başlarken önceki turun kayıtlarını geri yükler. */
62
+ function restore() {
63
+ try {
64
+ const saved = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
65
+ requests = saved.requests ?? [];
66
+ errors = saved.errors ?? [];
67
+ nextId =
68
+ Math.max(0, ...requests.map((item) => item.id), ...errors.map((item) => item.id)) + 1;
69
+ } catch {
70
+ // İlk çalıştırma ya da bozuk dosya: temiz başla.
71
+ }
72
+ }
73
+
74
+ /** @type {NodeJS.Timeout | null} */
75
+ let saveTimer = null;
76
+
77
+ /** Yazma her istekte değil, kısa bir sessizlikten sonra yapılır. */
78
+ function persist() {
79
+ if (saveTimer) return;
80
+
81
+ saveTimer = setTimeout(() => {
82
+ saveTimer = null;
83
+ try {
84
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
85
+ fs.writeFileSync(STATE_FILE, JSON.stringify({ requests, errors }));
86
+ } catch {
87
+ // Günlük kalıcılığı en iyi çaba; başarısızlık dev akışını durdurmamalı.
88
+ }
89
+ }, 300);
90
+
91
+ saveTimer.unref?.();
92
+ }
93
+
94
+ /**
95
+ * @param {unknown[]} list
96
+ */
97
+ function trim(list) {
98
+ while (list.length > MAX_ENTRIES) list.shift();
99
+ }
100
+
101
+ /**
102
+ * @param {string} level
103
+ * @param {string} message
104
+ * @param {{ stack?: string | null, url?: string | null }} [extra]
105
+ */
106
+ export function recordServerError(level, message, extra = {}) {
107
+ errors.push({
108
+ id: nextId++,
109
+ level,
110
+ message,
111
+ stack: extra.stack ?? null,
112
+ url: extra.url ?? null,
113
+ at: Date.now(),
114
+ });
115
+ trim(errors);
116
+ persist();
117
+ }
118
+
119
+ /**
120
+ * `console.error` / `console.warn` çıktısını da overlay'e taşır: sunucudaki
121
+ * uyarılar terminalde kaybolmasın.
122
+ */
123
+ function patchConsole() {
124
+ for (const level of /** @type {const} */ (["error", "warn"])) {
125
+ const original = console[level].bind(console);
126
+ console[level] = (...args) => {
127
+ const error = args.find((arg) => arg instanceof Error);
128
+ recordServerError(level, args.map(format).join(" "), {
129
+ stack: error?.stack ?? null,
130
+ });
131
+ original(...args);
132
+ };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * @param {unknown} value
138
+ * @returns {string}
139
+ */
140
+ function format(value) {
141
+ if (typeof value === "string") return value;
142
+ if (value instanceof Error) return `${value.name}: ${value.message}`;
143
+ try {
144
+ return JSON.stringify(value);
145
+ } catch {
146
+ return String(value);
147
+ }
148
+ }
149
+
150
+ /** Her HTML isteğinin süresini ve cache durumunu kaydeder. */
151
+ function timing() {
152
+ /** @type {import('express').RequestHandler} */
153
+ return (req, res, next) => {
154
+ if (req.path.startsWith(brand.devBasePath)) return next();
155
+
156
+ // Önbellek ısıtması yüzlerce istek atıyor; terminali ve panelin istek
157
+ // listesini doldurmasın. İlerlemesi baloncuğun yanındaki rozette görünür.
158
+ if (req.headers["user-agent"] === brand.prewarmUserAgent) return next();
159
+
160
+ const started = process.hrtime.bigint();
161
+
162
+ res.on("finish", () => {
163
+ const type = res.getHeader("Content-Type");
164
+ if (!String(type ?? "").includes("text/html")) return;
165
+
166
+ const entry = {
167
+ id: nextId++,
168
+ method: req.method,
169
+ url: req.originalUrl,
170
+ status: res.statusCode,
171
+ ms: Number(process.hrtime.bigint() - started) / 1e6,
172
+ cache: /** @type {string | null} */ (res.getHeader(brand.cacheHeader) ?? null),
173
+ at: Date.now(),
174
+ };
175
+
176
+ requests.push(entry);
177
+ trim(requests);
178
+ persist();
179
+ // Terminalde canlı istek satırı.
180
+ log.http(entry);
181
+ });
182
+
183
+ next();
184
+ };
185
+ }
186
+
187
+ /* ------------------------------------------------------------ live reload */
188
+
189
+ /** @type {Set<import('express').Response>} */
190
+ const clients = new Set();
191
+
192
+ /**
193
+ * @param {import('express').Response} res
194
+ * @param {object} payload
195
+ */
196
+ function send(res, payload) {
197
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
198
+ }
199
+
200
+ /** @param {object} payload */
201
+ function broadcast(payload) {
202
+ for (const client of clients) send(client, payload);
203
+ }
204
+
205
+ /**
206
+ * Build watch'ı manifest'i her turda yeniden yazar. CSS değiştiyse sayfa
207
+ * yenilenmeden stylesheet takas edilir (durum ve kaydırma korunur); JS ya da
208
+ * başka bir varlık değiştiyse tam yenileme gerekir.
209
+ */
210
+ function watchManifest() {
211
+ const file = path.join(getConfig().dirs.generated, "manifest.json");
212
+
213
+ /** @returns {Record<string, string>} */
214
+ const read = () => {
215
+ try {
216
+ return JSON.parse(fs.readFileSync(file, "utf8"));
217
+ } catch {
218
+ return {};
219
+ }
220
+ };
221
+
222
+ let previous = read();
223
+ /** @type {NodeJS.Timeout | null} */
224
+ let timer = null;
225
+
226
+ try {
227
+ fs.watch(path.dirname(file), (event, name) => {
228
+ if (name !== "manifest.json") return;
229
+
230
+ clearTimeout(timer);
231
+ timer = setTimeout(() => {
232
+ const next = read();
233
+ const changed = Object.keys(next).filter((key) => next[key] !== previous[key]);
234
+ previous = next;
235
+
236
+ if (!changed.length) return;
237
+
238
+ // Önbellekteki HTML eski hash'li varlık URL'lerini taşıyor; temizlenmezse
239
+ // sayfa silinmiş dosyayı istemeye devam eder.
240
+ clearHtmlCache();
241
+
242
+ if (changed.length === 1 && changed[0] === "app.css") {
243
+ broadcast({ type: "css", href: next["app.css"] });
244
+ return;
245
+ }
246
+
247
+ broadcast({ type: "reload" });
248
+ }, 120);
249
+ });
250
+ } catch {
251
+ // Watch desteklenmiyorsa live reload devre dışı kalır; gerisi çalışır.
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Overlay script'i + istatistik uçları.
257
+ * @returns {import('express').Router}
258
+ */
259
+ function router() {
260
+ const api = express.Router();
261
+
262
+ api.get("/overlay.js", (req, res) => {
263
+ res.type("application/javascript");
264
+ res.setHeader("Cache-Control", "no-store");
265
+ fs.createReadStream(OVERLAY_FILE).pipe(res);
266
+ });
267
+
268
+ api.get("/logo.png", (req, res) => {
269
+ res.type("image/png");
270
+ res.setHeader("Cache-Control", "no-store");
271
+ fs.createReadStream(LOGO_FILE).pipe(res);
272
+ });
273
+
274
+ api.get("/events", (req, res) => {
275
+ res.writeHead(200, {
276
+ "Content-Type": "text/event-stream",
277
+ "Cache-Control": "no-store",
278
+ Connection: "keep-alive",
279
+ });
280
+
281
+ send(res, { type: "hello", boot: BOOT_ID });
282
+ clients.add(res);
283
+ req.on("close", () => clients.delete(res));
284
+ });
285
+
286
+ api.get("/stats", (req, res) => {
287
+ const usage = process.memoryUsage();
288
+ res.setHeader("Cache-Control", "no-store");
289
+ res.json({
290
+ pid: process.pid,
291
+ // Overlay yeniden başlatmayı bu kimlikten anlar; kendi durumunu
292
+ // sıfırlamadan yalnızca "restarted" bilgisini gösterir.
293
+ boot: BOOT_ID,
294
+ uptime: process.uptime(),
295
+ node: process.version,
296
+ memory: { rss: usage.rss, heapUsed: usage.heapUsed },
297
+ prewarm: { ...prewarmProgress },
298
+ requests: requests.slice(-25).reverse(),
299
+ errors: errors.slice(-25).reverse(),
300
+ });
301
+ });
302
+
303
+ // Detaylı rapor: kendi sayfası, script'i ve veri ucu.
304
+ api.get("/report", (req, res) => {
305
+ res.type("html");
306
+ res.setHeader("Cache-Control", "no-store");
307
+ fs.createReadStream(path.join(DEVTOOLS_DIR, "report.html")).pipe(res);
308
+ });
309
+
310
+ api.get("/report.js", (req, res) => {
311
+ res.type("application/javascript");
312
+ res.setHeader("Cache-Control", "no-store");
313
+ fs.createReadStream(path.join(DEVTOOLS_DIR, "report.js")).pipe(res);
314
+ });
315
+
316
+ api.get("/report/data", (req, res) => {
317
+ res.setHeader("Cache-Control", "no-store");
318
+ res.json(
319
+ buildReport({
320
+ requests: requests.slice().reverse(),
321
+ errors: errors.slice().reverse(),
322
+ }),
323
+ );
324
+ });
325
+
326
+ // Overlay her sayfadan ölçüm paketi gönderir; rapor bunları biriktirir.
327
+ api.post("/vitals", express.json({ limit: "512kb" }), (req, res) => {
328
+ recordPageReport(req.body);
329
+ res.status(204).end();
330
+ });
331
+
332
+ api.post("/report/clear", (req, res) => {
333
+ clearPageReports();
334
+ res.json({ ok: true });
335
+ });
336
+
337
+ // Panelden ısıtmayı elle tetikleme. Gövdede `paths` gelirse yalnızca o
338
+ // yollar denenir; boşsa sitemap'in tamamı baştan taranır.
339
+ api.post("/prewarm", express.json({ limit: "256kb" }), (req, res) => {
340
+ if (prewarmProgress.active) {
341
+ res.status(409).json({ ok: false, reason: "active" });
342
+ return;
343
+ }
344
+
345
+ const requested = Array.isArray(req.body?.paths) ? req.body.paths : [];
346
+ const paths = requested.filter(
347
+ (value) => typeof value === "string" && value.startsWith("/"),
348
+ );
349
+
350
+ prewarm({
351
+ origin: `${req.protocol}://${req.get("host")}`,
352
+ paths: paths.length ? paths : undefined,
353
+ }).catch((error) => {
354
+ console.error("[prewarm] elle tetikleme başarısız", error);
355
+ });
356
+
357
+ res.json({ ok: true, scope: paths.length || "all" });
358
+ });
359
+
360
+ api.post("/clear", express.json({ limit: "8kb" }), (req, res) => {
361
+ errors.length = 0;
362
+ requests.length = 0;
363
+ persist();
364
+ res.json({ ok: true });
365
+ });
366
+
367
+ return api;
368
+ }
369
+
370
+ /**
371
+ * Dev araçlarını uygulamaya bağlar.
372
+ * @param {import('express').Express} app
373
+ */
374
+ export function mountDevtools(app) {
375
+ brand = getConfig().brand;
376
+
377
+ restore();
378
+ patchConsole();
379
+ trackServerFetch();
380
+ watchManifest();
381
+ app.use(timing());
382
+ app.use(brand.devBasePath, router());
383
+ }