jskelet 0.5.5 → 0.6.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 (156) hide show
  1. package/AGENTS.md +19 -15
  2. package/CHANGELOG.md +165 -15
  3. package/README.md +16 -21
  4. package/bin/jskelet.mjs +23 -9
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +10 -4
  7. package/docs/03-routing.md +14 -7
  8. package/docs/04-render-ve-sablonlar.md +60 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/06-cache.md +18 -7
  11. package/docs/07-yapilandirma.md +69 -27
  12. package/docs/08-build.md +15 -9
  13. package/docs/09-dev-araclari.md +22 -8
  14. package/docs/10-dagitim.md +14 -13
  15. package/docs/11-tasima.md +51 -17
  16. package/docs/12-panel-ve-oturum.md +10 -4
  17. package/docs/README.md +10 -33
  18. package/docs/en/01-getting-started.md +4 -3
  19. package/docs/en/02-architecture.md +12 -6
  20. package/docs/en/03-routing.md +15 -8
  21. package/docs/en/04-rendering.md +71 -59
  22. package/docs/en/05-islands.md +13 -8
  23. package/docs/en/06-caching.md +21 -7
  24. package/docs/en/07-configuration.md +69 -29
  25. package/docs/en/08-build.md +16 -10
  26. package/docs/en/09-dev-tools.md +24 -8
  27. package/docs/en/10-deployment.md +14 -14
  28. package/docs/en/11-migration.md +51 -16
  29. package/docs/en/12-dashboards-and-sessions.md +9 -4
  30. package/docs/en/README.md +10 -35
  31. package/package.json +48 -13
  32. package/src/build/tasks/client.mjs +91 -10
  33. package/src/build/tasks/icons.mjs +11 -1
  34. package/src/client/index.js +2 -2
  35. package/src/compile/codegen.js +4 -0
  36. package/src/compile/compile-all.js +12 -21
  37. package/src/compile/expr.js +5 -0
  38. package/src/compile/parse.js +64 -8
  39. package/src/compile/resolve.js +3 -0
  40. package/src/config/defaults.js +48 -5
  41. package/src/config/index.js +138 -27
  42. package/src/dev-server.mjs +26 -3
  43. package/src/http/cookies-entry.js +1 -0
  44. package/src/http/cookies.js +18 -0
  45. package/src/logo.png +0 -0
  46. package/src/migrate/apply.mjs +262 -0
  47. package/src/migrate/babel.mjs +79 -0
  48. package/src/migrate/classify.mjs +155 -0
  49. package/src/migrate/config.mjs +126 -0
  50. package/src/migrate/fs-walk.mjs +191 -0
  51. package/src/migrate/parse.mjs +26 -0
  52. package/src/migrate/scan.mjs +177 -0
  53. package/src/migrate/transform/expr-source.mjs +168 -0
  54. package/src/migrate/transform/island.mjs +67 -0
  55. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  56. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  57. package/src/migrate/transform/page-split.mjs +435 -0
  58. package/src/migrate/write.mjs +81 -0
  59. package/src/migrate.mjs +171 -0
  60. package/src/server/auth/handoff.js +94 -11
  61. package/src/server/create-app.js +37 -10
  62. package/src/server/ejs-adapter.js +59 -0
  63. package/src/server/html-cache.js +178 -32
  64. package/src/server/image-optimizer.js +94 -26
  65. package/src/server/middleware/dev-gate.js +21 -8
  66. package/src/server/middleware/robots-txt.js +341 -0
  67. package/src/server/port-guard.js +255 -0
  68. package/src/server/prewarm.js +137 -51
  69. package/src/server/render.js +30 -10
  70. package/src/server/status-page.js +105 -4
  71. package/src/start.mjs +18 -3
  72. package/src/templates/layout.ejs +8 -28
  73. package/src/templates/layout.jsk +30 -0
  74. package/src/templates/layout.render.js +41 -0
  75. package/src/views/helpers/tags.js +86 -3
  76. package/types/build/resolve-peer.d.mts +13 -0
  77. package/types/client/dom.d.ts +55 -0
  78. package/types/client/form.d.ts +19 -0
  79. package/types/client/index.d.ts +20 -0
  80. package/types/client/registry.d.ts +53 -0
  81. package/types/client/safe-image.d.ts +19 -0
  82. package/types/client/shared-cookie.d.ts +82 -0
  83. package/types/client/store.d.ts +18 -0
  84. package/types/client/swap.d.ts +46 -0
  85. package/types/compile/codegen.d.ts +32 -0
  86. package/types/compile/compile-all.d.ts +42 -0
  87. package/types/compile/errors.d.ts +30 -0
  88. package/types/compile/expr.d.ts +67 -0
  89. package/types/compile/index.d.ts +10 -0
  90. package/types/compile/parse.d.ts +82 -0
  91. package/types/compile/resolve.d.ts +46 -0
  92. package/types/compile/scan-exports.d.ts +9 -0
  93. package/types/config/defaults.d.ts +477 -0
  94. package/types/config/index.d.ts +304 -0
  95. package/types/config/pattern.d.ts +38 -0
  96. package/types/http/control-flow.d.ts +45 -0
  97. package/types/http/cookies-entry.d.ts +5 -0
  98. package/types/http/cookies.d.ts +113 -0
  99. package/types/http/request-cache.d.ts +13 -0
  100. package/types/http/request-context.d.ts +67 -0
  101. package/types/http/shared-cookie.d.ts +73 -0
  102. package/types/index.d.ts +30 -0
  103. package/types/log.d.mts +153 -0
  104. package/types/server/admin/actions.d.ts +16 -0
  105. package/types/server/admin/auth.d.ts +52 -0
  106. package/types/server/admin/event-log.d.ts +38 -0
  107. package/types/server/admin/gate.d.ts +43 -0
  108. package/types/server/admin/inventory.d.ts +40 -0
  109. package/types/server/admin/mount.d.ts +6 -0
  110. package/types/server/admin/router.d.ts +6 -0
  111. package/types/server/admin/snapshot.d.ts +6 -0
  112. package/types/server/assets.d.ts +47 -0
  113. package/types/server/auth/handoff.d.ts +12 -0
  114. package/types/server/cache-deps.d.ts +16 -0
  115. package/types/server/cache-vary.d.ts +30 -0
  116. package/types/server/cloudflare.d.ts +163 -0
  117. package/types/server/create-app.d.ts +25 -0
  118. package/types/server/data-cache.d.ts +116 -0
  119. package/types/server/dev/devtools.d.ts +44 -0
  120. package/types/server/dev/report.d.ts +229 -0
  121. package/types/server/dev/socket.d.ts +17 -0
  122. package/types/server/dev/version-check.d.mts +15 -0
  123. package/types/server/ejs-adapter.d.ts +11 -0
  124. package/types/server/head-hints.d.ts +40 -0
  125. package/types/server/html-cache.d.ts +207 -0
  126. package/types/server/image-optimizer.d.ts +68 -0
  127. package/types/server/logs/access-middleware.d.ts +7 -0
  128. package/types/server/logs/file-sink.d.ts +17 -0
  129. package/types/server/logs/pipeline.d.ts +37 -0
  130. package/types/server/logs/s3-put.d.ts +85 -0
  131. package/types/server/logs/s3-sink.d.ts +26 -0
  132. package/types/server/metadata.d.ts +38 -0
  133. package/types/server/middleware/compression.d.ts +17 -0
  134. package/types/server/middleware/csrf.d.ts +4 -0
  135. package/types/server/middleware/dev-gate.d.ts +2 -0
  136. package/types/server/middleware/headers.d.ts +2 -0
  137. package/types/server/middleware/redirects.d.ts +2 -0
  138. package/types/server/middleware/robots-txt.d.ts +33 -0
  139. package/types/server/middleware/static-precompressed.d.ts +5 -0
  140. package/types/server/middleware/trailing-slash.d.ts +11 -0
  141. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  142. package/types/server/og-image.d.ts +149 -0
  143. package/types/server/port-guard.d.ts +50 -0
  144. package/types/server/prewarm.d.ts +131 -0
  145. package/types/server/redis.d.ts +163 -0
  146. package/types/server/render.d.ts +101 -0
  147. package/types/server/router.d.ts +5 -0
  148. package/types/server/status-page.d.ts +24 -0
  149. package/types/server/upstream-limiter.d.ts +123 -0
  150. package/types/server/upstream-tracking.d.ts +42 -0
  151. package/types/shared/cookie-domain.d.ts +29 -0
  152. package/types/templates/layout.render.d.ts +7 -0
  153. package/types/version.d.mts +10 -0
  154. package/types/views/components/loader.d.ts +5 -0
  155. package/types/views/helpers/html.d.ts +39 -0
  156. package/types/views/helpers/tags.d.ts +127 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Dinlenecek port doluysa başlamadan önce net bir hata verilir; `--murder`
