jskelet 0.4.6 → 0.4.8
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/CHANGELOG.md +10 -0
- package/docs/06-cache.md +64 -10
- package/docs/07-yapilandirma.md +24 -1
- package/docs/09-dev-araclari.md +11 -5
- package/docs/en/06-caching.md +65 -13
- package/docs/en/07-configuration.md +24 -1
- package/docs/en/09-dev-tools.md +11 -6
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +194 -5
- package/src/client/devtools/report.js +20 -0
- package/src/client/registry.js +8 -0
- package/src/config/defaults.js +40 -1
- package/src/config/index.js +106 -1
- package/src/http/request-context.js +4 -1
- package/src/index.js +1 -1
- package/src/server/dev/devtools.js +118 -5
- package/src/server/dev/report.js +131 -15
- package/src/server/html-cache.js +14 -0
- package/src/server/prewarm.js +268 -19
- package/src/server/render.js +22 -3
|
@@ -17,7 +17,7 @@ const BASE = "/__jskelet/dev";
|
|
|
17
17
|
const POLL_MS = 2000;
|
|
18
18
|
const MAX_ERRORS = 100;
|
|
19
19
|
|
|
20
|
-
/** @type {{ id: number, level: string, message: string, stack: string | null, source: string, at: number }[]} */
|
|
20
|
+
/** @type {{ id: number, level: string, message: string, stack: string | null, source: string, page?: string | null, island?: string | null, url?: string | null, details?: unknown, at: number }[]} */
|
|
21
21
|
const clientErrors = [];
|
|
22
22
|
let nextId = 1;
|
|
23
23
|
|
|
@@ -109,7 +109,14 @@ function loadState() {
|
|
|
109
109
|
/**
|
|
110
110
|
* @param {string} level
|
|
111
111
|
* @param {string} message
|
|
112
|
-
* @param {{
|
|
112
|
+
* @param {{
|
|
113
|
+
* stack?: string | null,
|
|
114
|
+
* source?: string,
|
|
115
|
+
* page?: string | null,
|
|
116
|
+
* island?: string | null,
|
|
117
|
+
* url?: string | null,
|
|
118
|
+
* details?: unknown,
|
|
119
|
+
* }} [extra]
|
|
113
120
|
*/
|
|
114
121
|
function pushError(level, message, extra = {}) {
|
|
115
122
|
clientErrors.unshift({
|
|
@@ -118,6 +125,10 @@ function pushError(level, message, extra = {}) {
|
|
|
118
125
|
message,
|
|
119
126
|
stack: extra.stack ?? null,
|
|
120
127
|
source: extra.source ?? "client",
|
|
128
|
+
page: extra.page ?? location.pathname,
|
|
129
|
+
island: extra.island ?? null,
|
|
130
|
+
url: extra.url ?? null,
|
|
131
|
+
details: extra.details ?? null,
|
|
121
132
|
at: Date.now(),
|
|
122
133
|
});
|
|
123
134
|
if (clientErrors.length > MAX_ERRORS) clientErrors.length = MAX_ERRORS;
|
|
@@ -318,6 +329,8 @@ const clientApi = [];
|
|
|
318
329
|
/**
|
|
319
330
|
* Tarayıcıdaki veri çağrıları. Panelin kendi uçları ve statik varlıklar
|
|
320
331
|
* sayılmaz; amaç sayfanın hangi API'lere kaç ms harcadığını görmek.
|
|
332
|
+
* 4xx/5xx ve ağ hataları Errors sekmesine de düşer — hangi sayfa / island
|
|
333
|
+
* bağlamı mümkün olduğunca doldurulur.
|
|
321
334
|
*/
|
|
322
335
|
function captureFetch() {
|
|
323
336
|
const original = window.fetch.bind(window);
|
|
@@ -326,13 +339,34 @@ function captureFetch() {
|
|
|
326
339
|
const url = typeof input === "string" ? input : (input?.url ?? String(input));
|
|
327
340
|
if (url.includes(BASE)) return original(input, init);
|
|
328
341
|
|
|
342
|
+
const method = String(init?.method ?? "GET").toUpperCase();
|
|
329
343
|
const started = performance.now();
|
|
330
344
|
try {
|
|
331
345
|
const response = await original(input, init);
|
|
332
346
|
record(url, performance.now() - started, response.status, response);
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
void reportFailedFetch({
|
|
349
|
+
url,
|
|
350
|
+
method,
|
|
351
|
+
status: response.status,
|
|
352
|
+
response,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
333
355
|
return response;
|
|
334
356
|
} catch (error) {
|
|
335
357
|
record(url, performance.now() - started, 0, null);
|
|
358
|
+
pushError(
|
|
359
|
+
"error",
|
|
360
|
+
`${method} ${shortUrl(url)} → network error`,
|
|
361
|
+
{
|
|
362
|
+
source: "fetch",
|
|
363
|
+
url,
|
|
364
|
+
page: location.pathname + location.search,
|
|
365
|
+
island: inferIsland(),
|
|
366
|
+
stack: error instanceof Error ? error.stack : null,
|
|
367
|
+
details: error instanceof Error ? error.message : String(error),
|
|
368
|
+
},
|
|
369
|
+
);
|
|
336
370
|
throw error;
|
|
337
371
|
}
|
|
338
372
|
};
|
|
@@ -355,6 +389,99 @@ function captureFetch() {
|
|
|
355
389
|
}
|
|
356
390
|
}
|
|
357
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Başarısız bir tarayıcı fetch'ini Errors listesine yazar. Gövdeyi klonlayıp
|
|
394
|
+
* okur; asıl Response tüketilmez.
|
|
395
|
+
*
|
|
396
|
+
* @param {{ url: string, method: string, status: number, response: Response }} call
|
|
397
|
+
*/
|
|
398
|
+
async function reportFailedFetch(call) {
|
|
399
|
+
const details = await readResponseDetails(call.response);
|
|
400
|
+
const summary = detailSummary(details);
|
|
401
|
+
pushError(
|
|
402
|
+
"error",
|
|
403
|
+
`${call.method} ${shortUrl(call.url)} → ${call.status}${
|
|
404
|
+
summary ? `: ${summary}` : ""
|
|
405
|
+
}`,
|
|
406
|
+
{
|
|
407
|
+
source: "fetch",
|
|
408
|
+
url: call.url,
|
|
409
|
+
page: location.pathname + location.search,
|
|
410
|
+
island: inferIsland(),
|
|
411
|
+
details,
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {Response} response
|
|
418
|
+
* @returns {Promise<unknown>}
|
|
419
|
+
*/
|
|
420
|
+
async function readResponseDetails(response) {
|
|
421
|
+
try {
|
|
422
|
+
const text = (await response.clone().text()).slice(0, 4000);
|
|
423
|
+
if (!text) return null;
|
|
424
|
+
try {
|
|
425
|
+
return JSON.parse(text);
|
|
426
|
+
} catch {
|
|
427
|
+
return text;
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @param {unknown} details
|
|
436
|
+
* @returns {string | null}
|
|
437
|
+
*/
|
|
438
|
+
function detailSummary(details) {
|
|
439
|
+
if (details == null) return null;
|
|
440
|
+
if (typeof details === "string") return details.slice(0, 200);
|
|
441
|
+
if (typeof details !== "object") return String(details);
|
|
442
|
+
const record = /** @type {Record<string, unknown>} */ (details);
|
|
443
|
+
for (const key of ["details", "detail", "message", "error", "title"]) {
|
|
444
|
+
const value = record[key];
|
|
445
|
+
if (typeof value === "string" && value.trim()) return value.slice(0, 200);
|
|
446
|
+
if (value && typeof value === "object") {
|
|
447
|
+
const nested = detailSummary(value);
|
|
448
|
+
if (nested) return nested;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
return JSON.stringify(details).slice(0, 200);
|
|
453
|
+
} catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @param {string} url */
|
|
459
|
+
function shortUrl(url) {
|
|
460
|
+
try {
|
|
461
|
+
const parsed = new URL(url, location.origin);
|
|
462
|
+
return parsed.origin === location.origin
|
|
463
|
+
? parsed.pathname + parsed.search
|
|
464
|
+
: parsed.host + parsed.pathname + parsed.search;
|
|
465
|
+
} catch {
|
|
466
|
+
return url;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Çağrı yığınında `islands/ad` izi varsa ya da bir island mount edilirken
|
|
472
|
+
* yakalanmışsa ada bağlar.
|
|
473
|
+
*
|
|
474
|
+
* @returns {string | null}
|
|
475
|
+
*/
|
|
476
|
+
function inferIsland() {
|
|
477
|
+
const mounting = globalThis.__JSKELET_MOUNTING_ISLAND;
|
|
478
|
+
if (typeof mounting === "string" && mounting) return mounting;
|
|
479
|
+
|
|
480
|
+
const stack = new Error().stack ?? "";
|
|
481
|
+
const match = stack.match(/islands[/\\]([A-Za-z0-9_-]+)/);
|
|
482
|
+
return match?.[1] ?? null;
|
|
483
|
+
}
|
|
484
|
+
|
|
358
485
|
/**
|
|
359
486
|
* Sayfa ölçümlerini rapora gönderir. Sekme kapanırken de gitmesi gerektiği
|
|
360
487
|
* için mümkünse `sendBeacon` kullanılır.
|
|
@@ -847,7 +974,11 @@ h4:first-child { margin-top: 0; }
|
|
|
847
974
|
.item.warn { border-inline-start-color: var(--mid); }
|
|
848
975
|
.item.selected { border-color: rgba(255,255,255,.22); background: var(--surface-hover); }
|
|
849
976
|
.item .meta { color: var(--muted); font-size: 10.5px; display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
|
|
977
|
+
.item .ctx-row { flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
|
978
|
+
.item .ctx { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 10.5px; color: #cfd6de; }
|
|
979
|
+
.item .ctx.muted { color: var(--muted); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
850
980
|
.tag { border: 1px solid var(--line); border-radius: 999px; padding: 0 7px; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; font-size: 9.5px; }
|
|
981
|
+
.tag.island { text-transform: none; letter-spacing: 0; color: #c4b5fd; border-color: rgba(196,181,253,.35); }
|
|
851
982
|
.msg { word-break: break-word; color: var(--text); }
|
|
852
983
|
.item pre { margin: 8px 0 0; padding: 8px 10px; background: rgba(0,0,0,.4); border-radius: 8px; white-space: pre-wrap; word-break: break-word; color: #b8c0cb; font-size: 11px; font-family: ui-monospace, SFMono-Regular, monospace; max-height: 150px; overflow: auto; }
|
|
853
984
|
.item .detail-text { margin: 8px 0 0; color: #b8c0cb; font-size: 12px; line-height: 1.55; }
|
|
@@ -1101,29 +1232,80 @@ const clipboard = new Map();
|
|
|
1101
1232
|
let copiedKey = null;
|
|
1102
1233
|
|
|
1103
1234
|
/**
|
|
1104
|
-
* @param {{
|
|
1235
|
+
* @param {{
|
|
1236
|
+
* level: string,
|
|
1237
|
+
* message: string,
|
|
1238
|
+
* stack?: string | null,
|
|
1239
|
+
* source?: string,
|
|
1240
|
+
* page?: string | null,
|
|
1241
|
+
* island?: string | null,
|
|
1242
|
+
* url?: string | null,
|
|
1243
|
+
* details?: unknown,
|
|
1244
|
+
* at: number,
|
|
1245
|
+
* }} item
|
|
1105
1246
|
* @returns {string}
|
|
1106
1247
|
*/
|
|
1107
1248
|
function asText(item) {
|
|
1108
1249
|
const time = new Date(item.at).toISOString();
|
|
1250
|
+
const details =
|
|
1251
|
+
item.details == null
|
|
1252
|
+
? ""
|
|
1253
|
+
: typeof item.details === "string"
|
|
1254
|
+
? item.details
|
|
1255
|
+
: safeJson(item.details);
|
|
1109
1256
|
return [
|
|
1110
1257
|
`[${item.level}] ${item.source ?? "server"} · ${time}`,
|
|
1258
|
+
item.page ? `page: ${item.page}` : "",
|
|
1259
|
+
item.island ? `island: ${item.island}` : "",
|
|
1260
|
+
item.url ? `url: ${item.url}` : "",
|
|
1111
1261
|
item.message,
|
|
1262
|
+
details,
|
|
1112
1263
|
item.stack ?? "",
|
|
1113
1264
|
]
|
|
1114
1265
|
.filter(Boolean)
|
|
1115
1266
|
.join("\n");
|
|
1116
1267
|
}
|
|
1117
1268
|
|
|
1118
|
-
/**
|
|
1269
|
+
/**
|
|
1270
|
+
* @param {unknown} value
|
|
1271
|
+
* @returns {string}
|
|
1272
|
+
*/
|
|
1273
|
+
function formatDetails(value) {
|
|
1274
|
+
if (value == null) return "";
|
|
1275
|
+
if (typeof value === "string") return value;
|
|
1276
|
+
try {
|
|
1277
|
+
return JSON.stringify(value, null, 2);
|
|
1278
|
+
} catch {
|
|
1279
|
+
return safeJson(value);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** @param {{ id?: number, level: string, message: string, stack?: string | null, source?: string, page?: string | null, island?: string | null, url?: string | null, details?: unknown, at: number }[]} list */
|
|
1119
1284
|
function errorList(list, scope) {
|
|
1120
1285
|
if (!list.length) return `<div class="empty">No records.</div>`;
|
|
1121
1286
|
return list
|
|
1122
1287
|
.map((item) => {
|
|
1123
1288
|
const key = `${scope}:${item.id ?? item.at}`;
|
|
1124
1289
|
const shown = expanded.has(key);
|
|
1290
|
+
const detailsKey = `${key}:details`;
|
|
1291
|
+
const detailsShown = expanded.has(detailsKey);
|
|
1292
|
+
const detailsText = formatDetails(item.details);
|
|
1125
1293
|
clipboard.set(key, asText(item));
|
|
1126
1294
|
|
|
1295
|
+
const context = [
|
|
1296
|
+
item.page
|
|
1297
|
+
? `<span class="ctx" title="Page">${pathLink(item.page)}</span>`
|
|
1298
|
+
: "",
|
|
1299
|
+
item.island
|
|
1300
|
+
? `<span class="tag island" title="Island">${escapeHtml(item.island)}</span>`
|
|
1301
|
+
: "",
|
|
1302
|
+
item.url && item.url !== item.page
|
|
1303
|
+
? `<span class="ctx muted" title="Request">${escapeHtml(shortUrl(item.url))}</span>`
|
|
1304
|
+
: "",
|
|
1305
|
+
]
|
|
1306
|
+
.filter(Boolean)
|
|
1307
|
+
.join("");
|
|
1308
|
+
|
|
1127
1309
|
return `<div class="item ${item.level === "warn" ? "warn" : ""}">
|
|
1128
1310
|
<div class="meta">
|
|
1129
1311
|
<span class="tag">${escapeHtml(item.source ?? "server")}</span>
|
|
@@ -1131,7 +1313,14 @@ function errorList(list, scope) {
|
|
|
1131
1313
|
<span class="spacer"></span>
|
|
1132
1314
|
${copyButton(key)}
|
|
1133
1315
|
</div>
|
|
1316
|
+
${context ? `<div class="meta ctx-row">${context}</div>` : ""}
|
|
1134
1317
|
<div class="msg">${escapeHtml(item.message)}</div>
|
|
1318
|
+
${
|
|
1319
|
+
detailsText
|
|
1320
|
+
? `<button class="link" data-action="stack" data-key="${escapeHtml(detailsKey)}">${detailsShown ? "▾ hide details" : "▸ show details"}</button>
|
|
1321
|
+
${detailsShown ? `<pre>${escapeHtml(detailsText)}</pre>` : ""}`
|
|
1322
|
+
: ""
|
|
1323
|
+
}
|
|
1135
1324
|
${
|
|
1136
1325
|
item.stack
|
|
1137
1326
|
? `<button class="link" data-action="stack" data-key="${escapeHtml(key)}">${shown ? "▾ hide stack" : "▸ show stack"}</button>
|
|
@@ -1187,7 +1376,7 @@ function errorsTab() {
|
|
|
1187
1376
|
${lede(
|
|
1188
1377
|
errorCount ? "bad" : total ? "mid" : "good",
|
|
1189
1378
|
errorCount
|
|
1190
|
-
? `<strong>${errorCount} errors</strong> captured${total - errorCount ? `, plus ${total - errorCount} warnings` : ""}. Newest first; expand the stack
|
|
1379
|
+
? `<strong>${errorCount} errors</strong> captured${total - errorCount ? `, plus ${total - errorCount} warnings` : ""}. Newest first; expand details or the stack, and copy a record with one click. Failed fetch calls include the page and API path.`
|
|
1191
1380
|
: total
|
|
1192
1381
|
? `No errors, <strong>${total} warnings</strong>. Warnings usually come from missing data or islands that were never ported.`
|
|
1193
1382
|
: "No browser or server side errors were seen in this session.",
|
|
@@ -556,6 +556,7 @@ function apiTab() {
|
|
|
556
556
|
"api-calls",
|
|
557
557
|
[
|
|
558
558
|
{ key: "url", label: "URL" },
|
|
559
|
+
{ key: "page", label: "Page", render: (row) => (row.page ? link(row.page) : "—") },
|
|
559
560
|
{ key: "method", label: "Method" },
|
|
560
561
|
{ key: "status", label: "Status", render: (row) => `<span class="status ${row.error ? "bad" : ""}">${row.status || "—"}</span>` },
|
|
561
562
|
{ key: "ms", label: "Time", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [200, 800])}">${ms(row.ms)}</span>` },
|
|
@@ -589,6 +590,25 @@ function errorsTab() {
|
|
|
589
590
|
"errors",
|
|
590
591
|
[
|
|
591
592
|
{ key: "message", label: "Message" },
|
|
593
|
+
{ key: "page", label: "Page", render: (row) => (row.page ? link(row.page) : "—") },
|
|
594
|
+
{
|
|
595
|
+
key: "details",
|
|
596
|
+
label: "Details",
|
|
597
|
+
render: (row) => {
|
|
598
|
+
if (row.details == null) return "—";
|
|
599
|
+
const text =
|
|
600
|
+
typeof row.details === "string"
|
|
601
|
+
? row.details
|
|
602
|
+
: (() => {
|
|
603
|
+
try {
|
|
604
|
+
return JSON.stringify(row.details);
|
|
605
|
+
} catch {
|
|
606
|
+
return String(row.details);
|
|
607
|
+
}
|
|
608
|
+
})();
|
|
609
|
+
return `<span class="hint" title="${escapeHtml(text)}">${escapeHtml(text.slice(0, 120))}${text.length > 120 ? "…" : ""}</span>`;
|
|
610
|
+
},
|
|
611
|
+
},
|
|
592
612
|
{ key: "level", label: "Level", render: (row) => `<span class="tag">${escapeHtml(row.level)}</span>` },
|
|
593
613
|
{ key: "at", label: "Time", value: (row) => row.at, render: (row) => new Date(row.at).toLocaleTimeString() },
|
|
594
614
|
],
|
package/src/client/registry.js
CHANGED
|
@@ -110,6 +110,12 @@ async function mountIsland(element) {
|
|
|
110
110
|
|
|
111
111
|
markMounted(element);
|
|
112
112
|
|
|
113
|
+
// Dev overlay başarısız fetch'i hangi island'dan geldiğine bağlayabilsin.
|
|
114
|
+
// Mount bittikten sonra (event handler'lar) bu işaret kalkar; o durumda
|
|
115
|
+
// yığın izine düşülür.
|
|
116
|
+
const previous = globalThis.__JSKELET_MOUNTING_ISLAND;
|
|
117
|
+
globalThis.__JSKELET_MOUNTING_ISLAND = name;
|
|
118
|
+
|
|
113
119
|
try {
|
|
114
120
|
const module = await loader();
|
|
115
121
|
const cleanup = module.mount(element, readProps(element));
|
|
@@ -130,6 +136,8 @@ async function mountIsland(element) {
|
|
|
130
136
|
element.dataset.islandReady = "true";
|
|
131
137
|
} catch (error) {
|
|
132
138
|
console.error(`[island] ${name} failed to load`, error);
|
|
139
|
+
} finally {
|
|
140
|
+
globalThis.__JSKELET_MOUNTING_ISLAND = previous;
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
143
|
|
package/src/config/defaults.js
CHANGED
|
@@ -50,7 +50,11 @@ export const DEFAULT_DEV_GATE_BYPASS = [
|
|
|
50
50
|
"/favicon.ico",
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Klasik (liste tabanlı) prewarm ayarları. `onVisit` modu bunlarla birlikte
|
|
55
|
+
* kullanılamaz — ya açılış/`prewarmPaths` turu, ya ziyaret edilen sayfadaki
|
|
56
|
+
* linkleri ısıtma.
|
|
57
|
+
*/
|
|
54
58
|
export const DEFAULT_PREWARM = {
|
|
55
59
|
enabled: true,
|
|
56
60
|
max: 400,
|
|
@@ -85,6 +89,41 @@ export const DEFAULT_PREWARM = {
|
|
|
85
89
|
priority: [],
|
|
86
90
|
};
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Ziyaret tabanlı ısıtma. Bir sayfa servis edilince HTML'deki aynı-origin
|
|
94
|
+
* linkler kuyruğa alınır; bir sonraki tıklama (veya başka ziyaretçi) çoğu
|
|
95
|
+
* zaman HIT görür. Klasik `prewarm` alanlarıyla karşılıklı dışlayıcıdır.
|
|
96
|
+
*/
|
|
97
|
+
export const DEFAULT_PREWARM_ON_VISIT = {
|
|
98
|
+
enabled: false,
|
|
99
|
+
/** Sayfa başına üstten alta en fazla kaç link kuyruğa alınır. */
|
|
100
|
+
perPage: 20,
|
|
101
|
+
/**
|
|
102
|
+
* Paralel işçi; `null` → klasik prewarm ile aynı varsayılan
|
|
103
|
+
* (prod 4 / dev 1) `startPrewarm` içinde çözülür.
|
|
104
|
+
* @type {number | null}
|
|
105
|
+
*/
|
|
106
|
+
concurrency: null,
|
|
107
|
+
/**
|
|
108
|
+
* Saniyedeki istek tavanı; `null` → klasik ile aynı (prod 0 / dev 4).
|
|
109
|
+
* @type {number | null}
|
|
110
|
+
*/
|
|
111
|
+
rps: null,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** `onVisit` açıkken `cache().prewarm` kökünde yasak olan klasik alanlar. */
|
|
115
|
+
export const CLASSIC_PREWARM_KEYS = [
|
|
116
|
+
"enabled",
|
|
117
|
+
"max",
|
|
118
|
+
"intervalSeconds",
|
|
119
|
+
"concurrency",
|
|
120
|
+
"delayMs",
|
|
121
|
+
"rps",
|
|
122
|
+
"retryDelayMs",
|
|
123
|
+
"rotate",
|
|
124
|
+
"priority",
|
|
125
|
+
];
|
|
126
|
+
|
|
88
127
|
/**
|
|
89
128
|
* HTML önbelleğinin girdi sınırı. 500 girdi ortalama bir sayfa boyutunda
|
|
90
129
|
* yaklaşık 100-200 MB tutar; uzun kuyruklu siteler bunu yükseltmek yerine
|
package/src/config/index.js
CHANGED
|
@@ -42,7 +42,9 @@ import {
|
|
|
42
42
|
DEFAULT_NAVIGATION,
|
|
43
43
|
DEFAULT_NAVIGATION_EXCLUDE,
|
|
44
44
|
DEFAULT_PREWARM,
|
|
45
|
+
DEFAULT_PREWARM_ON_VISIT,
|
|
45
46
|
DEFAULT_PREWARM_SKIP,
|
|
47
|
+
CLASSIC_PREWARM_KEYS,
|
|
46
48
|
DEFAULT_REDIS,
|
|
47
49
|
DEFAULT_SECURITY,
|
|
48
50
|
DEFAULT_STATIC,
|
|
@@ -655,7 +657,7 @@ function normalizeCache(raw) {
|
|
|
655
657
|
html.push({ pattern, seconds: value });
|
|
656
658
|
}
|
|
657
659
|
|
|
658
|
-
const prewarm =
|
|
660
|
+
const prewarm = normalizePrewarm(raw?.prewarm);
|
|
659
661
|
const queryRules = normalizeQueryRules(raw?.query);
|
|
660
662
|
const maxEntries = Number(raw?.maxEntries);
|
|
661
663
|
|
|
@@ -688,6 +690,98 @@ function normalizeCache(raw) {
|
|
|
688
690
|
};
|
|
689
691
|
}
|
|
690
692
|
|
|
693
|
+
/**
|
|
694
|
+
* @param {unknown} raw
|
|
695
|
+
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
696
|
+
*/
|
|
697
|
+
function normalizeOnVisit(raw) {
|
|
698
|
+
if (raw === true) {
|
|
699
|
+
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: true };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (raw == null || raw === false) {
|
|
703
|
+
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: false };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"[config] cache().prewarm.onVisit must be true, false, or an object",
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const source = /** @type {Record<string, unknown>} */ (raw);
|
|
713
|
+
const perPage = Number(source.perPage);
|
|
714
|
+
const concurrency = Number(source.concurrency);
|
|
715
|
+
const rps = Number(source.rps);
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
...DEFAULT_PREWARM_ON_VISIT,
|
|
719
|
+
enabled: source.enabled !== false,
|
|
720
|
+
perPage:
|
|
721
|
+
Number.isFinite(perPage) && perPage > 0
|
|
722
|
+
? Math.floor(perPage)
|
|
723
|
+
: DEFAULT_PREWARM_ON_VISIT.perPage,
|
|
724
|
+
concurrency:
|
|
725
|
+
Number.isFinite(concurrency) && concurrency > 0
|
|
726
|
+
? Math.floor(concurrency)
|
|
727
|
+
: null,
|
|
728
|
+
rps: Number.isFinite(rps) && rps >= 0 ? rps : null,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Klasik liste ısıtması ile `onVisit` karşılıklı dışlayıcıdır. İkisini birden
|
|
734
|
+
* yazmak sessizce yanlış moda düşmesin diye yüklemede hata verir.
|
|
735
|
+
*
|
|
736
|
+
* @param {unknown} raw
|
|
737
|
+
* @returns {Record<string, unknown>}
|
|
738
|
+
*/
|
|
739
|
+
function normalizePrewarm(raw) {
|
|
740
|
+
const source =
|
|
741
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
742
|
+
? /** @type {Record<string, unknown>} */ ({ ...raw })
|
|
743
|
+
: {};
|
|
744
|
+
|
|
745
|
+
const onVisit = normalizeOnVisit(source.onVisit);
|
|
746
|
+
|
|
747
|
+
if (onVisit.enabled) {
|
|
748
|
+
const conflicts = Object.keys(source).filter(
|
|
749
|
+
(key) => key !== "onVisit" && CLASSIC_PREWARM_KEYS.includes(key),
|
|
750
|
+
);
|
|
751
|
+
if (conflicts.length) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
"[config] cache().prewarm.onVisit cannot be combined with classic " +
|
|
754
|
+
`prewarm settings (${conflicts.join(", ")}). Use either onVisit or ` +
|
|
755
|
+
"classic settings (max, priority, rotate, …), not both.",
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const unknown = Object.keys(source).filter((key) => key !== "onVisit");
|
|
760
|
+
if (unknown.length) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
"[config] cache().prewarm.onVisit cannot be combined with " +
|
|
763
|
+
`${unknown.join(", ")}. On-visit mode only accepts the onVisit object.`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
...DEFAULT_PREWARM,
|
|
769
|
+
enabled: true,
|
|
770
|
+
onVisit,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Klasik mod: `onVisit: false` yazılmış olabilir; diğer alanlar varsayılanlarla
|
|
775
|
+
// birleşir. `onVisit` anahtarı çözülmüş nesnede her zaman durur.
|
|
776
|
+
const classic = { ...source };
|
|
777
|
+
delete classic.onVisit;
|
|
778
|
+
return {
|
|
779
|
+
...DEFAULT_PREWARM,
|
|
780
|
+
...classic,
|
|
781
|
+
onVisit,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
691
785
|
/** Speculation Rules'un tanıdığı eagerness değerleri. */
|
|
692
786
|
const EAGERNESS = new Set(["conservative", "moderate", "eager"]);
|
|
693
787
|
|
|
@@ -1043,6 +1137,17 @@ export async function loadConfig(options = {}) {
|
|
|
1043
1137
|
clientEnv: source.clientEnv ?? [],
|
|
1044
1138
|
};
|
|
1045
1139
|
|
|
1140
|
+
if (
|
|
1141
|
+
config.prewarm?.onVisit?.enabled &&
|
|
1142
|
+
typeof config.hooks?.prewarmPaths === "function"
|
|
1143
|
+
) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
"[config] hooks.prewarmPaths() cannot be used with cache().prewarm.onVisit. " +
|
|
1146
|
+
"On-visit mode warms links from each response; classic mode uses prewarmPaths. " +
|
|
1147
|
+
"Choose one.",
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1046
1151
|
// Dev'de build ve sunucu ayrı alt süreçler; üçü de aynı özeti basınca satır
|
|
1047
1152
|
// banner'ın ve build bloğunun arasına üç kez giriyor. Özeti dış süreç basar.
|
|
1048
1153
|
if (loaded && !process.env.JSKELET_CHILD) {
|
|
@@ -25,13 +25,15 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
25
25
|
* @property {string[]} taintReasons Hangi erişimler işaretledi (teşhis için).
|
|
26
26
|
* @property {string | null} csrfToken
|
|
27
27
|
* @property {import('express').Response | null} res
|
|
28
|
+
* @property {string | null} pathname Devtools: bu isteğin sayfa yolu (SSR
|
|
29
|
+
* sırasında upstream hatasına "hangi sayfa" bağlamak için).
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
32
|
/** @type {AsyncLocalStorage<RequestContext>} */
|
|
31
33
|
const storage = new AsyncLocalStorage();
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
* @param {{ private?: boolean, res?: import('express').Response }} [initial]
|
|
36
|
+
* @param {{ private?: boolean, res?: import('express').Response, pathname?: string | null }} [initial]
|
|
35
37
|
* @returns {RequestContext}
|
|
36
38
|
*/
|
|
37
39
|
export function createRequestContext(initial = {}) {
|
|
@@ -41,6 +43,7 @@ export function createRequestContext(initial = {}) {
|
|
|
41
43
|
taintReasons: [],
|
|
42
44
|
csrfToken: null,
|
|
43
45
|
res: initial.res ?? null,
|
|
46
|
+
pathname: initial.pathname ?? null,
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
package/src/index.js
CHANGED
|
@@ -77,7 +77,7 @@ export {
|
|
|
77
77
|
} from "./server/cloudflare.js";
|
|
78
78
|
// Upstream hız freninin host başına durumu: healthcheck ve teşhis uçları için.
|
|
79
79
|
export { getUpstreamLimiterStatus } from "./server/upstream-limiter.js";
|
|
80
|
-
export { prewarm, prewarmProgress } from "./server/prewarm.js";
|
|
80
|
+
export { prewarm, prewarmProgress, extractSameOriginLinks, isOnVisitPrewarm } from "./server/prewarm.js";
|
|
81
81
|
export { createProxy } from "./server/middleware/upstream-proxy.js";
|
|
82
82
|
export { getConfig, loadConfig } from "./config/index.js";
|
|
83
83
|
export { attrs, cn, cx, esc, jsonScript } from "./views/helpers/html.js";
|