jskelet 0.6.12 → 0.6.13
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 +13 -0
- package/docs/07-yapilandirma.md +1 -1
- package/docs/09-dev-araclari.md +6 -0
- package/docs/en/07-configuration.md +1 -1
- package/docs/en/09-dev-tools.md +5 -0
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +357 -20
- package/src/server/dev/devtools.js +9 -1
- package/src/server/dev/report.js +20 -2
- package/src/server/logs/s3-put.js +52 -15
- package/types/server/dev/report.d.ts +15 -1
- package/types/server/logs/s3-put.d.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,8 +8,21 @@ one is listed under a **Breaking** heading.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- S3 PutObject uses path-style URLs for bucket names that contain dots
|
|
14
|
+
(e.g. `static.investvio.dev`). Virtual-hosted
|
|
15
|
+
`bucket.s3.region.amazonaws.com` fails TLS because the cert only covers
|
|
16
|
+
`*.s3.region.amazonaws.com` (one label). Fetch errors are also shortened.
|
|
17
|
+
|
|
11
18
|
### Added
|
|
12
19
|
|
|
20
|
+
- Dev overlay **API** tab (between Performance and Server): outbound
|
|
21
|
+
`fetch` calls for the current page — browser and SSR — with host
|
|
22
|
+
rollup, duration, status, and a detail pane on click (URL, timing,
|
|
23
|
+
size, island, response body when captured). The live stats channel
|
|
24
|
+
now includes recent server API calls so the tab updates without a
|
|
25
|
+
refresh.
|
|
13
26
|
- Optional CDN asset publishing via `build.assets`: `prefix` rewrites
|
|
14
27
|
`asset()` / optimised image URLs (manifest stays relative on disk);
|
|
15
28
|
`upload.bucket` / `keyPrefix` / `region` / `endpoint` come from config only.
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -631,7 +631,7 @@ değişmez** — asset'ler origin'den servis edilir.
|
|
|
631
631
|
| --- | --- | --- | --- |
|
|
632
632
|
| `assets.prefix` | `string` | `""` | CDN origin; `asset()` URL öneki (sonunda `/` yok). Yalnızca config. Manifest diskte göreli kalır. |
|
|
633
633
|
| `assets.upload.enabled` | `boolean` | `false` | `true` veya config'te `bucket` doluysa açılır; açık `false` ezer. |
|
|
634
|
-
| `assets.upload.bucket` | `string \| null` | `null` | **S3 bucket adı** (`static.ornek.com` gibi noktalı adlar geçerli) veya `bucket/key/prefix`. `https://…` şeması kabul edilmez — o `prefix`. |
|
|
634
|
+
| `assets.upload.bucket` | `string \| null` | `null` | **S3 bucket adı** (`static.ornek.com` gibi noktalı adlar geçerli; PutObject path-style kullanır) veya `bucket/key/prefix`. `https://…` şeması kabul edilmez — o `prefix`. |
|
|
635
635
|
| `assets.upload.keyPrefix` | `string` | `""` | Nesne anahtarı öneki; bucket yolunda verilmişse o kazanır (`prefix` takma adı da kabul). |
|
|
636
636
|
| `assets.upload.region` | `string` | `us-east-1` | Config; custom `endpoint` varken varsayılan `auto`. |
|
|
637
637
|
| `assets.upload.endpoint` | `string \| null` | `null` | S3-uyumlu API (R2 / MinIO); config. |
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -196,6 +196,12 @@ Gösterdikleri:
|
|
|
196
196
|
dikdörtgenler çizilir, kenarda kısa başlık durur. Etikete (ya da panel
|
|
197
197
|
satırına) tıklanınca ayrıntılı açıklama açılır. Tarama overlay'in yanındaki
|
|
198
198
|
`/seo.js` dosyasındadır; production paketine girmez.
|
|
199
|
+
- **API:** açık sayfanın dışarı giden `fetch` çağrıları — tarayıcıdaki
|
|
200
|
+
island/veri istekleri ve SSR sırasında yapılan upstream çağrıları.
|
|
201
|
+
Host kırılımı, toplam/ortalama süre, durum kodu; satıra tıklanınca
|
|
202
|
+
yöntem, tam URL, boyut, island ve (başarısızsa) yanıt gövdesi. Canlı
|
|
203
|
+
kanal `serverApi` listesini de taşır, böylece SSR çağrıları yenileme
|
|
204
|
+
olmadan görünür.
|
|
199
205
|
- **İstekler:** her HTML isteğinin metodu, yolu, durumu, süresi ve
|
|
200
206
|
`X-JSkelet-Cache` değeri. Aynı satırlar terminale de basılır.
|
|
201
207
|
- **Web Vitals:** TTFB, FCP, LCP, CLS, INP, DCL, load, uzun task sayısı ve
|
|
@@ -644,7 +644,7 @@ changes** — assets stay on the origin.
|
|
|
644
644
|
| --- | --- | --- | --- |
|
|
645
645
|
| `assets.prefix` | `string` | `""` | CDN origin; prefix for `asset()` URLs (no trailing `/`). Config only. The on-disk manifest stays relative. |
|
|
646
646
|
| `assets.upload.enabled` | `boolean` | `false` | Turns on when `true` or when `bucket` is set in config; explicit `false` wins. |
|
|
647
|
-
| `assets.upload.bucket` | `string \| null` | `null` | **S3 bucket name** (dotted names like `static.example.com` are fine) or `bucket/key/prefix`. A `https://…` scheme is rejected — that belongs in `prefix`. |
|
|
647
|
+
| `assets.upload.bucket` | `string \| null` | `null` | **S3 bucket name** (dotted names like `static.example.com` are fine; PutObject uses path-style) or `bucket/key/prefix`. A `https://…` scheme is rejected — that belongs in `prefix`. |
|
|
648
648
|
| `assets.upload.keyPrefix` | `string` | `""` | Object key prefix; a path on the bucket wins (`prefix` is accepted as an alias). |
|
|
649
649
|
| `assets.upload.region` | `string` | `us-east-1` | Config; defaults to `auto` when `endpoint` is set. |
|
|
650
650
|
| `assets.upload.endpoint` | `string \| null` | `null` | S3-compatible API (R2 / MinIO); config. |
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -201,6 +201,11 @@ What it shows:
|
|
|
201
201
|
title on the border. Clicking the label (or a row in the panel) opens the
|
|
202
202
|
full explanation. The scan lives in `/seo.js` next to the overlay and is not
|
|
203
203
|
part of the production bundle.
|
|
204
|
+
- **API:** outbound `fetch` calls for the open page — browser island/data
|
|
205
|
+
requests and upstream calls made during SSR. Host rollup, total/average
|
|
206
|
+
duration, status codes; click a row for method, full URL, size, island
|
|
207
|
+
and (on failure) the response body. The live channel also carries the
|
|
208
|
+
`serverApi` list so SSR calls appear without a refresh.
|
|
204
209
|
- **Requests:** the method, path, status, duration and `X-JSkelet-Cache` value
|
|
205
210
|
of every HTML request. The same lines are printed to the terminal too.
|
|
206
211
|
- **Web Vitals:** TTFB, FCP, LCP, CLS, INP, DCL, load, long task count and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dev overlay: sağ altta yüzen baloncuk; hataları, performans
|
|
3
|
-
* istatistiklerini ve SEO taramasını gösterir.
|
|
3
|
+
* istatistiklerini, sayfa API çağrılarını ve SEO taramasını gösterir.
|
|
4
4
|
*
|
|
5
5
|
* Bu dosya **build'e dahil değildir**. `npm run dev` sırasında sunucu onu
|
|
6
6
|
* `/__jskelet/dev/overlay.js` altından ham olarak servis eder, layout da
|
|
@@ -46,6 +46,9 @@ let expanded = new Set();
|
|
|
46
46
|
let seoHighlight = false;
|
|
47
47
|
/** Panel listesinde açılan sorun açıklaması. */
|
|
48
48
|
let seoExpandedId = null;
|
|
49
|
+
|
|
50
|
+
/** API sekmesinde açılan çağrı; `side:id` biçiminde. */
|
|
51
|
+
let selectedApiKey = null;
|
|
49
52
|
/** @type {import('./seo.js').SeoIssue[]} */
|
|
50
53
|
let seoIssues = [];
|
|
51
54
|
const seoLayer = new SeoHighlighter();
|
|
@@ -323,14 +326,34 @@ const islandSummary = cached(() => ({
|
|
|
323
326
|
|
|
324
327
|
/* --------------------------------------------------------- API çağrıları */
|
|
325
328
|
|
|
326
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* @typedef {{
|
|
331
|
+
* id: number,
|
|
332
|
+
* url: string,
|
|
333
|
+
* method: string,
|
|
334
|
+
* ms: number,
|
|
335
|
+
* status: number,
|
|
336
|
+
* bytes: number,
|
|
337
|
+
* contentType: string | null,
|
|
338
|
+
* initiator: string,
|
|
339
|
+
* island: string | null,
|
|
340
|
+
* page: string,
|
|
341
|
+
* at: number,
|
|
342
|
+
* error: string | null,
|
|
343
|
+
* details: unknown,
|
|
344
|
+
* }} ClientApiCall
|
|
345
|
+
*/
|
|
346
|
+
|
|
347
|
+
/** @type {ClientApiCall[]} */
|
|
327
348
|
const clientApi = [];
|
|
349
|
+
let nextApiId = 1;
|
|
328
350
|
|
|
329
351
|
/**
|
|
330
352
|
* Tarayıcıdaki veri çağrıları. Panelin kendi uçları ve statik varlıklar
|
|
331
353
|
* sayılmaz; amaç sayfanın hangi API'lere kaç ms harcadığını görmek.
|
|
332
354
|
* 4xx/5xx ve ağ hataları Errors sekmesine de düşer — hangi sayfa / island
|
|
333
|
-
* bağlamı mümkün olduğunca doldurulur.
|
|
355
|
+
* bağlamı mümkün olduğunca doldurulur. Aynı kayıtlar API sekmesinde de
|
|
356
|
+
* listelenir.
|
|
334
357
|
*/
|
|
335
358
|
function captureFetch() {
|
|
336
359
|
const original = window.fetch.bind(window);
|
|
@@ -343,9 +366,10 @@ function captureFetch() {
|
|
|
343
366
|
const started = performance.now();
|
|
344
367
|
try {
|
|
345
368
|
const response = await original(input, init);
|
|
346
|
-
record(url, performance.now() - started, response.status, response);
|
|
369
|
+
const entry = record(url, method, performance.now() - started, response.status, response);
|
|
347
370
|
if (!response.ok) {
|
|
348
371
|
void reportFailedFetch({
|
|
372
|
+
id: entry.id,
|
|
349
373
|
url,
|
|
350
374
|
method,
|
|
351
375
|
status: response.status,
|
|
@@ -354,7 +378,9 @@ function captureFetch() {
|
|
|
354
378
|
}
|
|
355
379
|
return response;
|
|
356
380
|
} catch (error) {
|
|
357
|
-
record(url, performance.now() - started, 0, null);
|
|
381
|
+
const entry = record(url, method, performance.now() - started, 0, null);
|
|
382
|
+
entry.error = "network error";
|
|
383
|
+
entry.details = error instanceof Error ? error.message : String(error);
|
|
358
384
|
pushError(
|
|
359
385
|
"error",
|
|
360
386
|
`${method} ${shortUrl(url)} → network error`,
|
|
@@ -362,9 +388,9 @@ function captureFetch() {
|
|
|
362
388
|
source: "fetch",
|
|
363
389
|
url,
|
|
364
390
|
page: location.pathname + location.search,
|
|
365
|
-
island:
|
|
391
|
+
island: entry.island,
|
|
366
392
|
stack: error instanceof Error ? error.stack : null,
|
|
367
|
-
details:
|
|
393
|
+
details: entry.details,
|
|
368
394
|
},
|
|
369
395
|
);
|
|
370
396
|
throw error;
|
|
@@ -373,31 +399,56 @@ function captureFetch() {
|
|
|
373
399
|
|
|
374
400
|
/**
|
|
375
401
|
* @param {string} url
|
|
402
|
+
* @param {string} method
|
|
376
403
|
* @param {number} ms
|
|
377
404
|
* @param {number} status
|
|
378
405
|
* @param {Response | null} response
|
|
406
|
+
* @returns {ClientApiCall}
|
|
379
407
|
*/
|
|
380
|
-
function record(url, ms, status, response) {
|
|
381
|
-
|
|
408
|
+
function record(url, method, ms, status, response) {
|
|
409
|
+
/** @type {ClientApiCall} */
|
|
410
|
+
const entry = {
|
|
411
|
+
id: nextApiId++,
|
|
382
412
|
url,
|
|
413
|
+
method,
|
|
383
414
|
ms: Math.round(ms),
|
|
384
415
|
status,
|
|
385
416
|
bytes: Number(response?.headers.get("content-length") ?? 0),
|
|
386
|
-
|
|
387
|
-
|
|
417
|
+
contentType: response?.headers.get("content-type") ?? null,
|
|
418
|
+
initiator: "client",
|
|
419
|
+
island: inferIsland(),
|
|
420
|
+
page: location.pathname + location.search,
|
|
421
|
+
at: Date.now(),
|
|
422
|
+
error: status === 0 || (status >= 400) ? `HTTP ${status || "network"}` : null,
|
|
423
|
+
details: null,
|
|
424
|
+
};
|
|
425
|
+
clientApi.push(entry);
|
|
388
426
|
if (clientApi.length > 100) clientApi.shift();
|
|
427
|
+
// API sekmesi açıkken yeni çağrı hemen görünsün.
|
|
428
|
+
lastBody = "";
|
|
429
|
+
render();
|
|
430
|
+
return entry;
|
|
389
431
|
}
|
|
390
432
|
}
|
|
391
433
|
|
|
392
434
|
/**
|
|
393
435
|
* Başarısız bir tarayıcı fetch'ini Errors listesine yazar. Gövdeyi klonlayıp
|
|
394
|
-
* okur; asıl Response tüketilmez.
|
|
436
|
+
* okur; asıl Response tüketilmez. Aynı gövde API satırının detayına da yazılır.
|
|
395
437
|
*
|
|
396
|
-
* @param {{ url: string, method: string, status: number, response: Response }} call
|
|
438
|
+
* @param {{ id: number, url: string, method: string, status: number, response: Response }} call
|
|
397
439
|
*/
|
|
398
440
|
async function reportFailedFetch(call) {
|
|
399
441
|
const details = await readResponseDetails(call.response);
|
|
400
442
|
const summary = detailSummary(details);
|
|
443
|
+
const entry = clientApi.find((item) => item.id === call.id);
|
|
444
|
+
if (entry) {
|
|
445
|
+
entry.details = details;
|
|
446
|
+
entry.error = summary
|
|
447
|
+
? `HTTP ${call.status}: ${summary}`
|
|
448
|
+
: `HTTP ${call.status}`;
|
|
449
|
+
lastBody = "";
|
|
450
|
+
render();
|
|
451
|
+
}
|
|
401
452
|
pushError(
|
|
402
453
|
"error",
|
|
403
454
|
`${call.method} ${shortUrl(call.url)} → ${call.status}${
|
|
@@ -407,7 +458,7 @@ async function reportFailedFetch(call) {
|
|
|
407
458
|
source: "fetch",
|
|
408
459
|
url: call.url,
|
|
409
460
|
page: location.pathname + location.search,
|
|
410
|
-
island: inferIsland(),
|
|
461
|
+
island: entry?.island ?? inferIsland(),
|
|
411
462
|
details,
|
|
412
463
|
},
|
|
413
464
|
);
|
|
@@ -734,6 +785,8 @@ async function startPrewarm(paths) {
|
|
|
734
785
|
|
|
735
786
|
async function clearAll() {
|
|
736
787
|
clientErrors.length = 0;
|
|
788
|
+
clientApi.length = 0;
|
|
789
|
+
selectedApiKey = null;
|
|
737
790
|
try {
|
|
738
791
|
await fetch(`${BASE}/clear`, { method: "POST" });
|
|
739
792
|
} catch {
|
|
@@ -1047,6 +1100,27 @@ h4 .head-action { float: inline-end; text-transform: none; letter-spacing: 0; }
|
|
|
1047
1100
|
.status.bad { background: rgba(251,113,133,.14); color: var(--bad); }
|
|
1048
1101
|
.hint { color: var(--muted); font-size: 10.5px; }
|
|
1049
1102
|
.row .good { color: var(--good); } .row .mid { color: var(--mid); } .row .bad { color: var(--bad); }
|
|
1103
|
+
.row.clickable { cursor: pointer; grid-template-columns: auto auto 1fr auto auto; }
|
|
1104
|
+
.row.clickable.selected { border-color: rgba(255,255,255,.22); background: var(--surface-hover); }
|
|
1105
|
+
.row .method {
|
|
1106
|
+
font-size: 10px; font-weight: 700; letter-spacing: .04em; color: #c4b5fd;
|
|
1107
|
+
min-width: 2.6em; text-align: center;
|
|
1108
|
+
}
|
|
1109
|
+
.api-detail {
|
|
1110
|
+
margin: 0 0 8px; padding: 12px 14px; border-radius: 12px;
|
|
1111
|
+
background: rgba(0,0,0,.35); border: 1px solid var(--line);
|
|
1112
|
+
font-size: 12px; line-height: 1.55;
|
|
1113
|
+
}
|
|
1114
|
+
.api-detail .kv { display: grid; grid-template-columns: 88px 1fr; gap: 6px 12px; margin: 0 0 10px; }
|
|
1115
|
+
.api-detail .kv dt { color: var(--muted); font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; }
|
|
1116
|
+
.api-detail .kv dd { margin: 0; color: #dbe2ea; word-break: break-all; font-family: ui-monospace, SFMono-Regular, monospace; font-size: 11.5px; }
|
|
1117
|
+
.api-detail .good { color: var(--good); } .api-detail .mid { color: var(--mid); } .api-detail .bad { color: var(--bad); }
|
|
1118
|
+
.api-detail pre {
|
|
1119
|
+
margin: 0; padding: 8px 10px; background: rgba(0,0,0,.4); border-radius: 8px;
|
|
1120
|
+
white-space: pre-wrap; word-break: break-word; color: #b8c0cb;
|
|
1121
|
+
font-size: 11px; font-family: ui-monospace, SFMono-Regular, monospace;
|
|
1122
|
+
max-height: 220px; overflow: auto;
|
|
1123
|
+
}
|
|
1050
1124
|
|
|
1051
1125
|
.btn {
|
|
1052
1126
|
background: var(--surface); border: 1px solid var(--line); color: var(--text);
|
|
@@ -1079,6 +1153,7 @@ const TAB_ICONS = {
|
|
|
1079
1153
|
errors: `<svg viewBox="0 0 16 16"><path d="M8 5.5v4M8 11.5h.01M8 1.8 1.8 13.2h12.4L8 1.8Z"/></svg>`,
|
|
1080
1154
|
seo: `<svg viewBox="0 0 16 16"><circle cx="7.2" cy="7.2" r="4.4"/><path d="M10.4 10.4 13.6 13.6M5.2 7.2h4M7.2 5.2v4"/></svg>`,
|
|
1081
1155
|
perf: `<svg viewBox="0 0 16 16"><path d="M2 13V8m4 5V3m4 10V6m4 7v-3"/></svg>`,
|
|
1156
|
+
api: `<svg viewBox="0 0 16 16"><path d="M2.5 8h3l1.5-3.5L9.5 12 11 8h2.5"/><circle cx="3.2" cy="8" r=".7"/><circle cx="12.8" cy="8" r=".7"/></svg>`,
|
|
1082
1157
|
server: `<svg viewBox="0 0 16 16"><path d="M2.5 3.5h11v3h-11zM2.5 9.5h11v3h-11zM5 5h.01M5 11h.01"/></svg>`,
|
|
1083
1158
|
warm: `<svg viewBox="0 0 16 16"><path d="M8 14c2.5 0 4.2-1.7 4.2-3.9C12.2 6.5 8 2 8 2S3.8 6.5 3.8 10.1C3.8 12.3 5.5 14 8 14Z"/></svg>`,
|
|
1084
1159
|
about: `<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.2"/><path d="M8 7.2v4M8 4.9h.01"/></svg>`,
|
|
@@ -1105,6 +1180,7 @@ const SKELETON = `
|
|
|
1105
1180
|
<button class="tab" data-tab="errors">${TAB_ICONS.errors} Errors<span class="chip" data-part="chip" hidden></span></button>
|
|
1106
1181
|
<button class="tab" data-tab="seo">${TAB_ICONS.seo} SEO<span class="chip" data-part="seo-chip" hidden></span></button>
|
|
1107
1182
|
<button class="tab" data-tab="perf">${TAB_ICONS.perf} Performance</button>
|
|
1183
|
+
<button class="tab" data-tab="api">${TAB_ICONS.api} API<span class="chip neutral" data-part="api-chip" hidden></span></button>
|
|
1108
1184
|
<button class="tab" data-tab="server">${TAB_ICONS.server} Server<span class="chip neutral" data-part="version-chip" hidden>update</span></button>
|
|
1109
1185
|
<button class="tab" data-tab="warm">${TAB_ICONS.warm} Prewarming<span class="chip neutral" data-part="warm-chip" hidden></span></button>
|
|
1110
1186
|
<div class="rail-foot">
|
|
@@ -1572,6 +1648,247 @@ function versionNotice() {
|
|
|
1572
1648
|
);
|
|
1573
1649
|
}
|
|
1574
1650
|
|
|
1651
|
+
/**
|
|
1652
|
+
* Bu sayfanın tarayıcı + SSR API çağrılarını birleştirir.
|
|
1653
|
+
*
|
|
1654
|
+
* @returns {{
|
|
1655
|
+
* key: string,
|
|
1656
|
+
* side: "client" | "server",
|
|
1657
|
+
* method: string,
|
|
1658
|
+
* url: string,
|
|
1659
|
+
* host: string,
|
|
1660
|
+
* status: number,
|
|
1661
|
+
* ms: number,
|
|
1662
|
+
* bytes: number,
|
|
1663
|
+
* contentType: string | null,
|
|
1664
|
+
* island: string | null,
|
|
1665
|
+
* page: string | null,
|
|
1666
|
+
* at: number,
|
|
1667
|
+
* error: string | null,
|
|
1668
|
+
* details: unknown,
|
|
1669
|
+
* }[]}
|
|
1670
|
+
*/
|
|
1671
|
+
function pageApiCalls() {
|
|
1672
|
+
const pagePath = location.pathname;
|
|
1673
|
+
|
|
1674
|
+
const client = clientApi.map((call) => {
|
|
1675
|
+
let host = location.host;
|
|
1676
|
+
try {
|
|
1677
|
+
host = new URL(call.url, location.origin).host;
|
|
1678
|
+
} catch {
|
|
1679
|
+
// göreli / bozuk URL
|
|
1680
|
+
}
|
|
1681
|
+
return {
|
|
1682
|
+
key: `client:${call.id}`,
|
|
1683
|
+
side: /** @type {const} */ ("client"),
|
|
1684
|
+
method: call.method,
|
|
1685
|
+
url: call.url,
|
|
1686
|
+
host,
|
|
1687
|
+
status: call.status,
|
|
1688
|
+
ms: call.ms,
|
|
1689
|
+
bytes: call.bytes,
|
|
1690
|
+
contentType: call.contentType,
|
|
1691
|
+
island: call.island,
|
|
1692
|
+
page: call.page,
|
|
1693
|
+
at: call.at,
|
|
1694
|
+
error: call.error,
|
|
1695
|
+
details: call.details,
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
const server = (serverStats?.serverApi ?? [])
|
|
1700
|
+
.filter((call) => {
|
|
1701
|
+
if (!call.page) return false;
|
|
1702
|
+
return call.page.split("?")[0] === pagePath;
|
|
1703
|
+
})
|
|
1704
|
+
.map((call, index) => ({
|
|
1705
|
+
key: `server:${call.at}:${index}`,
|
|
1706
|
+
side: /** @type {const} */ ("server"),
|
|
1707
|
+
method: call.method,
|
|
1708
|
+
url: call.url,
|
|
1709
|
+
host: call.host ?? "—",
|
|
1710
|
+
status: call.status,
|
|
1711
|
+
ms: Math.round(call.ms),
|
|
1712
|
+
bytes: call.bytes ?? 0,
|
|
1713
|
+
contentType: null,
|
|
1714
|
+
island: null,
|
|
1715
|
+
page: call.page,
|
|
1716
|
+
at: call.at,
|
|
1717
|
+
error: call.error,
|
|
1718
|
+
details: call.details,
|
|
1719
|
+
}));
|
|
1720
|
+
|
|
1721
|
+
return [...client, ...server].sort((a, b) => b.at - a.at);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function apiTab() {
|
|
1725
|
+
const calls = pageApiCalls();
|
|
1726
|
+
const failed = calls.filter((call) => call.error || call.status >= 400 || call.status === 0);
|
|
1727
|
+
const totalMs = calls.reduce((sum, call) => sum + call.ms, 0);
|
|
1728
|
+
const byHost = new Map();
|
|
1729
|
+
|
|
1730
|
+
for (const call of calls) {
|
|
1731
|
+
const bucket = byHost.get(call.host) ?? { count: 0, ms: 0 };
|
|
1732
|
+
bucket.count += 1;
|
|
1733
|
+
bucket.ms += call.ms;
|
|
1734
|
+
byHost.set(call.host, bucket);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const hosts = [...byHost.entries()]
|
|
1738
|
+
.sort((a, b) => b[1].ms - a[1].ms)
|
|
1739
|
+
.slice(0, 8);
|
|
1740
|
+
|
|
1741
|
+
clipboard.set(
|
|
1742
|
+
"api",
|
|
1743
|
+
calls
|
|
1744
|
+
.map(
|
|
1745
|
+
(call) =>
|
|
1746
|
+
`${new Date(call.at).toISOString()} [${call.side}] ${call.method} ${call.url} ${call.status} ${call.ms}ms${
|
|
1747
|
+
call.error ? ` ${call.error}` : ""
|
|
1748
|
+
}`,
|
|
1749
|
+
)
|
|
1750
|
+
.join("\n"),
|
|
1751
|
+
);
|
|
1752
|
+
|
|
1753
|
+
const selected = selectedApiKey
|
|
1754
|
+
? calls.find((call) => call.key === selectedApiKey) ?? null
|
|
1755
|
+
: null;
|
|
1756
|
+
|
|
1757
|
+
const tone = failed.length
|
|
1758
|
+
? "bad"
|
|
1759
|
+
: calls.some((call) => call.ms > 500)
|
|
1760
|
+
? "mid"
|
|
1761
|
+
: calls.length
|
|
1762
|
+
? "good"
|
|
1763
|
+
: "mid";
|
|
1764
|
+
|
|
1765
|
+
return `
|
|
1766
|
+
${lede(
|
|
1767
|
+
tone,
|
|
1768
|
+
calls.length
|
|
1769
|
+
? `This page made <strong>${calls.length} API call${calls.length === 1 ? "" : "s"}</strong> ` +
|
|
1770
|
+
`(${clientApi.length} in the browser, ${calls.length - clientApi.length} during SSR), ` +
|
|
1771
|
+
`taking <strong>${ms(totalMs)}</strong> in total` +
|
|
1772
|
+
`${failed.length ? ` · <strong>${failed.length} failed</strong>` : ""}. ` +
|
|
1773
|
+
`Click a row for method, host, timing and response details.`
|
|
1774
|
+
: "No outbound API calls recorded for this page yet. Browser <code>fetch</code> and SSR upstream calls show up here as they happen.",
|
|
1775
|
+
)}
|
|
1776
|
+
${
|
|
1777
|
+
calls.length
|
|
1778
|
+
? `${metricGrid([
|
|
1779
|
+
{ label: "Calls", value: String(calls.length) },
|
|
1780
|
+
{ label: "Total time", value: ms(totalMs), tone: grade(totalMs, [300, 1200]), ratio: ratio(totalMs, 1200) },
|
|
1781
|
+
{
|
|
1782
|
+
label: "Avg",
|
|
1783
|
+
value: ms(totalMs / calls.length),
|
|
1784
|
+
tone: grade(totalMs / calls.length, [150, 500]),
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
label: "Failed",
|
|
1788
|
+
value: String(failed.length),
|
|
1789
|
+
tone: failed.length ? "bad" : "good",
|
|
1790
|
+
},
|
|
1791
|
+
{ label: "Hosts", value: String(byHost.size) },
|
|
1792
|
+
{ label: "Browser", value: String(clientApi.length) },
|
|
1793
|
+
{ label: "SSR", value: String(calls.length - clientApi.length) },
|
|
1794
|
+
])}
|
|
1795
|
+
${
|
|
1796
|
+
hosts.length
|
|
1797
|
+
? `<h4>By host</h4>
|
|
1798
|
+
<div class="rows">${hosts
|
|
1799
|
+
.map(
|
|
1800
|
+
([host, info]) => `<div class="row">
|
|
1801
|
+
<span class="tag">${info.count}×</span>
|
|
1802
|
+
<span class="path">${escapeHtml(host)}</span>
|
|
1803
|
+
<span class="hint">${Math.round((info.ms / Math.max(totalMs, 1)) * 100)}%</span>
|
|
1804
|
+
<span class="${grade(info.ms / info.count, [150, 500])}">${ms(info.ms)}</span>
|
|
1805
|
+
</div>`,
|
|
1806
|
+
)
|
|
1807
|
+
.join("")}</div>`
|
|
1808
|
+
: ""
|
|
1809
|
+
}
|
|
1810
|
+
<h4>Calls<span class="head-action">${copyButton("api", "copy all")}</span></h4>
|
|
1811
|
+
${
|
|
1812
|
+
selected
|
|
1813
|
+
? apiDetailPanel(selected)
|
|
1814
|
+
: `<div class="empty" style="margin-bottom:8px">Select a call to inspect headers, timing and the response body when available.</div>`
|
|
1815
|
+
}
|
|
1816
|
+
<div class="rows">${calls
|
|
1817
|
+
.map((call) => {
|
|
1818
|
+
const bad = call.error || call.status >= 400 || call.status === 0;
|
|
1819
|
+
const warn = !bad && call.status >= 300;
|
|
1820
|
+
return `<div class="row clickable ${selectedApiKey === call.key ? "selected" : ""}" data-action="api-select" data-key="${escapeHtml(call.key)}">
|
|
1821
|
+
<span class="status ${bad ? "bad" : warn ? "warn" : ""}">${call.status || "—"}</span>
|
|
1822
|
+
<span class="method">${escapeHtml(call.method)}</span>
|
|
1823
|
+
<span class="path" title="${escapeHtml(call.url)}">${escapeHtml(shortUrl(call.url))}</span>
|
|
1824
|
+
<span class="hint">${escapeHtml(call.side)}${call.island ? ` · ${escapeHtml(call.island)}` : ""}</span>
|
|
1825
|
+
<span class="${grade(call.ms, [150, 500])}">${call.ms} ms</span>
|
|
1826
|
+
</div>`;
|
|
1827
|
+
})
|
|
1828
|
+
.join("")}</div>`
|
|
1829
|
+
: ""
|
|
1830
|
+
}
|
|
1831
|
+
`;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* @param {ReturnType<typeof pageApiCalls>[number]} call
|
|
1836
|
+
*/
|
|
1837
|
+
function apiDetailPanel(call) {
|
|
1838
|
+
const detailKey = `api-detail:${call.key}`;
|
|
1839
|
+
clipboard.set(
|
|
1840
|
+
detailKey,
|
|
1841
|
+
[
|
|
1842
|
+
`${call.method} ${call.url}`,
|
|
1843
|
+
`status: ${call.status}`,
|
|
1844
|
+
`ms: ${call.ms}`,
|
|
1845
|
+
`bytes: ${call.bytes}`,
|
|
1846
|
+
`side: ${call.side}`,
|
|
1847
|
+
call.host ? `host: ${call.host}` : "",
|
|
1848
|
+
call.page ? `page: ${call.page}` : "",
|
|
1849
|
+
call.island ? `island: ${call.island}` : "",
|
|
1850
|
+
call.contentType ? `content-type: ${call.contentType}` : "",
|
|
1851
|
+
call.error ? `error: ${call.error}` : "",
|
|
1852
|
+
call.details == null
|
|
1853
|
+
? ""
|
|
1854
|
+
: typeof call.details === "string"
|
|
1855
|
+
? call.details
|
|
1856
|
+
: safeJson(call.details),
|
|
1857
|
+
]
|
|
1858
|
+
.filter(Boolean)
|
|
1859
|
+
.join("\n"),
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
const detailsText = formatDetails(call.details);
|
|
1863
|
+
|
|
1864
|
+
return `<div class="api-detail">
|
|
1865
|
+
<div class="meta" style="display:flex;align-items:center;gap:8px;margin-bottom:10px;color:var(--muted);font-size:10.5px">
|
|
1866
|
+
<span class="tag">${escapeHtml(call.side)}</span>
|
|
1867
|
+
<span>${new Date(call.at).toLocaleTimeString()}</span>
|
|
1868
|
+
<span class="spacer"></span>
|
|
1869
|
+
${copyButton(detailKey, "copy")}
|
|
1870
|
+
<button class="link" data-action="api-select" data-key="${escapeHtml(call.key)}" style="margin:0">close</button>
|
|
1871
|
+
</div>
|
|
1872
|
+
<dl class="kv">
|
|
1873
|
+
<dt>Method</dt><dd>${escapeHtml(call.method)}</dd>
|
|
1874
|
+
<dt>URL</dt><dd>${escapeHtml(call.url)}</dd>
|
|
1875
|
+
<dt>Host</dt><dd>${escapeHtml(call.host)}</dd>
|
|
1876
|
+
<dt>Status</dt><dd class="${call.error || call.status >= 400 || call.status === 0 ? "bad" : call.status >= 300 ? "mid" : "good"}">${call.status || "—"}</dd>
|
|
1877
|
+
<dt>Duration</dt><dd class="${grade(call.ms, [150, 500])}">${call.ms} ms</dd>
|
|
1878
|
+
<dt>Size</dt><dd>${call.bytes ? kb(call.bytes) : "—"}</dd>
|
|
1879
|
+
${call.contentType ? `<dt>Type</dt><dd>${escapeHtml(call.contentType)}</dd>` : ""}
|
|
1880
|
+
${call.page ? `<dt>Page</dt><dd>${pathLink(call.page)}</dd>` : ""}
|
|
1881
|
+
${call.island ? `<dt>Island</dt><dd>${escapeHtml(call.island)}</dd>` : ""}
|
|
1882
|
+
${call.error ? `<dt>Error</dt><dd class="bad">${escapeHtml(call.error)}</dd>` : ""}
|
|
1883
|
+
</dl>
|
|
1884
|
+
${
|
|
1885
|
+
detailsText
|
|
1886
|
+
? `<h4 style="margin:0 0 8px">Response body</h4><pre>${escapeHtml(detailsText)}</pre>`
|
|
1887
|
+
: `<div class="hint">No response body captured${call.side === "client" && !call.error ? " (successful browser calls keep the body for the page)" : ""}.</div>`
|
|
1888
|
+
}
|
|
1889
|
+
</div>`;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1575
1892
|
function serverTab() {
|
|
1576
1893
|
if (!serverStats) return `<div class="empty">Waiting for server stats…</div>`;
|
|
1577
1894
|
|
|
@@ -1807,7 +2124,7 @@ const ABOUT = {
|
|
|
1807
2124
|
<div class="step"><span class="n">2</span><span><b>Islands</b><span>Every interactive piece is marked with <code>data-island</code> and its module loads once it enters the viewport. The whole page is never hydrated.</span></span></div>
|
|
1808
2125
|
<div class="step"><span class="n">3</span><span><b>Build pipeline</b><span>esbuild compiles JS, Tailwind + LightningCSS the styles, sharp the images, Phosphor the icons; the output is hashed and written to <code>manifest.json</code>.</span></span></div>
|
|
1809
2126
|
<div class="step"><span class="n">4</span><span><b>Dev loop</b><span>Sources are watched: on a CSS change the stylesheet is swapped, on a JS or server change a single full reload happens.</span></span></div>
|
|
1810
|
-
<div class="step"><span class="n">5</span><span><b>Observability</b><span>This panel collects errors, Web Vitals
|
|
2127
|
+
<div class="step"><span class="n">5</span><span><b>Observability</b><span>This panel collects errors, Web Vitals, outbound API calls and server requests; records survive a server restart.</span></span></div>
|
|
1811
2128
|
</div>
|
|
1812
2129
|
<div class="cards">
|
|
1813
2130
|
<div class="card"><b>Runtime</b><span>Node + Express 5, ESM. Page templates are EJS, the data layer is the services under <code>lib/</code>.</span></div>
|
|
@@ -2126,6 +2443,16 @@ function paint() {
|
|
|
2126
2443
|
versionChip.hidden = !version?.outdated;
|
|
2127
2444
|
if (version?.outdated) versionChip.title = `JSkelet ${version.latest} is available`;
|
|
2128
2445
|
|
|
2446
|
+
const apiCalls = pageApiCalls();
|
|
2447
|
+
const apiChip = root.querySelector("[data-part='api-chip']");
|
|
2448
|
+
apiChip.hidden = !apiCalls.length;
|
|
2449
|
+
apiChip.className = `chip ${
|
|
2450
|
+
apiCalls.some((call) => call.error || call.status >= 400 || call.status === 0)
|
|
2451
|
+
? ""
|
|
2452
|
+
: "neutral"
|
|
2453
|
+
}`;
|
|
2454
|
+
apiChip.textContent = String(apiCalls.length);
|
|
2455
|
+
|
|
2129
2456
|
const warm = serverStats?.prewarm;
|
|
2130
2457
|
const warmChip = root.querySelector("[data-part='warm-chip']");
|
|
2131
2458
|
warmChip.hidden = !warm?.total;
|
|
@@ -2149,11 +2476,13 @@ function paint() {
|
|
|
2149
2476
|
? seoTab()
|
|
2150
2477
|
: activeTab === "perf"
|
|
2151
2478
|
? perfTab()
|
|
2152
|
-
: activeTab === "
|
|
2153
|
-
?
|
|
2154
|
-
: activeTab === "
|
|
2155
|
-
?
|
|
2156
|
-
:
|
|
2479
|
+
: activeTab === "api"
|
|
2480
|
+
? apiTab()
|
|
2481
|
+
: activeTab === "about"
|
|
2482
|
+
? aboutTab()
|
|
2483
|
+
: activeTab === "warm"
|
|
2484
|
+
? warmTab()
|
|
2485
|
+
: serverTab();
|
|
2157
2486
|
|
|
2158
2487
|
if (html !== lastBody) {
|
|
2159
2488
|
const body = root.querySelector(".body");
|
|
@@ -2238,6 +2567,14 @@ function bind(root) {
|
|
|
2238
2567
|
return;
|
|
2239
2568
|
}
|
|
2240
2569
|
|
|
2570
|
+
if (target.dataset.action === "api-select") {
|
|
2571
|
+
const key = target.dataset.key ?? null;
|
|
2572
|
+
selectedApiKey = selectedApiKey === key ? null : key;
|
|
2573
|
+
lastBody = "";
|
|
2574
|
+
render();
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2241
2578
|
if (target.dataset.action === "toggle") {
|
|
2242
2579
|
setOpen(!open);
|
|
2243
2580
|
return;
|
|
@@ -19,6 +19,7 @@ import { clearHtmlCache } from "../html-cache.js";
|
|
|
19
19
|
import {
|
|
20
20
|
buildReport,
|
|
21
21
|
clearPageReports,
|
|
22
|
+
getServerApiCalls,
|
|
22
23
|
recordPageReport,
|
|
23
24
|
trackServerFetch,
|
|
24
25
|
} from "./report.js";
|
|
@@ -326,6 +327,9 @@ function statsPayload() {
|
|
|
326
327
|
prewarm: { ...prewarmProgress },
|
|
327
328
|
requests: requests.slice(-25).reverse(),
|
|
328
329
|
errors: errors.slice(-25).reverse(),
|
|
330
|
+
// SSR dış çağrıları: overlay API sekmesi hangi sayfanın hangi
|
|
331
|
+
// upstream'e kaç ms harcadığını buradan okur.
|
|
332
|
+
serverApi: getServerApiCalls(50),
|
|
329
333
|
};
|
|
330
334
|
}
|
|
331
335
|
|
|
@@ -565,7 +569,11 @@ export function mountDevtools(app) {
|
|
|
565
569
|
|
|
566
570
|
restore();
|
|
567
571
|
patchConsole();
|
|
568
|
-
trackServerFetch({
|
|
572
|
+
trackServerFetch({
|
|
573
|
+
onFailure: recordApiFailure,
|
|
574
|
+
// Başarılı çağrılar Errors'a düşmez ama API sekmesi onları da görmeli.
|
|
575
|
+
onCall: () => pushStats(),
|
|
576
|
+
});
|
|
569
577
|
watchManifest();
|
|
570
578
|
startVersionCheck();
|
|
571
579
|
startHeartbeat();
|
package/src/server/dev/report.js
CHANGED
|
@@ -121,13 +121,19 @@ const DETAILS_MAX = 4_000;
|
|
|
121
121
|
* overlay hata günlüğüne de düşer — uygulama kendi logger'ıyla stderr'e
|
|
122
122
|
* yazsa bile panel "hangi sayfa hangi API" bilgisini görsün.
|
|
123
123
|
*
|
|
124
|
-
*
|
|
124
|
+
* Her dış çağrı (başarılı ya da değil) `onCall` ile de bildirilir; overlay
|
|
125
|
+
* API sekmesi canlı kanal üzerinden güncellensin diye.
|
|
126
|
+
*
|
|
127
|
+
* @param {{
|
|
128
|
+
* onFailure?: (call: ApiFailure) => void,
|
|
129
|
+
* onCall?: (call: ApiFailure) => void,
|
|
130
|
+
* }} [options]
|
|
125
131
|
*/
|
|
126
132
|
export function trackServerFetch(options = {}) {
|
|
127
133
|
const original = globalThis.fetch;
|
|
128
134
|
if (/** @type {any} */ (original).__jskeletWrapped) return;
|
|
129
135
|
|
|
130
|
-
const { onFailure } = options;
|
|
136
|
+
const { onFailure, onCall } = options;
|
|
131
137
|
|
|
132
138
|
/** @type {typeof fetch} */
|
|
133
139
|
const wrapped = async (input, init) => {
|
|
@@ -157,6 +163,7 @@ export function trackServerFetch(options = {}) {
|
|
|
157
163
|
details,
|
|
158
164
|
};
|
|
159
165
|
push(call);
|
|
166
|
+
onCall?.(call);
|
|
160
167
|
if (error) onFailure?.(call);
|
|
161
168
|
}
|
|
162
169
|
return response;
|
|
@@ -174,6 +181,7 @@ export function trackServerFetch(options = {}) {
|
|
|
174
181
|
details: null,
|
|
175
182
|
};
|
|
176
183
|
push(call);
|
|
184
|
+
onCall?.(call);
|
|
177
185
|
onFailure?.(call);
|
|
178
186
|
}
|
|
179
187
|
throw error;
|
|
@@ -184,6 +192,16 @@ export function trackServerFetch(options = {}) {
|
|
|
184
192
|
globalThis.fetch = wrapped;
|
|
185
193
|
}
|
|
186
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Overlay ve istatistik uçları için son dış çağrıları döner (yeniden eskiye).
|
|
197
|
+
*
|
|
198
|
+
* @param {number} [limit]
|
|
199
|
+
* @returns {ServerApiCall[]}
|
|
200
|
+
*/
|
|
201
|
+
export function getServerApiCalls(limit = 50) {
|
|
202
|
+
return serverApiCalls.slice(-limit).reverse();
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
/** @returns {string | null} */
|
|
188
206
|
function currentPage() {
|
|
189
207
|
return getRequestContext()?.pathname ?? null;
|
|
@@ -116,6 +116,21 @@ export function encodeS3Path(value) {
|
|
|
116
116
|
.join("/");
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Virtual-hosted (`bucket.s3.region…`) TLS sertifikası `*.s3.region…` —
|
|
121
|
+
* yalnızca **tek** etiket. `static.investvio.dev` gibi noktalı bucket'lar
|
|
122
|
+
* `Host: static.investvio.dev.s3.…` üretir ve `ERR_TLS_CERT_ALTNAME_INVALID`
|
|
123
|
+
* verir. Path-style (`s3.region…/bucket/key`) bunu çözer.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} bucket
|
|
126
|
+
* @param {string | null | undefined} endpoint
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
export function useS3PathStyle(bucket, endpoint) {
|
|
130
|
+
if (endpoint) return true;
|
|
131
|
+
return bucket.includes(".");
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
/**
|
|
120
135
|
* PutObject isteğini imzalar; ağ çağrısı yapmaz — test edilebilir olsun diye.
|
|
121
136
|
*
|
|
@@ -130,13 +145,24 @@ export function buildSignedPutObject(input) {
|
|
|
130
145
|
const payloadHash = sha256Hex(body);
|
|
131
146
|
|
|
132
147
|
const keyPath = encodeS3Path(input.key.replace(/^\/+/, ""));
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
const pathStyle = useS3PathStyle(input.bucket, input.endpoint);
|
|
149
|
+
|
|
150
|
+
/** @type {string} */
|
|
151
|
+
let host;
|
|
152
|
+
/** @type {string} */
|
|
153
|
+
let base;
|
|
154
|
+
if (input.endpoint) {
|
|
155
|
+
base = String(input.endpoint).replace(/\/+$/, "");
|
|
156
|
+
host = new URL(input.endpoint).host;
|
|
157
|
+
} else if (pathStyle) {
|
|
158
|
+
host = `s3.${input.region}.amazonaws.com`;
|
|
159
|
+
base = `https://${host}`;
|
|
160
|
+
} else {
|
|
161
|
+
host = `${input.bucket}.s3.${input.region}.amazonaws.com`;
|
|
162
|
+
base = `https://${host}`;
|
|
163
|
+
}
|
|
138
164
|
|
|
139
|
-
const canonicalUri =
|
|
165
|
+
const canonicalUri = pathStyle
|
|
140
166
|
? `/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
141
167
|
: `/${keyPath}`;
|
|
142
168
|
|
|
@@ -186,10 +212,7 @@ export function buildSignedPutObject(input) {
|
|
|
186
212
|
`AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}, ` +
|
|
187
213
|
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
188
214
|
|
|
189
|
-
const
|
|
190
|
-
? String(input.endpoint).replace(/\/+$/, "")
|
|
191
|
-
: `https://${host}`;
|
|
192
|
-
const url = usePathStyle
|
|
215
|
+
const url = pathStyle
|
|
193
216
|
? `${base}/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
194
217
|
: `${base}/${keyPath}`;
|
|
195
218
|
|
|
@@ -202,11 +225,25 @@ export function buildSignedPutObject(input) {
|
|
|
202
225
|
*/
|
|
203
226
|
export async function putObject(input) {
|
|
204
227
|
const signed = buildSignedPutObject(input);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
228
|
+
/** @type {Response} */
|
|
229
|
+
let response;
|
|
230
|
+
try {
|
|
231
|
+
response = await fetch(signed.url, {
|
|
232
|
+
method: "PUT",
|
|
233
|
+
headers: signed.headers,
|
|
234
|
+
body: signed.body,
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const cause =
|
|
238
|
+
error && typeof error === "object" && "cause" in error && error.cause
|
|
239
|
+
? /** @type {{ message?: string, code?: string }} */ (error.cause)
|
|
240
|
+
: null;
|
|
241
|
+
const detail =
|
|
242
|
+
cause?.code ||
|
|
243
|
+
cause?.message ||
|
|
244
|
+
(error instanceof Error ? error.message : String(error));
|
|
245
|
+
throw new Error(`S3 PutObject fetch failed (${signed.url}): ${detail}`);
|
|
246
|
+
}
|
|
210
247
|
|
|
211
248
|
if (!response.ok) {
|
|
212
249
|
const text = await response.text().catch(() => "");
|
|
@@ -76,11 +76,25 @@ export type ApiFailure = {
|
|
|
76
76
|
* overlay hata günlüğüne de düşer — uygulama kendi logger'ıyla stderr'e
|
|
77
77
|
* yazsa bile panel "hangi sayfa hangi API" bilgisini görsün.
|
|
78
78
|
*
|
|
79
|
-
*
|
|
79
|
+
* Her dış çağrı (başarılı ya da değil) `onCall` ile de bildirilir; overlay
|
|
80
|
+
* API sekmesi canlı kanal üzerinden güncellensin diye.
|
|
81
|
+
*
|
|
82
|
+
* @param {{
|
|
83
|
+
* onFailure?: (call: ApiFailure) => void,
|
|
84
|
+
* onCall?: (call: ApiFailure) => void,
|
|
85
|
+
* }} [options]
|
|
80
86
|
*/
|
|
81
87
|
export declare function trackServerFetch(options?: {
|
|
82
88
|
onFailure?: (call: ApiFailure) => void;
|
|
89
|
+
onCall?: (call: ApiFailure) => void;
|
|
83
90
|
}): void;
|
|
91
|
+
/**
|
|
92
|
+
* Overlay ve istatistik uçları için son dış çağrıları döner (yeniden eskiye).
|
|
93
|
+
*
|
|
94
|
+
* @param {number} [limit]
|
|
95
|
+
* @returns {ServerApiCall[]}
|
|
96
|
+
*/
|
|
97
|
+
export declare function getServerApiCalls(limit?: number): ServerApiCall[];
|
|
84
98
|
/**
|
|
85
99
|
* Rapor sayfasının tek veri kaynağı.
|
|
86
100
|
* @param {{ requests: object[], errors: object[] }} devtools
|
|
@@ -69,6 +69,17 @@ export declare function buildStringToSign(parts: {
|
|
|
69
69
|
* @returns {string}
|
|
70
70
|
*/
|
|
71
71
|
export declare function encodeS3Path(value: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Virtual-hosted (`bucket.s3.region…`) TLS sertifikası `*.s3.region…` —
|
|
74
|
+
* yalnızca **tek** etiket. `static.investvio.dev` gibi noktalı bucket'lar
|
|
75
|
+
* `Host: static.investvio.dev.s3.…` üretir ve `ERR_TLS_CERT_ALTNAME_INVALID`
|
|
76
|
+
* verir. Path-style (`s3.region…/bucket/key`) bunu çözer.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} bucket
|
|
79
|
+
* @param {string | null | undefined} endpoint
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export declare function useS3PathStyle(bucket: string, endpoint: string | null | undefined): boolean;
|
|
72
83
|
/**
|
|
73
84
|
* PutObject isteğini imzalar; ağ çağrısı yapmaz — test edilebilir olsun diye.
|
|
74
85
|
*
|