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.
@@ -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 {{ stack?: string | null, source?: string }} [extra]
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 {{ level: string, message: string, stack?: string | null, source?: string, at: number }} item
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
- /** @param {{ id?: number, level: string, message: string, stack?: string | null, source?: string, at: number }[]} list */
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 trace and copy a record with one click.`
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
  ],
@@ -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
 
@@ -50,7 +50,11 @@ export const DEFAULT_DEV_GATE_BYPASS = [
50
50
  "/favicon.ico",
51
51
  ];
52
52
 
53
- /** Prewarm ayarlarının kod içi varsayılanları. */
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
@@ -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 = { ...DEFAULT_PREWARM, ...(raw?.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";