3
+ * verilmişse o porttaki dinleyici süreçler öldürülüp bağlama denenir.
4
+ *
5
+ * Windows: `netstat` + `taskkill`. Unix: `lsof` (yoksa `ss`) + `process.kill`.
6
+ * Bu süreçlerin PID'lerini okumak için yerleşik bir Node API'si yok.
7
+ */
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import process from "node:process";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ /**
15
+ * @param {number} ms
16
+ * @returns {Promise<void>}
17
+ */
18
+ function sleep(ms) {
19
+ return new Promise((resolve) => setTimeout(resolve, ms));
20
+ }
21
+
22
+ /**
23
+ * `netstat -ano -p tcp` çıktısından LISTENING PID'lerini çıkarır.
24
+ * `3000` ile `30001` karışmasın diye porttan sonra rakam olmamalı.
25
+ *
26
+ * @param {string} stdout
27
+ * @param {number} port
28
+ * @returns {number[]}
29
+ */
30
+ export function parseWindowsNetstat(stdout, port) {
31
+ const re = new RegExp(
32
+ String.raw`TCP\s+\S*:${port}(?!\d)\s+\S+\s+LISTENING\s+(\d+)`,
33
+ "gi",
34
+ );
35
+ /** @type {number[]} */
36
+ const pids = [];
37
+ for (const match of stdout.matchAll(re)) {
38
+ pids.push(Number(match[1]));
39
+ }
40
+ return pids;
41
+ }
42
+
43
+ /**
44
+ * `lsof -t` çıktısı: satır başına bir PID.
45
+ *
46
+ * @param {string} stdout
47
+ * @returns {number[]}
48
+ */
49
+ export function parseLsofPids(stdout) {
50
+ return stdout
51
+ .split(/\r?\n/)
52
+ .map((line) => Number(line.trim()))
53
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
54
+ }
55
+
56
+ /**
57
+ * `ss -H -lptn` satırlarından `pid=` değerlerini alır.
58
+ *
59
+ * @param {string} stdout
60
+ * @returns {number[]}
61
+ */
62
+ export function parseSsPids(stdout) {
63
+ /** @type {number[]} */
64
+ const pids = [];
65
+ for (const match of stdout.matchAll(/pid=(\d+)/g)) {
66
+ pids.push(Number(match[1]));
67
+ }
68
+ return pids;
69
+ }
70
+
71
+ /**
72
+ * Bu süreç ve ebeveyni öldürülmesin: `--murder` yenilemede kendi CLI'sını
73
+ * ya da henüz dinlemeyen kendisini vurmasın.
74
+ *
75
+ * @param {number} pid
76
+ * @returns {boolean}
77
+ */
78
+ function isSafeToKill(pid) {
79
+ return pid > 0 && pid !== process.pid && pid !== process.ppid;
80
+ }
81
+
82
+ /**
83
+ * @param {number} port
84
+ * @returns {Promise<number[]>}
85
+ */
86
+ async function listWindows(port) {
87
+ const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], {
88
+ windowsHide: true,
89
+ maxBuffer: 10 * 1024 * 1024,
90
+ });
91
+ return parseWindowsNetstat(stdout, port);
92
+ }
93
+
94
+ /**
95
+ * @param {number} port
96
+ * @returns {Promise<number[]>}
97
+ */
98
+ async function listUnix(port) {
99
+ try {
100
+ const { stdout } = await execFileAsync(
101
+ "lsof",
102
+ ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
103
+ { maxBuffer: 1024 * 1024 },
104
+ );
105
+ return parseLsofPids(stdout);
106
+ } catch (error) {
107
+ // lsof yoksa (exit 1 = dinleyen yok; ENOENT = komut yok) ss'e düş.
108
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") {
109
+ return listUnixSs(port);
110
+ }
111
+ const status = /** @type {{ status?: number | null }} */ (error).status;
112
+ if (status === 1) return [];
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {number} port
119
+ * @returns {Promise<number[]>}
120
+ */
121
+ async function listUnixSs(port) {
122
+ try {
123
+ const { stdout } = await execFileAsync(
124
+ "ss",
125
+ ["-H", "-lptn", `sport = :${port}`],
126
+ { maxBuffer: 1024 * 1024 },
127
+ );
128
+ return parseSsPids(stdout);
129
+ } catch (error) {
130
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") {
131
+ return [];
132
+ }
133
+ const status = /** @type {{ status?: number | null }} */ (error).status;
134
+ if (status === 1) return [];
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Portu dinleyen yabancı süreçlerin PID listesi.
141
+ *
142
+ * @param {number} port
143
+ * @returns {Promise<number[]>}
144
+ */
145
+ export async function listListeningPids(port) {
146
+ const raw =
147
+ process.platform === "win32"
148
+ ? await listWindows(port)
149
+ : await listUnix(port);
150
+ return [...new Set(raw)].filter(isSafeToKill);
151
+ }
152
+
153
+ /**
154
+ * @param {number} pid
155
+ * @returns {Promise<void>}
156
+ */
157
+ async function killPid(pid) {
158
+ if (process.platform === "win32") {
159
+ try {
160
+ await execFileAsync("taskkill", ["/PID", String(pid), "/F"], {
161
+ windowsHide: true,
162
+ });
163
+ } catch (error) {
164
+ // Süreç zaten gitmiş olabilir.
165
+ const status = /** @type {{ status?: number | null }} */ (error).status;
166
+ if (status === 128 || status === 1) return;
167
+ throw error;
168
+ }
169
+ return;
170
+ }
171
+
172
+ try {
173
+ process.kill(pid, "SIGTERM");
174
+ } catch (error) {
175
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === "ESRCH") return;
176
+ throw error;
177
+ }
178
+
179
+ await sleep(200);
180
+ try {
181
+ process.kill(pid, 0);
182
+ process.kill(pid, "SIGKILL");
183
+ } catch {
184
+ // ESRCH: SIGTERM yetmiş.
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Porttaki dinleyicileri öldürür ve soketin boşalmasını kısa süre bekler.
190
+ *
191
+ * @param {number} port
192
+ * @returns {Promise<number[]>} öldürülen PID'ler
193
+ */
194
+ export async function murderPort(port) {
195
+ const pids = await listListeningPids(port);
196
+ for (const pid of pids) {
197
+ await killPid(pid);
198
+ }
199
+
200
+ const deadline = Date.now() + 3000;
201
+ while (Date.now() < deadline) {
202
+ const still = await listListeningPids(port);
203
+ if (still.length === 0) return pids;
204
+ await sleep(50);
205
+ }
206
+
207
+ return pids;
208
+ }
209
+
210
+ /**
211
+ * Port doluysa ya net hata fırlatır ya da `--murder` ile temizler.
212
+ *
213
+ * Dinleyici listesi alınamazsa (araç yok / yetki) sessizce geçer; asıl bağlama
214
+ * `EADDRINUSE` ile yine düşer.
215
+ *
216
+ * @param {number} port
217
+ * @param {{ murder?: boolean }} [options]
218
+ * @returns {Promise<void>}
219
+ */
220
+ export async function ensurePortFree(port, options = {}) {
221
+ const murder = options.murder === true;
222
+
223
+ /** @type {number[]} */
224
+ let pids;
225
+ try {
226
+ pids = await listListeningPids(port);
227
+ } catch {
228
+ return;
229
+ }
230
+
231
+ if (pids.length === 0) return;
232
+
233
+ if (!murder) {
234
+ const error = new Error(
235
+ `port ${port} is already in use (pid ${pids.join(", ")}). ` +
236
+ `Pass --murder to kill it and start, or set PORT to another value.`,
237
+ );
238
+ /** @type {NodeJS.ErrnoException} */ (error).code = "EADDRINUSE";
239
+ throw error;
240
+ }
241
+
242
+ console.error(
243
+ `[jskelet] port ${port} in use by pid ${pids.join(", ")} — killing (--murder)`,
244
+ );
245
+ await murderPort(port);
246
+
247
+ const still = await listListeningPids(port);
248
+ if (still.length > 0) {
249
+ const error = new Error(
250
+ `port ${port} still in use after --murder (pid ${still.join(", ")})`,
251
+ );
252
+ /** @type {NodeJS.ErrnoException} */ (error).code = "EADDRINUSE";
253
+ throw error;
254
+ }
255
+ }
@@ -17,9 +17,16 @@
17
17
  * middleware zinciri normal trafikle bire bir aynı olsun.
18
18
  */
19
19
  import process from "node:process";
20
+ import { ON_VISIT_QUEUE_MAX } from "../config/defaults.js";
20
21
  import { getConfig, hook } from "../config/index.js";
21
22
  import { getRequestContext } from "../http/request-context.js";
22
- import { isHtmlCacheFresh, takeInvalidatedPaths, startEarlyExpirySweep } from "./html-cache.js";
23
+ import { publicHost } from "./cache-vary.js";
24
+ import {
25
+ isHtmlCacheFresh,
26
+ startEarlyExpirySweep,
27
+ takeInvalidatedPaths,
28
+ takeInvalidatedTargets,
29
+ } from "./html-cache.js";
23
30
  import { getDataCacheStats } from "./data-cache.js";
24
31
  import { isTransientStatus } from "./upstream-tracking.js";
25
32
  import { upstreamCooldownMs } from "./upstream-limiter.js";
@@ -51,36 +58,39 @@ function resolvePrewarmOrigins(port) {
51
58
  }
52
59
 
53
60
  /**
54
- * Vary açıkken ısıtma isteği ziyaretçinin Host'unu taşısın — aksi halde
55
- * loopback anahtarı ısınır, gerçek locale host soğuk kalır.
61
+ * `vary.host` açıkken ısıtma isteğinin cache anahtarı ziyaretçinin public
62
+ * host'u olsun. Fetch'in kendi `Host` başlığı loopback'tir ve değiştirilemez;
63
+ * `x-forwarded-host` ise `publicHost` tarafından önce okunur. İsteği public
64
+ * origin'e yollamak ALB üzerinden döner ve `h=127.0.0.1` diye ikinci girdi açar.
56
65
  *
57
66
  * @param {{ get?: (name: string) => string | undefined,
58
- * headers?: Record<string, unknown>, protocol?: string } | undefined} req
59
- * @param {string} fallback
67
+ * headers?: Record<string, unknown> } | undefined} req
60
68
  * @returns {string}
61
69
  */
62
- function originFromRequest(req, fallback) {
63
- if (!req) return fallback;
64
-
65
- let vary;
70
+ function forwardedHostFrom(req) {
71
+ if (!req) return "";
66
72
  try {
67
- vary = getConfig().cacheVary;
73
+ if (!getConfig().cacheVary?.host) return "";
68
74
  } catch {
69
- return fallback;
75
+ return "";
70
76
  }
71
- if (!vary?.host && !vary?.headers?.length && !vary?.fn) return fallback;
72
-
73
- const host =
74
- req.get?.("host") ||
75
- (typeof req.headers?.host === "string" ? req.headers.host : "");
76
- if (!host) return fallback;
77
-
78
- const forwarded = req.headers?.["x-forwarded-proto"];
79
- const protoRaw = Array.isArray(forwarded)
80
- ? forwarded[0]
81
- : forwarded || req.protocol || "http";
82
- const proto = String(protoRaw).split(",")[0].trim() || "http";
83
- return `${proto}://${host}`;
77
+ return publicHost(req);
78
+ }
79
+
80
+ /**
81
+ * Tazelik kontrolü, ısıtma isteğinin göreceği host ile aynı öneki arasın.
82
+ *
83
+ * @param {string} host
84
+ * @returns {{ headers: Record<string, string>, get: (name: string) => string | undefined } | undefined}
85
+ */
86
+ function requestForHost(host) {
87
+ if (!host) return undefined;
88
+ return {
89
+ headers: { host, "x-forwarded-host": host },
90
+ get(name) {
91
+ return this.headers[name];
92
+ },
93
+ };
84
94
  }
85
95
 
86
96
  /** Klasik turu yöneten env'ler; `onVisit` ile birlikte yasak. */
@@ -327,13 +337,19 @@ function createPacer(rps) {
327
337
  }
328
338
 
329
339
  /**
330
- * `DEV_TOKEN` ayarlıyken `devGate` token taşımayan her isteğe 404 döner.
331
- * Isıtma kendi sunucusuna istek attığı için token'ı çerez olarak taşımalı;
332
- * yoksa tüm sayfalar 404 alır ve önbellek hiç dolmaz.
340
+ * Gate açıkken token taşımayan her isteğe 404 döner. Isıtma kendi sunucusuna
341
+ * istek attığı için token'ı çerez olarak taşımalı; yoksa tüm sayfalar 404
342
+ * alır ve önbellek hiç dolmaz. Gate kapalıyken çerez gönderilmez.
333
343
  *
334
344
  * @returns {Record<string, string>}
335
345
  */
336
346
  function devGateHeader() {
347
+ try {
348
+ if (!getConfig().devGate) return {};
349
+ } catch {
350
+ return {};
351
+ }
352
+
337
353
  const token = process.env.DEV_TOKEN;
338
354
  if (!token) return {};
339
355
 
@@ -349,12 +365,20 @@ function devGateHeader() {
349
365
  * Tur ilerlemesini `prewarmProgress`'e yazar. Tekrar turunda sayaçların
350
366
  * anlamı değiştiği için çağıran taraf kendi formülünü verir.
351
367
  * @param {() => Promise<void>} [pace] İstek başına beklenen hız freni.
368
+ * @param {string} [forwardedHost] `vary.host` için `x-forwarded-host`.
352
369
  * @returns {Promise<{ ok: number, failed: number,
353
370
  * failures: { path: string, status: number }[] }>}
354
371
  * `failures` durum koduyla birlikte döner: tekrar turuna yalnızca geçici
355
372
  * hatalar alınıyor, kalıcı olanı yeniden denemek boşa çağrı.
356
373
  */
357
- async function crawl(origin, paths, concurrency, report = undefined, pace = undefined) {
374
+ async function crawl(
375
+ origin,
376
+ paths,
377
+ concurrency,
378
+ report = undefined,
379
+ pace = undefined,
380
+ forwardedHost = "",
381
+ ) {
358
382
  const { brand } = getConfig();
359
383
  const cacheHeader = brand.cacheHeader.toLowerCase();
360
384
 
@@ -379,6 +403,7 @@ async function crawl(origin, paths, concurrency, report = undefined, pace = unde
379
403
  // Sıkıştırılmış gövde de cache'lensin.
380
404
  "accept-encoding": "br, gzip",
381
405
  "user-agent": brand.prewarmUserAgent,
406
+ ...(forwardedHost ? { "x-forwarded-host": forwardedHost } : {}),
382
407
  ...devGateHeader(),
383
408
  },
384
409
  });
@@ -489,12 +514,14 @@ function upstreamUsage(before) {
489
514
  }
490
515
 
491
516
  /**
492
- * @param {{ origin: string, quiet?: boolean, paths?: string[] }} options
517
+ * @param {{ origin: string, quiet?: boolean, paths?: string[],
518
+ * forwardedHost?: string }} options
493
519
  * `paths` verilirse hook çağrılmaz, yalnızca o yollar ısıtılır (dev
494
- * panelindeki "tekrar dene" bunu kullanır).
520
+ * panelindeki "tekrar dene" bunu kullanır). `forwardedHost` loopback
521
+ * isteğine public host'u taşır; cache anahtarı `h=127.0.0.1` olmasın.
495
522
  * @returns {Promise<{ ok: number, failed: number, total: number, elapsed: number }>}
496
523
  */
497
- export async function prewarm({ origin, quiet = false, paths: only }) {
524
+ export async function prewarm({ origin, quiet = false, paths: only, forwardedHost = "" }) {
498
525
  const started = Date.now();
499
526
  const limit = setting("PREWARM_MAX", "max", 400);
500
527
  const isDev = process.env.NODE_ENV === "development";
@@ -560,6 +587,7 @@ export async function prewarm({ origin, quiet = false, paths: only }) {
560
587
  concurrency,
561
588
  undefined,
562
589
  pace,
590
+ forwardedHost,
563
591
  ));
564
592
 
565
593
  // Hatalar çoğunlukla upstream rate limit'i (429): ilk tur yüzlerce sayfayı
@@ -584,6 +612,7 @@ export async function prewarm({ origin, quiet = false, paths: only }) {
584
612
  prewarmProgress.failed = firstFailed - retriedOk;
585
613
  },
586
614
  pace,
615
+ forwardedHost,
587
616
  );
588
617
  recovered = retry.ok;
589
618
  ok += retry.ok;
@@ -736,6 +765,12 @@ function normalizeWarmPath(href, basePath = "/") {
736
765
 
737
766
  /** @type {string | null} */
738
767
  let visitOrigin = null;
768
+ /**
769
+ * onVisit turunun `x-forwarded-host` değeri. Boşsa vary kapalıdır ve
770
+ * loopback anahtarı doğrudur.
771
+ * @type {string}
772
+ */
773
+ let visitForwardedHost = "";
739
774
  /** @type {string[]} */
740
775
  const visitPending = [];
741
776
  /** @type {Set<string>} */
@@ -774,8 +809,9 @@ export function noteVisitWarm(html, context) {
774
809
  /** @type {string | undefined} */ (context.req?.headers?.["user-agent"]);
775
810
  if (ua && ua === getConfig().brand.prewarmUserAgent) return;
776
811
 
777
- // Vary açıksa bu ziyaretin Host'u üzerinden ısıt — loopback anahtarı değil.
778
- visitOrigin = originFromRequest(context.req, visitOrigin);
812
+ // Public origin'e çıkmak yerine loopback'te kal, host'u başlıkla taşı.
813
+ visitForwardedHost = forwardedHostFrom(context.req);
814
+ const freshReq = requestForHost(visitForwardedHost);
779
815
 
780
816
  const perPage = Number(getConfig().prewarm?.onVisit?.perPage) || 20;
781
817
  const links = extractSameOriginLinks(html, {
@@ -785,16 +821,29 @@ export function noteVisitWarm(html, context) {
785
821
 
786
822
  for (const path of links) {
787
823
  if (path === context.path) continue;
788
- if (isHtmlCacheFresh(path)) continue;
789
- if (visitQueued.has(path)) continue;
790
- visitQueued.add(path);
791
- visitPending.push(path);
824
+ if (isHtmlCacheFresh(path, freshReq)) continue;
825
+ if (!enqueueWarmPath(path)) break;
792
826
  }
793
827
 
794
828
  enqueueInvalidatedForVisit();
795
829
  if (visitPending.length) void drainVisitWarm();
796
830
  }
797
831
 
832
+ /**
833
+ * Kuyruk tavanı dolunca yeni yol alınmaz. Birikmiş yüzlerce link, rps
834
+ * freni olsa bile task'ı uzun süre meşgul eder.
835
+ *
836
+ * @param {string} path
837
+ * @returns {boolean} Kuyruğa girdi mi.
838
+ */
839
+ function enqueueWarmPath(path) {
840
+ if (visitPending.length >= ON_VISIT_QUEUE_MAX) return false;
841
+ if (visitQueued.has(path)) return false;
842
+ visitQueued.add(path);
843
+ visitPending.push(path);
844
+ return true;
845
+ }
846
+
798
847
  /**
799
848
  * Invalidate edilmiş yolları da kuyruğa alır (klasik turdaki
800
849
  * `takeInvalidatedPaths` karşılığı).
@@ -802,10 +851,13 @@ export function noteVisitWarm(html, context) {
802
851
  * @returns {void}
803
852
  */
804
853
  function enqueueInvalidatedForVisit() {
805
- for (const path of takeInvalidatedPaths()) {
806
- if (visitQueued.has(path)) continue;
807
- visitQueued.add(path);
808
- visitPending.push(path);
854
+ // Başka locale'in anahtarını bu ziyaretçinin host'uyla ısıtma; süre
855
+ // dolumu onları kendi `x-forwarded-host` değeriyle alır.
856
+ for (const target of takeInvalidatedTargets(visitForwardedHost || undefined)) {
857
+ if (isHtmlCacheFresh(target.path, requestForHost(target.host || visitForwardedHost))) {
858
+ continue;
859
+ }
860
+ if (!enqueueWarmPath(target.path)) break;
809
861
  }
810
862
  }
811
863
 
@@ -822,17 +874,23 @@ async function drainVisitWarm() {
822
874
  const batch = visitPending.splice(0, 32);
823
875
  for (const path of batch) visitQueued.delete(path);
824
876
 
825
- const cold = batch.filter((path) => !isHtmlCacheFresh(path));
877
+ const cold = batch.filter(
878
+ (path) => !isHtmlCacheFresh(path, requestForHost(visitForwardedHost)),
879
+ );
826
880
  if (!cold.length) continue;
827
881
 
828
882
  // Klasik `prewarm()` turunu yeniden kullan: retry, progress, UA aynı.
829
- // onVisit.concurrency / rps verilmişse `visitWarmSettings` ile iner;
830
- // yoksa `prewarm()` kendi (env → config → isDev) zincirine düşer.
883
+ // onVisit.concurrency / rps config tavanına çekilmiş hâlde iner.
831
884
  visitWarmSettings.concurrency = onVisit.concurrency;
832
885
  visitWarmSettings.rps = onVisit.rps;
833
886
 
834
887
  try {
835
- await prewarm({ origin: visitOrigin, paths: cold, quiet: true });
888
+ await prewarm({
889
+ origin: visitOrigin,
890
+ paths: cold,
891
+ quiet: true,
892
+ forwardedHost: visitForwardedHost,
893
+ });
836
894
  } catch (error) {
837
895
  console.error("[prewarm] onVisit warm failed", error);
838
896
  } finally {
@@ -953,16 +1011,44 @@ function startExpiryWarmDrain(origins) {
953
1011
  async function drainExpiryWarm() {
954
1012
  if (expiryDraining || !expiryOrigins.length) return;
955
1013
 
956
- const paths = takeInvalidatedPaths();
957
- if (!paths.length) return;
1014
+ const targets = takeInvalidatedTargets();
1015
+ if (!targets.length) return;
958
1016
 
959
1017
  expiryDraining = true;
960
1018
  try {
961
- const cold = paths.filter((path) => !isHtmlCacheFresh(path));
962
- if (!cold.length) return;
1019
+ /** @type {Map<string, string[]>} */
1020
+ const byHost = new Map();
1021
+ for (const target of targets) {
1022
+ if (isHtmlCacheFresh(target.path, requestForHost(target.host))) continue;
1023
+ const host = target.host || "";
1024
+ const list = byHost.get(host);
1025
+ if (list) list.push(target.path);
1026
+ else byHost.set(host, [target.path]);
1027
+ }
1028
+ if (!byHost.size) return;
963
1029
 
964
- for (const origin of expiryOrigins) {
965
- await prewarm({ origin, paths: cold, quiet: true });
1030
+ // Vary kapalı: anahtarda host yok, her yapılandırılmış origin ısınır.
1031
+ const bare = byHost.get("") ?? [];
1032
+ if (bare.length && byHost.size === 1) {
1033
+ for (const origin of expiryOrigins) {
1034
+ await prewarm({ origin, paths: bare, quiet: true });
1035
+ }
1036
+ return;
1037
+ }
1038
+
1039
+ // Host'lu anahtar loopback'e gider. `x-forwarded-host` cache anahtarını
1040
+ // public host yapar; aksi hâlde `h=127.0.0.1|/yol` diye ikinci girdi
1041
+ // açılır ve `maxEntries` slotunu yer.
1042
+ const origin = expiryOrigins[0];
1043
+ for (const [host, paths] of byHost) {
1044
+ if (!paths.length) continue;
1045
+ if (!host) {
1046
+ for (const next of expiryOrigins) {
1047
+ await prewarm({ origin: next, paths, quiet: true });
1048
+ }
1049
+ continue;
1050
+ }
1051
+ await prewarm({ origin, paths, quiet: true, forwardedHost: host });
966
1052
  }
967
1053
  } catch (error) {
968
1054
  console.error("[prewarm] expiry warm failed", error);
@@ -1,5 +1,6 @@
1
1
  /**
2
- * EJS render katmanı + HTML TTL cache (ISR ikamesi).
2
+ * Render katmanı + HTML TTL cache (ISR ikamesi).
3
+ * Varsayılan şablon yolu derlenmiş `.jsk`; EJS opsiyonel legacy peer.
3
4
  *
4
5
  * Controller sözleşmesi:
5
6
  * async (ctx) => { view, data?, metadata?, status?, revalidate?, head?,
@@ -16,10 +17,9 @@ import path from "node:path";
16
17
  import process from "node:process";
17
18
  import fs from "node:fs";
18
19
  import { pathToFileURL } from "node:url";
19
- import ejs from "ejs";
20
- import { withHtmlCache } from "./html-cache.js";
20
+ import { noteHtmlCacheGrowth, withHtmlCache } from "./html-cache.js";
21
21
  import { buildVaryPrefix } from "./cache-vary.js";
22
- import { getConfig, hook } from "../config/index.js";
22
+ import { getConfig, hook, FRAMEWORK_ROOT } from "../config/index.js";
23
23
  import { matchPattern } from "../config/pattern.js";
24
24
  import { encodeText, negotiateEncoding } from "./middleware/compression.js";
25
25
  import { navigationHints, preconnectHints } from "./head-hints.js";
@@ -48,15 +48,16 @@ import {
48
48
  getComponentDirs,
49
49
  getViewRoots,
50
50
  } from "../compile/index.js";
51
+ import { renderEjsFile } from "./ejs-adapter.js";
51
52
 
52
53
  const isDev = process.env.NODE_ENV === "development";
53
54
 
54
55
  /**
55
- * Şablonlara otomatik geçen yardımcılar ve EJS ayarları. Bileşen taraması
56
- * dosya sistemine dokunduğu için bir kez yapılır; config yüklenmeden
56
+ * Şablonlara otomatik geçen yardımcılar ve (legacy) EJS ayarları. Bileşen
57
+ * taraması dosya sistemine dokunduğu için bir kez yapılır; config yüklenmeden
57
58
  * hesaplanamaz, bu yüzden ilk render'da kurulur.
58
59
  *
59
- * @type {{ helpers: Record<string, unknown>, options: ejs.Options,
60
+ * @type {{ helpers: Record<string, unknown>, options: Record<string, unknown>,
60
61
  * viewsDir: string, viewRoots: string[], layout: string,
61
62
  * compiled: Map<string, (data: object, helpers: object) => string>,
62
63
  * layoutRender: ((data: object, helpers: object) => string) | null
@@ -89,6 +90,9 @@ async function getEngine() {
89
90
  Icon: (props) => tags.icon(props),
90
91
  CsrfField: () => tags.csrfField(),
91
92
  PreloadImage: (props) => tags.preloadImage(props),
93
+ Stylesheets: (props) => tags.stylesheets(props),
94
+ BodyScripts: (props) => tags.bodyScripts(props),
95
+ JsonLd: (props) => tags.jsonLd(props),
92
96
  };
93
97
 
94
98
  // camelCase JS bileşenlerini PascalCase etiket adıyla da erişilebilir yap.
@@ -99,7 +103,21 @@ async function getEngine() {
99
103
  if (!(pascal in helpers)) helpers[pascal] = value;
100
104
  }
101
105
 
102
- const { compiled, layoutRender } = await loadCompiledTemplates(config, helpers);
106
+ const { compiled, layoutRender: appLayoutRender } = await loadCompiledTemplates(
107
+ config,
108
+ helpers,
109
+ );
110
+
111
+ // Yalnızca framework varsayılan layout'una düşüldüğünde hazır modülü kullan;
112
+ // uygulamanın kendi layout.jsk'si derlenmemişse sessizce framework'e kayma.
113
+ let layoutRender = appLayoutRender;
114
+ if (!layoutRender) {
115
+ const fwLayout = path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
116
+ if (path.resolve(config.layout) === path.resolve(fwLayout)) {
117
+ const fw = await import("../templates/layout.render.js");
118
+ if (typeof fw.render === "function") layoutRender = fw.render;
119
+ }
120
+ }
103
121
 
104
122
  engine = {
105
123
  viewsDir,
@@ -212,7 +230,7 @@ export async function renderView(view, data = {}) {
212
230
  const file =
213
231
  findEjsView(viewRoots.length ? viewRoots : [viewsDir], view) ??
214
232
  path.join(viewsDir, `${view}.ejs`);
215
- return ejs.renderFile(file, { ...helpers, ...data }, options);
233
+ return renderEjsFile(file, { ...helpers, ...data }, options);
216
234
  }
217
235
 
218
236
  /**
@@ -277,7 +295,7 @@ export async function renderPage(page) {
277
295
  );
278
296
  }
279
297
 
280
- return ejs.renderFile(layout, locals, options);
298
+ return renderEjsFile(layout, locals, options);
281
299
  }
282
300
 
283
301
  /**
@@ -682,6 +700,8 @@ async function sendHtml(req, res, body, encoded, options = {}) {
682
700
  if (!buffer) {
683
701
  buffer = await encodeText(body, encoding);
684
702
  encoded.set(encoding, buffer);
703
+ // Gövde L1 girdisine sonradan eklenir; bayt bütçesi ham HTML'i aşmasın.
704
+ noteHtmlCacheGrowth();
685
705
  }
686
706
 
687
707
  res.setHeader("Content-Encoding", encoding);