jskelet 0.4.7 → 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 +5 -0
- package/docs/09-dev-araclari.md +11 -5
- 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/http/request-context.js +4 -1
- package/src/server/dev/devtools.js +118 -5
- package/src/server/dev/report.js +131 -15
- package/src/server/render.js +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,11 @@ one is listed under a **Breaking** heading.
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- Dev overlay Errors tab now lists failed SSR and browser `fetch` calls with
|
|
14
|
+
page path, API URL, optional island name, and expandable response-body
|
|
15
|
+
details (JSON instead of `[object Object]`). Server `console.error` /
|
|
16
|
+
`console.warn` records also carry the current page when they fire during
|
|
17
|
+
render.
|
|
13
18
|
- Visit-driven HTML prewarm (`cache().prewarm.onVisit`): after each public
|
|
14
19
|
cacheable page response, same-origin links in the HTML are warmed in the
|
|
15
20
|
background (document order, `perPage` cap). Mutually exclusive with classic
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -177,9 +177,13 @@ Tüm arayüz shadow DOM içinde durur, sayfanın CSS'i ile karışmaz.
|
|
|
177
177
|
Gösterdikleri:
|
|
178
178
|
|
|
179
179
|
- **Hatalar:** tarayıcı tarafındaki JS hataları, kaynak yükleme hataları
|
|
180
|
-
(`img`/`script`/`link`),
|
|
181
|
-
çıktılar
|
|
182
|
-
|
|
180
|
+
(`img`/`script`/`link`), sunucudaki `console.error` / `console.warn`
|
|
181
|
+
çıktıları, ve SSR / tarayıcı `fetch` çağrılarının 4xx/5xx ya da ağ
|
|
182
|
+
başarısızlıkları. Her kayıt mümkün olduğunca **sayfa yolu**, **API URL** ve
|
|
183
|
+
(istemcide) **island adı** taşır; yanıt gövdesi **show details** ile açılır —
|
|
184
|
+
`[object Object]` yerine JSON. Sunucu tarafında `console` sarılır, böylece
|
|
185
|
+
uyarılar terminalde kaybolmaz; upstream hataları uygulamanın kendi logger'ı
|
|
186
|
+
stderr'e yazsa bile overlay'e düşer.
|
|
183
187
|
- **SEO:** açık sayfanın istemci tarafı taraması — title ve meta description
|
|
184
188
|
uzunluğu, `html lang`, viewport, canonical, robots/`noindex`, Open Graph ve
|
|
185
189
|
Twitter etiketleri, H1/outline, görsel `alt`, boş linkler ve JSON-LD parse
|
|
@@ -244,9 +248,11 @@ http://localhost:3000/__jskelet/dev/report
|
|
|
244
248
|
gezilmemiş ama ısıtılmış sayfalar da listelenir: SSR tarafı bilinir, istemci
|
|
245
249
|
ölçümleri boş kalır.
|
|
246
250
|
- **Sunucu API çağrıları:** SSR sırasında yapılan dış `fetch` çağrıları — URL,
|
|
247
|
-
host, metot, durum, süre, bayt
|
|
251
|
+
host, metot, durum, süre, bayt, hangi sayfa render edilirken yapıldığı ve
|
|
252
|
+
başarısız cevaplarda gövde özeti. `globalThis.fetch` yalnızca development'ta
|
|
248
253
|
sarılır; üretim yolu dokunulmaz kalır. Kendi sunucumuza yapılan istekler
|
|
249
|
-
(ısıtma, sağlık kontrolü) API sayılmaz.
|
|
254
|
+
(ısıtma, sağlık kontrolü) API sayılmaz. Başarısız çağrılar overlay Errors
|
|
255
|
+
sekmesine de düşer.
|
|
250
256
|
- **Build çıktısı:** manifest'teki her varlığın ham/gzip/brotli boyutu, ve
|
|
251
257
|
esbuild metafile'ından chunk analizi — her çıktının boyutu, hangi kaynaklardan
|
|
252
258
|
oluştuğu, hangi chunk'ları import ettiği. Kaynaklar okunur gruplara indirgenir
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -181,9 +181,13 @@ the page's CSS.
|
|
|
181
181
|
What it shows:
|
|
182
182
|
|
|
183
183
|
- **Errors:** browser-side JS errors, resource loading errors
|
|
184
|
-
(`img`/`script`/`link`),
|
|
185
|
-
output
|
|
186
|
-
the
|
|
184
|
+
(`img`/`script`/`link`), the server's `console.error` / `console.warn`
|
|
185
|
+
output, and failed SSR / browser `fetch` calls (4xx/5xx or network). Each
|
|
186
|
+
record carries a **page path**, **API URL**, and (on the client) an **island
|
|
187
|
+
name** when known; the response body opens under **show details** as JSON
|
|
188
|
+
instead of `[object Object]`. On the server side `console` is wrapped so
|
|
189
|
+
warnings do not get lost in the terminal; upstream failures still land in the
|
|
190
|
+
overlay even when the app's own logger writes them only to stderr.
|
|
187
191
|
- **SEO:** a client-side scan of the current page — title and meta description
|
|
188
192
|
length, `html lang`, viewport, canonical, robots/`noindex`, Open Graph and
|
|
189
193
|
Twitter tags, H1/outline, image `alt`, empty links, and JSON-LD parse errors.
|
|
@@ -248,9 +252,10 @@ Its contents:
|
|
|
248
252
|
status of the SSR output. Pages that were never visited but were warmed are
|
|
249
253
|
listed too: the SSR side is known, the client measurements stay empty.
|
|
250
254
|
- **Server API calls:** outbound `fetch` calls made during SSR — URL, host,
|
|
251
|
-
method, status, duration, bytes
|
|
252
|
-
|
|
253
|
-
(warming, health check) do
|
|
255
|
+
method, status, duration, bytes, which page was rendering, and a body summary
|
|
256
|
+
on failures. `globalThis.fetch` is only wrapped in development; the production
|
|
257
|
+
path is left untouched. Requests to our own server (warming, health check) do
|
|
258
|
+
not count as API calls. Failures also appear on the overlay Errors tab.
|
|
254
259
|
- **Build output:** the raw/gzip/brotli size of every asset in the manifest, and
|
|
255
260
|
chunk analysis from esbuild's metafile — the size of each output, which
|
|
256
261
|
sources it is made of, which chunks it imports. Sources are reduced to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -13,6 +13,7 @@ import { createHash } from "node:crypto";
|
|
|
13
13
|
import express from "express";
|
|
14
14
|
import * as log from "../../log.mjs";
|
|
15
15
|
import { FRAMEWORK_ROOT, getConfig } from "../../config/index.js";
|
|
16
|
+
import { getRequestContext } from "../../http/request-context.js";
|
|
16
17
|
import { prewarm, prewarmProgress } from "../prewarm.js";
|
|
17
18
|
import { clearHtmlCache } from "../html-cache.js";
|
|
18
19
|
import {
|
|
@@ -55,7 +56,21 @@ const STATE_FILE = path.join(
|
|
|
55
56
|
/** @type {{ id: number, method: string, url: string, status: number, ms: number, cache: string | null, at: number }[]} */
|
|
56
57
|
let requests = [];
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* id: number,
|
|
62
|
+
* level: string,
|
|
63
|
+
* message: string,
|
|
64
|
+
* stack: string | null,
|
|
65
|
+
* url: string | null,
|
|
66
|
+
* page: string | null,
|
|
67
|
+
* island: string | null,
|
|
68
|
+
* details: unknown,
|
|
69
|
+
* at: number,
|
|
70
|
+
* }} ServerError
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/** @type {ServerError[]} */
|
|
59
74
|
let errors = [];
|
|
60
75
|
|
|
61
76
|
let nextId = 1;
|
|
@@ -103,7 +118,13 @@ function trim(list) {
|
|
|
103
118
|
/**
|
|
104
119
|
* @param {string} level
|
|
105
120
|
* @param {string} message
|
|
106
|
-
* @param {{
|
|
121
|
+
* @param {{
|
|
122
|
+
* stack?: string | null,
|
|
123
|
+
* url?: string | null,
|
|
124
|
+
* page?: string | null,
|
|
125
|
+
* island?: string | null,
|
|
126
|
+
* details?: unknown,
|
|
127
|
+
* }} [extra]
|
|
107
128
|
*/
|
|
108
129
|
export function recordServerError(level, message, extra = {}) {
|
|
109
130
|
errors.push({
|
|
@@ -112,6 +133,9 @@ export function recordServerError(level, message, extra = {}) {
|
|
|
112
133
|
message,
|
|
113
134
|
stack: extra.stack ?? null,
|
|
114
135
|
url: extra.url ?? null,
|
|
136
|
+
page: extra.page ?? null,
|
|
137
|
+
island: extra.island ?? null,
|
|
138
|
+
details: extra.details ?? null,
|
|
115
139
|
at: Date.now(),
|
|
116
140
|
});
|
|
117
141
|
trim(errors);
|
|
@@ -121,21 +145,49 @@ export function recordServerError(level, message, extra = {}) {
|
|
|
121
145
|
|
|
122
146
|
/**
|
|
123
147
|
* `console.error` / `console.warn` çıktısını da overlay'e taşır: sunucudaki
|
|
124
|
-
* uyarılar terminalde kaybolmasın.
|
|
148
|
+
* uyarılar terminalde kaybolmasın. Render bağlamındaysa sayfa yolu da yazılır.
|
|
125
149
|
*/
|
|
126
150
|
function patchConsole() {
|
|
127
151
|
for (const level of /** @type {const} */ (["error", "warn"])) {
|
|
128
152
|
const original = console[level].bind(console);
|
|
129
153
|
console[level] = (...args) => {
|
|
130
154
|
const error = args.find((arg) => arg instanceof Error);
|
|
155
|
+
const page = currentPage();
|
|
131
156
|
recordServerError(level, args.map(format).join(" "), {
|
|
132
157
|
stack: error?.stack ?? null,
|
|
158
|
+
page,
|
|
159
|
+
url: page,
|
|
160
|
+
details: extractDetails(args),
|
|
133
161
|
});
|
|
134
162
|
original(...args);
|
|
135
163
|
};
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
|
|
167
|
+
/** @returns {string | null} */
|
|
168
|
+
function currentPage() {
|
|
169
|
+
return getRequestContext()?.pathname ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* console argümanlarından yapılandırılmış bir `details` alanı ayıklar.
|
|
174
|
+
* Uygulama `console.error("msg", { details: {...} })` yazdığında overlay
|
|
175
|
+
* nesneyi `[object Object]` yerine açılabilir JSON olarak görsün.
|
|
176
|
+
*
|
|
177
|
+
* @param {unknown[]} args
|
|
178
|
+
* @returns {unknown}
|
|
179
|
+
*/
|
|
180
|
+
function extractDetails(args) {
|
|
181
|
+
for (const arg of args) {
|
|
182
|
+
if (!arg || typeof arg !== "object" || arg instanceof Error) continue;
|
|
183
|
+
const record = /** @type {Record<string, unknown>} */ (arg);
|
|
184
|
+
if ("details" in record) return record.details;
|
|
185
|
+
if ("detail" in record) return record.detail;
|
|
186
|
+
return record;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
139
191
|
/**
|
|
140
192
|
* @param {unknown} value
|
|
141
193
|
* @returns {string}
|
|
@@ -144,12 +196,73 @@ function format(value) {
|
|
|
144
196
|
if (typeof value === "string") return value;
|
|
145
197
|
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
146
198
|
try {
|
|
147
|
-
return JSON.stringify(value)
|
|
199
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
200
|
+
// Döngüsel referanslarda stringify zaten fırlar; burada yalnızca
|
|
201
|
+
// Error örneklerini okunabilir kılmak yeterli.
|
|
202
|
+
if (nested instanceof Error) {
|
|
203
|
+
return { name: nested.name, message: nested.message };
|
|
204
|
+
}
|
|
205
|
+
return nested;
|
|
206
|
+
});
|
|
148
207
|
} catch {
|
|
149
208
|
return String(value);
|
|
150
209
|
}
|
|
151
210
|
}
|
|
152
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Upstream `fetch` başarısızlığını overlay hata listesine yazar.
|
|
214
|
+
*
|
|
215
|
+
* @param {{
|
|
216
|
+
* url: string,
|
|
217
|
+
* method: string,
|
|
218
|
+
* status: number,
|
|
219
|
+
* ms: number,
|
|
220
|
+
* bytes: number,
|
|
221
|
+
* error: string | null,
|
|
222
|
+
* page: string | null,
|
|
223
|
+
* details: unknown,
|
|
224
|
+
* }} call
|
|
225
|
+
*/
|
|
226
|
+
function recordApiFailure(call) {
|
|
227
|
+
// Aynı SSR turunda hem fetch sarmalayıcısı hem uygulama logger'ı aynı
|
|
228
|
+
// hatayı basabiliyor; kısa pencerede tekilleştir.
|
|
229
|
+
const last = errors.at(-1);
|
|
230
|
+
if (
|
|
231
|
+
last &&
|
|
232
|
+
last.url === call.url &&
|
|
233
|
+
last.page === call.page &&
|
|
234
|
+
Date.now() - last.at < 2000
|
|
235
|
+
) {
|
|
236
|
+
// İlk kayıtta details yoksa sonrakinin gövdesini birleştir.
|
|
237
|
+
if (last.details == null && call.details != null) {
|
|
238
|
+
last.details = call.details;
|
|
239
|
+
if (call.error && !last.message.includes(call.error)) {
|
|
240
|
+
last.message = `${call.method} ${shortApiPath(call.url)} → ${call.error}`;
|
|
241
|
+
}
|
|
242
|
+
persist();
|
|
243
|
+
pushStats();
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
recordServerError("error", `${call.method} ${shortApiPath(call.url)} → ${call.error ?? call.status}`, {
|
|
249
|
+
url: call.url,
|
|
250
|
+
page: call.page,
|
|
251
|
+
details: call.details,
|
|
252
|
+
stack: null,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @param {string} url */
|
|
257
|
+
function shortApiPath(url) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = new URL(url);
|
|
260
|
+
return parsed.pathname + parsed.search;
|
|
261
|
+
} catch {
|
|
262
|
+
return url;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
153
266
|
/** Her HTML isteğinin süresini ve cache durumunu kaydeder. */
|
|
154
267
|
function timing() {
|
|
155
268
|
/** @type {import('express').RequestHandler} */
|
|
@@ -448,7 +561,7 @@ export function mountDevtools(app) {
|
|
|
448
561
|
|
|
449
562
|
restore();
|
|
450
563
|
patchConsole();
|
|
451
|
-
trackServerFetch();
|
|
564
|
+
trackServerFetch({ onFailure: recordApiFailure });
|
|
452
565
|
watchManifest();
|
|
453
566
|
startVersionCheck();
|
|
454
567
|
startHeartbeat();
|
package/src/server/dev/report.js
CHANGED
|
@@ -18,6 +18,7 @@ import { getRedisStatus } from "../redis.js";
|
|
|
18
18
|
import { getUpstreamLimiterStatus } from "../upstream-limiter.js";
|
|
19
19
|
import { prewarmProgress } from "../prewarm.js";
|
|
20
20
|
import { getConfig } from "../../config/index.js";
|
|
21
|
+
import { getRequestContext } from "../../http/request-context.js";
|
|
21
22
|
|
|
22
23
|
/** Yollar config'ten: framework paket içine taşındığında `../..` sayan her hesap bozulur. */
|
|
23
24
|
const ROOT = getConfig().root;
|
|
@@ -78,61 +79,176 @@ export function clearPageReports() {
|
|
|
78
79
|
/* --------------------------------------------------- sunucu tarafı API çağrıları */
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
* @
|
|
82
|
-
*
|
|
82
|
+
* @typedef {{
|
|
83
|
+
* url: string,
|
|
84
|
+
* host: string,
|
|
85
|
+
* method: string,
|
|
86
|
+
* status: number,
|
|
87
|
+
* ms: number,
|
|
88
|
+
* bytes: number,
|
|
89
|
+
* at: number,
|
|
90
|
+
* error: string | null,
|
|
91
|
+
* page: string | null,
|
|
92
|
+
* details: unknown,
|
|
93
|
+
* }} ServerApiCall
|
|
83
94
|
*/
|
|
95
|
+
|
|
96
|
+
/** @type {ServerApiCall[]} */
|
|
84
97
|
const serverApiCalls = [];
|
|
85
98
|
const MAX_API_CALLS = 300;
|
|
86
99
|
|
|
100
|
+
/** Başarısız çağrı gövdesinin overlay'e taşınacak üst sınırı. */
|
|
101
|
+
const DETAILS_MAX = 4_000;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @typedef {{
|
|
105
|
+
* url: string,
|
|
106
|
+
* method: string,
|
|
107
|
+
* status: number,
|
|
108
|
+
* ms: number,
|
|
109
|
+
* bytes: number,
|
|
110
|
+
* error: string | null,
|
|
111
|
+
* page: string | null,
|
|
112
|
+
* details: unknown,
|
|
113
|
+
* }} ApiFailure
|
|
114
|
+
*/
|
|
115
|
+
|
|
87
116
|
/**
|
|
88
117
|
* SSR sırasında yapılan dış çağrıları ölçer. `globalThis.fetch` sarılır;
|
|
89
118
|
* yalnızca dev'de çağrıldığı için üretim yolu dokunulmaz kalır.
|
|
119
|
+
*
|
|
120
|
+
* Başarısız cevaplar (4xx/5xx ya da ağ) isteğe bağlı `onFailure` ile
|
|
121
|
+
* overlay hata günlüğüne de düşer — uygulama kendi logger'ıyla stderr'e
|
|
122
|
+
* yazsa bile panel "hangi sayfa hangi API" bilgisini görsün.
|
|
123
|
+
*
|
|
124
|
+
* @param {{ onFailure?: (call: ApiFailure) => void }} [options]
|
|
90
125
|
*/
|
|
91
|
-
export function trackServerFetch() {
|
|
126
|
+
export function trackServerFetch(options = {}) {
|
|
92
127
|
const original = globalThis.fetch;
|
|
93
|
-
if (original.__jskeletWrapped) return;
|
|
128
|
+
if (/** @type {any} */ (original).__jskeletWrapped) return;
|
|
129
|
+
|
|
130
|
+
const { onFailure } = options;
|
|
94
131
|
|
|
95
132
|
/** @type {typeof fetch} */
|
|
96
133
|
const wrapped = async (input, init) => {
|
|
97
134
|
const url = typeof input === "string" ? input : (input?.url ?? String(input));
|
|
135
|
+
const method = String(init?.method ?? "GET").toUpperCase();
|
|
98
136
|
const started = Date.now();
|
|
137
|
+
const page = currentPage();
|
|
99
138
|
|
|
100
139
|
// Kendi sunucumuza yapılan istekler (ısıtma, sağlık kontrolü) API sayılmaz.
|
|
101
|
-
const isSelf = /^https?:\/\/(127\.0\.0\.1|localhost)/i.test(url);
|
|
140
|
+
const isSelf = /^https?:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:|\/|$)/i.test(url);
|
|
102
141
|
|
|
103
142
|
try {
|
|
104
143
|
const response = await original(input, init);
|
|
105
144
|
if (!isSelf) {
|
|
106
|
-
|
|
145
|
+
const details = response.ok
|
|
146
|
+
? null
|
|
147
|
+
: await readFailureDetails(response);
|
|
148
|
+
const error = response.ok ? null : summarizeFailure(response.status, details);
|
|
149
|
+
const call = {
|
|
107
150
|
url,
|
|
108
|
-
method
|
|
151
|
+
method,
|
|
109
152
|
status: response.status,
|
|
110
153
|
ms: Date.now() - started,
|
|
111
154
|
bytes: Number(response.headers.get("content-length") ?? 0),
|
|
112
|
-
error
|
|
113
|
-
|
|
155
|
+
error,
|
|
156
|
+
page,
|
|
157
|
+
details,
|
|
158
|
+
};
|
|
159
|
+
push(call);
|
|
160
|
+
if (error) onFailure?.(call);
|
|
114
161
|
}
|
|
115
162
|
return response;
|
|
116
163
|
} catch (error) {
|
|
117
164
|
if (!isSelf) {
|
|
118
|
-
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
const call = {
|
|
119
167
|
url,
|
|
120
|
-
method
|
|
168
|
+
method,
|
|
121
169
|
status: 0,
|
|
122
170
|
ms: Date.now() - started,
|
|
123
171
|
bytes: 0,
|
|
124
|
-
error:
|
|
125
|
-
|
|
172
|
+
error: message,
|
|
173
|
+
page,
|
|
174
|
+
details: null,
|
|
175
|
+
};
|
|
176
|
+
push(call);
|
|
177
|
+
onFailure?.(call);
|
|
126
178
|
}
|
|
127
179
|
throw error;
|
|
128
180
|
}
|
|
129
181
|
};
|
|
130
182
|
|
|
131
|
-
wrapped.__jskeletWrapped = true;
|
|
183
|
+
/** @type {any} */ (wrapped).__jskeletWrapped = true;
|
|
132
184
|
globalThis.fetch = wrapped;
|
|
133
185
|
}
|
|
134
186
|
|
|
135
|
-
/** @
|
|
187
|
+
/** @returns {string | null} */
|
|
188
|
+
function currentPage() {
|
|
189
|
+
return getRequestContext()?.pathname ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {Response} response
|
|
194
|
+
* @returns {Promise<unknown>}
|
|
195
|
+
*/
|
|
196
|
+
async function readFailureDetails(response) {
|
|
197
|
+
try {
|
|
198
|
+
const text = (await response.clone().text()).slice(0, DETAILS_MAX);
|
|
199
|
+
if (!text) return null;
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(text);
|
|
202
|
+
} catch {
|
|
203
|
+
return text;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Overlay başlığı: durum + varsa API'nin kendi doğrulama mesajı.
|
|
212
|
+
*
|
|
213
|
+
* @param {number} status
|
|
214
|
+
* @param {unknown} details
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
function summarizeFailure(status, details) {
|
|
218
|
+
const label = `HTTP ${status}`;
|
|
219
|
+
const detail = failureMessage(details);
|
|
220
|
+
return detail ? `${label}: ${detail}` : label;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Upstream gövdesinden kısa, okunabilir bir cümle çıkarır. `[object Object]`
|
|
225
|
+
* basmamak için nesneleri bilinçli dolaşır.
|
|
226
|
+
*
|
|
227
|
+
* @param {unknown} details
|
|
228
|
+
* @returns {string | null}
|
|
229
|
+
*/
|
|
230
|
+
function failureMessage(details) {
|
|
231
|
+
if (details == null) return null;
|
|
232
|
+
if (typeof details === "string") return details.slice(0, 280);
|
|
233
|
+
if (typeof details !== "object") return String(details);
|
|
234
|
+
|
|
235
|
+
const record = /** @type {Record<string, unknown>} */ (details);
|
|
236
|
+
for (const key of ["details", "detail", "message", "error", "title"]) {
|
|
237
|
+
const value = record[key];
|
|
238
|
+
if (typeof value === "string" && value.trim()) return value.slice(0, 280);
|
|
239
|
+
if (value && typeof value === "object") {
|
|
240
|
+
const nested = failureMessage(value);
|
|
241
|
+
if (nested) return nested;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return JSON.stringify(details).slice(0, 280);
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @param {Omit<ServerApiCall, "host" | "at">} call */
|
|
136
252
|
function push(call) {
|
|
137
253
|
let host = "—";
|
|
138
254
|
try {
|
package/src/server/render.js
CHANGED
|
@@ -304,7 +304,11 @@ export function route(controller, options = {}) {
|
|
|
304
304
|
const isPrivate = options.private === true;
|
|
305
305
|
|
|
306
306
|
return async (req, res, next) => {
|
|
307
|
-
const context = createRequestContext({
|
|
307
|
+
const context = createRequestContext({
|
|
308
|
+
private: isPrivate,
|
|
309
|
+
res,
|
|
310
|
+
pathname: req.path,
|
|
311
|
+
});
|
|
308
312
|
const ctx = {
|
|
309
313
|
params: req.params ?? {},
|
|
310
314
|
query: req.query ?? {},
|
|
@@ -442,7 +446,11 @@ export function route(controller, options = {}) {
|
|
|
442
446
|
*/
|
|
443
447
|
export function fragment(controller) {
|
|
444
448
|
return async (req, res, next) => {
|
|
445
|
-
const context = createRequestContext({
|
|
449
|
+
const context = createRequestContext({
|
|
450
|
+
private: true,
|
|
451
|
+
res,
|
|
452
|
+
pathname: req.path,
|
|
453
|
+
});
|
|
446
454
|
const ctx = {
|
|
447
455
|
params: req.params ?? {},
|
|
448
456
|
query: req.query ?? {},
|