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.
- package/AGENTS.md +19 -15
- package/CHANGELOG.md +165 -15
- package/README.md +16 -21
- package/bin/jskelet.mjs +23 -9
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +10 -4
- package/docs/03-routing.md +14 -7
- package/docs/04-render-ve-sablonlar.md +60 -43
- package/docs/05-islands.md +12 -8
- package/docs/06-cache.md +18 -7
- package/docs/07-yapilandirma.md +69 -27
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +22 -8
- package/docs/10-dagitim.md +14 -13
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +10 -33
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +12 -6
- package/docs/en/03-routing.md +15 -8
- package/docs/en/04-rendering.md +71 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/06-caching.md +21 -7
- package/docs/en/07-configuration.md +69 -29
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +24 -8
- package/docs/en/10-deployment.md +14 -14
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +10 -35
- package/package.json +48 -13
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +11 -1
- package/src/client/index.js +2 -2
- package/src/compile/codegen.js +4 -0
- package/src/compile/compile-all.js +12 -21
- package/src/compile/expr.js +5 -0
- package/src/compile/parse.js +64 -8
- package/src/compile/resolve.js +3 -0
- package/src/config/defaults.js +48 -5
- package/src/config/index.js +138 -27
- package/src/dev-server.mjs +26 -3
- package/src/http/cookies-entry.js +1 -0
- package/src/http/cookies.js +18 -0
- package/src/logo.png +0 -0
- package/src/migrate/apply.mjs +262 -0
- package/src/migrate/babel.mjs +79 -0
- package/src/migrate/classify.mjs +155 -0
- package/src/migrate/config.mjs +126 -0
- package/src/migrate/fs-walk.mjs +191 -0
- package/src/migrate/parse.mjs +26 -0
- package/src/migrate/scan.mjs +177 -0
- package/src/migrate/transform/expr-source.mjs +168 -0
- package/src/migrate/transform/island.mjs +67 -0
- package/src/migrate/transform/jsx-to-component.mjs +302 -0
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
- package/src/migrate/transform/page-split.mjs +435 -0
- package/src/migrate/write.mjs +81 -0
- package/src/migrate.mjs +171 -0
- package/src/server/auth/handoff.js +94 -11
- package/src/server/create-app.js +37 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/html-cache.js +178 -32
- package/src/server/image-optimizer.js +94 -26
- package/src/server/middleware/dev-gate.js +21 -8
- package/src/server/middleware/robots-txt.js +341 -0
- package/src/server/port-guard.js +255 -0
- package/src/server/prewarm.js +137 -51
- package/src/server/render.js +30 -10
- package/src/server/status-page.js +105 -4
- package/src/start.mjs +18 -3
- package/src/templates/layout.ejs +8 -28
- package/src/templates/layout.jsk +30 -0
- package/src/templates/layout.render.js +41 -0
- package/src/views/helpers/tags.js +86 -3
- package/types/build/resolve-peer.d.mts +13 -0
- package/types/client/dom.d.ts +55 -0
- package/types/client/form.d.ts +19 -0
- package/types/client/index.d.ts +20 -0
- package/types/client/registry.d.ts +53 -0
- package/types/client/safe-image.d.ts +19 -0
- package/types/client/shared-cookie.d.ts +82 -0
- package/types/client/store.d.ts +18 -0
- package/types/client/swap.d.ts +46 -0
- package/types/compile/codegen.d.ts +32 -0
- package/types/compile/compile-all.d.ts +42 -0
- package/types/compile/errors.d.ts +30 -0
- package/types/compile/expr.d.ts +67 -0
- package/types/compile/index.d.ts +10 -0
- package/types/compile/parse.d.ts +82 -0
- package/types/compile/resolve.d.ts +46 -0
- package/types/compile/scan-exports.d.ts +9 -0
- package/types/config/defaults.d.ts +477 -0
- package/types/config/index.d.ts +304 -0
- package/types/config/pattern.d.ts +38 -0
- package/types/http/control-flow.d.ts +45 -0
- package/types/http/cookies-entry.d.ts +5 -0
- package/types/http/cookies.d.ts +113 -0
- package/types/http/request-cache.d.ts +13 -0
- package/types/http/request-context.d.ts +67 -0
- package/types/http/shared-cookie.d.ts +73 -0
- package/types/index.d.ts +30 -0
- package/types/log.d.mts +153 -0
- package/types/server/admin/actions.d.ts +16 -0
- package/types/server/admin/auth.d.ts +52 -0
- package/types/server/admin/event-log.d.ts +38 -0
- package/types/server/admin/gate.d.ts +43 -0
- package/types/server/admin/inventory.d.ts +40 -0
- package/types/server/admin/mount.d.ts +6 -0
- package/types/server/admin/router.d.ts +6 -0
- package/types/server/admin/snapshot.d.ts +6 -0
- package/types/server/assets.d.ts +47 -0
- package/types/server/auth/handoff.d.ts +12 -0
- package/types/server/cache-deps.d.ts +16 -0
- package/types/server/cache-vary.d.ts +30 -0
- package/types/server/cloudflare.d.ts +163 -0
- package/types/server/create-app.d.ts +25 -0
- package/types/server/data-cache.d.ts +116 -0
- package/types/server/dev/devtools.d.ts +44 -0
- package/types/server/dev/report.d.ts +229 -0
- package/types/server/dev/socket.d.ts +17 -0
- package/types/server/dev/version-check.d.mts +15 -0
- package/types/server/ejs-adapter.d.ts +11 -0
- package/types/server/head-hints.d.ts +40 -0
- package/types/server/html-cache.d.ts +207 -0
- package/types/server/image-optimizer.d.ts +68 -0
- package/types/server/logs/access-middleware.d.ts +7 -0
- package/types/server/logs/file-sink.d.ts +17 -0
- package/types/server/logs/pipeline.d.ts +37 -0
- package/types/server/logs/s3-put.d.ts +85 -0
- package/types/server/logs/s3-sink.d.ts +26 -0
- package/types/server/metadata.d.ts +38 -0
- package/types/server/middleware/compression.d.ts +17 -0
- package/types/server/middleware/csrf.d.ts +4 -0
- package/types/server/middleware/dev-gate.d.ts +2 -0
- package/types/server/middleware/headers.d.ts +2 -0
- package/types/server/middleware/redirects.d.ts +2 -0
- package/types/server/middleware/robots-txt.d.ts +33 -0
- package/types/server/middleware/static-precompressed.d.ts +5 -0
- package/types/server/middleware/trailing-slash.d.ts +11 -0
- package/types/server/middleware/upstream-proxy.d.ts +21 -0
- package/types/server/og-image.d.ts +149 -0
- package/types/server/port-guard.d.ts +50 -0
- package/types/server/prewarm.d.ts +131 -0
- package/types/server/redis.d.ts +163 -0
- package/types/server/render.d.ts +101 -0
- package/types/server/router.d.ts +5 -0
- package/types/server/status-page.d.ts +24 -0
- package/types/server/upstream-limiter.d.ts +123 -0
- package/types/server/upstream-tracking.d.ts +42 -0
- package/types/shared/cookie-domain.d.ts +29 -0
- package/types/templates/layout.render.d.ts +7 -0
- package/types/version.d.mts +10 -0
- package/types/views/components/loader.d.ts +5 -0
- package/types/views/helpers/html.d.ts +39 -0
- 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
|
+
}
|
package/src/server/prewarm.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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
|
|
59
|
-
* @param {string} fallback
|
|
67
|
+
* headers?: Record<string, unknown> } | undefined} req
|
|
60
68
|
* @returns {string}
|
|
61
69
|
*/
|
|
62
|
-
function
|
|
63
|
-
if (!req) return
|
|
64
|
-
|
|
65
|
-
let vary;
|
|
70
|
+
function forwardedHostFrom(req) {
|
|
71
|
+
if (!req) return "";
|
|
66
72
|
try {
|
|
67
|
-
|
|
73
|
+
if (!getConfig().cacheVary?.host) return "";
|
|
68
74
|
} catch {
|
|
69
|
-
return
|
|
75
|
+
return "";
|
|
70
76
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
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(
|
|
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[]
|
|
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
|
-
//
|
|
778
|
-
|
|
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 (
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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(
|
|
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
|
|
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({
|
|
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
|
|
957
|
-
if (!
|
|
1014
|
+
const targets = takeInvalidatedTargets();
|
|
1015
|
+
if (!targets.length) return;
|
|
958
1016
|
|
|
959
1017
|
expiryDraining = true;
|
|
960
1018
|
try {
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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);
|
package/src/server/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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);
|