jskelet 0.6.13 → 0.6.14
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
CHANGED
|
@@ -19,10 +19,12 @@ one is listed under a **Breaking** heading.
|
|
|
19
19
|
|
|
20
20
|
- Dev overlay **API** tab (between Performance and Server): outbound
|
|
21
21
|
`fetch` calls for the current page — browser and SSR — with host
|
|
22
|
-
rollup, duration, status, and a detail pane on click (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
rollup, duration, status, and a detail pane on click (component,
|
|
23
|
+
URL, timing, size, response body when captured). Browser calls are
|
|
24
|
+
attributed to the island/module (mount, stack, or last interaction);
|
|
25
|
+
SSR calls to the first `lib/` / `routes/` / `controllers/` / `views/`
|
|
26
|
+
frame. The live stats channel includes recent server API calls so the
|
|
27
|
+
tab updates without a refresh.
|
|
26
28
|
- Optional CDN asset publishing via `build.assets`: `prefix` rewrites
|
|
27
29
|
`asset()` / optimised image URLs (manifest stays relative on disk);
|
|
28
30
|
`upload.bucket` / `keyPrefix` / `region` / `endpoint` come from config only.
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -199,9 +199,9 @@ Gösterdikleri:
|
|
|
199
199
|
- **API:** açık sayfanın dışarı giden `fetch` çağrıları — tarayıcıdaki
|
|
200
200
|
island/veri istekleri ve SSR sırasında yapılan upstream çağrıları.
|
|
201
201
|
Host kırılımı, toplam/ortalama süre, durum kodu; satıra tıklanınca
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
olmadan görünür.
|
|
202
|
+
**component** (island adı veya `lib/` / `routes/` dosyası), yöntem,
|
|
203
|
+
tam URL, boyut ve (başarısızsa) yanıt gövdesi. Canlı kanal `serverApi`
|
|
204
|
+
listesini de taşır, böylece SSR çağrıları yenileme olmadan görünür.
|
|
205
205
|
- **İstekler:** her HTML isteğinin metodu, yolu, durumu, süresi ve
|
|
206
206
|
`X-JSkelet-Cache` değeri. Aynı satırlar terminale de basılır.
|
|
207
207
|
- **Web Vitals:** TTFB, FCP, LCP, CLS, INP, DCL, load, uzun task sayısı ve
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -203,9 +203,10 @@ What it shows:
|
|
|
203
203
|
part of the production bundle.
|
|
204
204
|
- **API:** outbound `fetch` calls for the open page — browser island/data
|
|
205
205
|
requests and upstream calls made during SSR. Host rollup, total/average
|
|
206
|
-
duration, status codes; click a row for
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
duration, status codes; click a row for the **component** (island name
|
|
207
|
+
or `lib/` / `routes/` file), method, full URL, size and (on failure)
|
|
208
|
+
the response body. The live channel also carries the `serverApi` list
|
|
209
|
+
so SSR calls appear without a refresh.
|
|
209
210
|
- **Requests:** the method, path, status, duration and `X-JSkelet-Cache` value
|
|
210
211
|
of every HTML request. The same lines are printed to the terminal too.
|
|
211
212
|
- **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.14",
|
|
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",
|
|
@@ -337,6 +337,7 @@ const islandSummary = cached(() => ({
|
|
|
337
337
|
* contentType: string | null,
|
|
338
338
|
* initiator: string,
|
|
339
339
|
* island: string | null,
|
|
340
|
+
* component: string | null,
|
|
340
341
|
* page: string,
|
|
341
342
|
* at: number,
|
|
342
343
|
* error: string | null,
|
|
@@ -348,6 +349,25 @@ const islandSummary = cached(() => ({
|
|
|
348
349
|
const clientApi = [];
|
|
349
350
|
let nextApiId = 1;
|
|
350
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Son etkileşilen island. Mount bittikten sonra event handler içindeki
|
|
354
|
+
* `fetch` yığında dosya adı her zaman görünmeyebiliyor; tıklanan island
|
|
355
|
+
* köke bakmak o boşluğu doldurur.
|
|
356
|
+
*/
|
|
357
|
+
function trackIslandInteraction() {
|
|
358
|
+
/** @param {Event} event */
|
|
359
|
+
const mark = (event) => {
|
|
360
|
+
const target = /** @type {Element | null} */ (event.target);
|
|
361
|
+
const host = target?.closest?.("[data-island]");
|
|
362
|
+
const name = host?.getAttribute?.("data-island");
|
|
363
|
+
if (name) globalThis.__JSKELET_LAST_ISLAND = name;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
for (const type of ["pointerdown", "click", "keydown", "submit", "change"]) {
|
|
367
|
+
document.addEventListener(type, mark, true);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
351
371
|
/**
|
|
352
372
|
* Tarayıcıdaki veri çağrıları. Panelin kendi uçları ve statik varlıklar
|
|
353
373
|
* sayılmaz; amaç sayfanın hangi API'lere kaç ms harcadığını görmek.
|
|
@@ -406,6 +426,7 @@ function captureFetch() {
|
|
|
406
426
|
* @returns {ClientApiCall}
|
|
407
427
|
*/
|
|
408
428
|
function record(url, method, ms, status, response) {
|
|
429
|
+
const component = inferComponent();
|
|
409
430
|
/** @type {ClientApiCall} */
|
|
410
431
|
const entry = {
|
|
411
432
|
id: nextApiId++,
|
|
@@ -416,7 +437,8 @@ function captureFetch() {
|
|
|
416
437
|
bytes: Number(response?.headers.get("content-length") ?? 0),
|
|
417
438
|
contentType: response?.headers.get("content-type") ?? null,
|
|
418
439
|
initiator: "client",
|
|
419
|
-
island:
|
|
440
|
+
island: component,
|
|
441
|
+
component,
|
|
420
442
|
page: location.pathname + location.search,
|
|
421
443
|
at: Date.now(),
|
|
422
444
|
error: status === 0 || (status >= 400) ? `HTTP ${status || "network"}` : null,
|
|
@@ -519,18 +541,59 @@ function shortUrl(url) {
|
|
|
519
541
|
}
|
|
520
542
|
|
|
521
543
|
/**
|
|
522
|
-
*
|
|
523
|
-
*
|
|
544
|
+
* Çağrıyı hangi island / client modülünden geldiğine bağlar.
|
|
545
|
+
* Sıra: mount işareti → yığın izi → son etkileşilen island.
|
|
524
546
|
*
|
|
525
547
|
* @returns {string | null}
|
|
526
548
|
*/
|
|
527
|
-
function
|
|
549
|
+
function inferComponent() {
|
|
528
550
|
const mounting = globalThis.__JSKELET_MOUNTING_ISLAND;
|
|
529
551
|
if (typeof mounting === "string" && mounting) return mounting;
|
|
530
552
|
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
553
|
+
const fromStack = componentFromClientStack(new Error().stack ?? "");
|
|
554
|
+
if (fromStack) return fromStack;
|
|
555
|
+
|
|
556
|
+
const last = globalThis.__JSKELET_LAST_ISLAND;
|
|
557
|
+
if (typeof last === "string" && last) return last;
|
|
558
|
+
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** @deprecated {@link inferComponent} */
|
|
563
|
+
function inferIsland() {
|
|
564
|
+
return inferComponent();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* @param {string} stack
|
|
569
|
+
* @returns {string | null}
|
|
570
|
+
*/
|
|
571
|
+
function componentFromClientStack(stack) {
|
|
572
|
+
const islandPath = stack.match(/islands[/\\]([A-Za-z0-9_-]+)/);
|
|
573
|
+
if (islandPath) return islandPath[1];
|
|
574
|
+
|
|
575
|
+
// Build çıktısı: /assets/islands/counter-a1b2c3.js
|
|
576
|
+
const assetIsland = stack.match(/[/\\]islands[/\\]([A-Za-z0-9_-]+?)(?:-[a-z0-9]+)?\.(?:js|mjs|ts|tsx)/i);
|
|
577
|
+
if (assetIsland) return assetIsland[1];
|
|
578
|
+
|
|
579
|
+
const clientPath = stack.match(/client[/\\]([A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+)/);
|
|
580
|
+
if (clientPath) return `client/${clientPath[1].replace(/\\/g, "/")}`;
|
|
581
|
+
|
|
582
|
+
// Kayıtlı island adları yığında (hash'li chunk URL'sinde) geçiyorsa.
|
|
583
|
+
for (const node of document.querySelectorAll("[data-island]")) {
|
|
584
|
+
const name = node.getAttribute("data-island");
|
|
585
|
+
if (!name || name.length < 2) continue;
|
|
586
|
+
if (
|
|
587
|
+
stack.includes(`/${name}.`) ||
|
|
588
|
+
stack.includes(`/${name}-`) ||
|
|
589
|
+
stack.includes(`islands/${name}`) ||
|
|
590
|
+
stack.includes(`islands%2F${name}`)
|
|
591
|
+
) {
|
|
592
|
+
return name;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return null;
|
|
534
597
|
}
|
|
535
598
|
|
|
536
599
|
/**
|
|
@@ -1661,7 +1724,7 @@ function versionNotice() {
|
|
|
1661
1724
|
* ms: number,
|
|
1662
1725
|
* bytes: number,
|
|
1663
1726
|
* contentType: string | null,
|
|
1664
|
-
*
|
|
1727
|
+
* component: string | null,
|
|
1665
1728
|
* page: string | null,
|
|
1666
1729
|
* at: number,
|
|
1667
1730
|
* error: string | null,
|
|
@@ -1688,7 +1751,7 @@ function pageApiCalls() {
|
|
|
1688
1751
|
ms: call.ms,
|
|
1689
1752
|
bytes: call.bytes,
|
|
1690
1753
|
contentType: call.contentType,
|
|
1691
|
-
|
|
1754
|
+
component: call.component ?? call.island,
|
|
1692
1755
|
page: call.page,
|
|
1693
1756
|
at: call.at,
|
|
1694
1757
|
error: call.error,
|
|
@@ -1711,7 +1774,7 @@ function pageApiCalls() {
|
|
|
1711
1774
|
ms: Math.round(call.ms),
|
|
1712
1775
|
bytes: call.bytes ?? 0,
|
|
1713
1776
|
contentType: null,
|
|
1714
|
-
|
|
1777
|
+
component: call.component ?? null,
|
|
1715
1778
|
page: call.page,
|
|
1716
1779
|
at: call.at,
|
|
1717
1780
|
error: call.error,
|
|
@@ -1743,7 +1806,7 @@ function apiTab() {
|
|
|
1743
1806
|
calls
|
|
1744
1807
|
.map(
|
|
1745
1808
|
(call) =>
|
|
1746
|
-
`${new Date(call.at).toISOString()} [${call.side}] ${call.method} ${call.url} ${call.status} ${call.ms}ms${
|
|
1809
|
+
`${new Date(call.at).toISOString()} [${call.side}] ${call.component ?? "—"} ${call.method} ${call.url} ${call.status} ${call.ms}ms${
|
|
1747
1810
|
call.error ? ` ${call.error}` : ""
|
|
1748
1811
|
}`,
|
|
1749
1812
|
)
|
|
@@ -1770,7 +1833,7 @@ function apiTab() {
|
|
|
1770
1833
|
`(${clientApi.length} in the browser, ${calls.length - clientApi.length} during SSR), ` +
|
|
1771
1834
|
`taking <strong>${ms(totalMs)}</strong> in total` +
|
|
1772
1835
|
`${failed.length ? ` · <strong>${failed.length} failed</strong>` : ""}. ` +
|
|
1773
|
-
`Click a row for
|
|
1836
|
+
`Click a row for the component, host, timing and response details.`
|
|
1774
1837
|
: "No outbound API calls recorded for this page yet. Browser <code>fetch</code> and SSR upstream calls show up here as they happen.",
|
|
1775
1838
|
)}
|
|
1776
1839
|
${
|
|
@@ -1821,7 +1884,11 @@ function apiTab() {
|
|
|
1821
1884
|
<span class="status ${bad ? "bad" : warn ? "warn" : ""}">${call.status || "—"}</span>
|
|
1822
1885
|
<span class="method">${escapeHtml(call.method)}</span>
|
|
1823
1886
|
<span class="path" title="${escapeHtml(call.url)}">${escapeHtml(shortUrl(call.url))}</span>
|
|
1824
|
-
<span class="hint"
|
|
1887
|
+
<span class="hint" title="${escapeHtml(call.component ?? call.side)}">${
|
|
1888
|
+
call.component
|
|
1889
|
+
? escapeHtml(shortComponent(call.component))
|
|
1890
|
+
: escapeHtml(call.side)
|
|
1891
|
+
}</span>
|
|
1825
1892
|
<span class="${grade(call.ms, [150, 500])}">${call.ms} ms</span>
|
|
1826
1893
|
</div>`;
|
|
1827
1894
|
})
|
|
@@ -1831,6 +1898,12 @@ function apiTab() {
|
|
|
1831
1898
|
`;
|
|
1832
1899
|
}
|
|
1833
1900
|
|
|
1901
|
+
/** @param {string} value */
|
|
1902
|
+
function shortComponent(value) {
|
|
1903
|
+
const parts = value.replace(/\\/g, "/").split("/");
|
|
1904
|
+
return parts.length > 2 ? parts.slice(-2).join("/") : value;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1834
1907
|
/**
|
|
1835
1908
|
* @param {ReturnType<typeof pageApiCalls>[number]} call
|
|
1836
1909
|
*/
|
|
@@ -1844,9 +1917,9 @@ function apiDetailPanel(call) {
|
|
|
1844
1917
|
`ms: ${call.ms}`,
|
|
1845
1918
|
`bytes: ${call.bytes}`,
|
|
1846
1919
|
`side: ${call.side}`,
|
|
1920
|
+
call.component ? `component: ${call.component}` : "",
|
|
1847
1921
|
call.host ? `host: ${call.host}` : "",
|
|
1848
1922
|
call.page ? `page: ${call.page}` : "",
|
|
1849
|
-
call.island ? `island: ${call.island}` : "",
|
|
1850
1923
|
call.contentType ? `content-type: ${call.contentType}` : "",
|
|
1851
1924
|
call.error ? `error: ${call.error}` : "",
|
|
1852
1925
|
call.details == null
|
|
@@ -1864,12 +1937,22 @@ function apiDetailPanel(call) {
|
|
|
1864
1937
|
return `<div class="api-detail">
|
|
1865
1938
|
<div class="meta" style="display:flex;align-items:center;gap:8px;margin-bottom:10px;color:var(--muted);font-size:10.5px">
|
|
1866
1939
|
<span class="tag">${escapeHtml(call.side)}</span>
|
|
1940
|
+
${
|
|
1941
|
+
call.component
|
|
1942
|
+
? `<span class="tag island" title="Component">${escapeHtml(call.component)}</span>`
|
|
1943
|
+
: ""
|
|
1944
|
+
}
|
|
1867
1945
|
<span>${new Date(call.at).toLocaleTimeString()}</span>
|
|
1868
1946
|
<span class="spacer"></span>
|
|
1869
1947
|
${copyButton(detailKey, "copy")}
|
|
1870
1948
|
<button class="link" data-action="api-select" data-key="${escapeHtml(call.key)}" style="margin:0">close</button>
|
|
1871
1949
|
</div>
|
|
1872
1950
|
<dl class="kv">
|
|
1951
|
+
<dt>Component</dt><dd>${
|
|
1952
|
+
call.component
|
|
1953
|
+
? escapeHtml(call.component)
|
|
1954
|
+
: `<span class="hint">unknown</span>`
|
|
1955
|
+
}</dd>
|
|
1873
1956
|
<dt>Method</dt><dd>${escapeHtml(call.method)}</dd>
|
|
1874
1957
|
<dt>URL</dt><dd>${escapeHtml(call.url)}</dd>
|
|
1875
1958
|
<dt>Host</dt><dd>${escapeHtml(call.host)}</dd>
|
|
@@ -1878,7 +1961,6 @@ function apiDetailPanel(call) {
|
|
|
1878
1961
|
<dt>Size</dt><dd>${call.bytes ? kb(call.bytes) : "—"}</dd>
|
|
1879
1962
|
${call.contentType ? `<dt>Type</dt><dd>${escapeHtml(call.contentType)}</dd>` : ""}
|
|
1880
1963
|
${call.page ? `<dt>Page</dt><dd>${pathLink(call.page)}</dd>` : ""}
|
|
1881
|
-
${call.island ? `<dt>Island</dt><dd>${escapeHtml(call.island)}</dd>` : ""}
|
|
1882
1964
|
${call.error ? `<dt>Error</dt><dd class="bad">${escapeHtml(call.error)}</dd>` : ""}
|
|
1883
1965
|
</dl>
|
|
1884
1966
|
${
|
|
@@ -2660,6 +2742,7 @@ function start() {
|
|
|
2660
2742
|
captureErrors();
|
|
2661
2743
|
captureMetrics();
|
|
2662
2744
|
captureFetch();
|
|
2745
|
+
trackIslandInteraction();
|
|
2663
2746
|
bind(ensureRoot());
|
|
2664
2747
|
refreshSeo();
|
|
2665
2748
|
render();
|
package/src/server/dev/report.js
CHANGED
|
@@ -89,6 +89,7 @@ export function clearPageReports() {
|
|
|
89
89
|
* at: number,
|
|
90
90
|
* error: string | null,
|
|
91
91
|
* page: string | null,
|
|
92
|
+
* component: string | null,
|
|
92
93
|
* details: unknown,
|
|
93
94
|
* }} ServerApiCall
|
|
94
95
|
*/
|
|
@@ -109,6 +110,7 @@ const DETAILS_MAX = 4_000;
|
|
|
109
110
|
* bytes: number,
|
|
110
111
|
* error: string | null,
|
|
111
112
|
* page: string | null,
|
|
113
|
+
* component: string | null,
|
|
112
114
|
* details: unknown,
|
|
113
115
|
* }} ApiFailure
|
|
114
116
|
*/
|
|
@@ -141,6 +143,8 @@ export function trackServerFetch(options = {}) {
|
|
|
141
143
|
const method = String(init?.method ?? "GET").toUpperCase();
|
|
142
144
|
const started = Date.now();
|
|
143
145
|
const page = currentPage();
|
|
146
|
+
// Yığını çağrı anında al: await sonrası stack sarmalayıcıda kalır.
|
|
147
|
+
const component = inferServerComponent();
|
|
144
148
|
|
|
145
149
|
// Kendi sunucumuza yapılan istekler (ısıtma, sağlık kontrolü) API sayılmaz.
|
|
146
150
|
const isSelf = /^https?:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:|\/|$)/i.test(url);
|
|
@@ -160,6 +164,7 @@ export function trackServerFetch(options = {}) {
|
|
|
160
164
|
bytes: Number(response.headers.get("content-length") ?? 0),
|
|
161
165
|
error,
|
|
162
166
|
page,
|
|
167
|
+
component,
|
|
163
168
|
details,
|
|
164
169
|
};
|
|
165
170
|
push(call);
|
|
@@ -178,6 +183,7 @@ export function trackServerFetch(options = {}) {
|
|
|
178
183
|
bytes: 0,
|
|
179
184
|
error: message,
|
|
180
185
|
page,
|
|
186
|
+
component,
|
|
181
187
|
details: null,
|
|
182
188
|
};
|
|
183
189
|
push(call);
|
|
@@ -207,6 +213,28 @@ function currentPage() {
|
|
|
207
213
|
return getRequestContext()?.pathname ?? null;
|
|
208
214
|
}
|
|
209
215
|
|
|
216
|
+
/**
|
|
217
|
+
* SSR `fetch` çağrısını uygulamanın hangi dosyasından geldiğine bağlar.
|
|
218
|
+
* Framework ve `node_modules` kareleri atlanır; `lib/`, `routes/`,
|
|
219
|
+
* `controllers/` veya `views/` altındaki ilk kare alınır.
|
|
220
|
+
*
|
|
221
|
+
* @returns {string | null}
|
|
222
|
+
*/
|
|
223
|
+
function inferServerComponent() {
|
|
224
|
+
const stack = new Error().stack ?? "";
|
|
225
|
+
for (const line of stack.split("\n")) {
|
|
226
|
+
if (/node_modules|server[/\\]dev[/\\]|node:internal/i.test(line)) continue;
|
|
227
|
+
|
|
228
|
+
const match = line.match(
|
|
229
|
+
/((?:lib|routes|controllers|views)[/\\][A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+)/i,
|
|
230
|
+
);
|
|
231
|
+
if (!match) continue;
|
|
232
|
+
|
|
233
|
+
return match[1].replace(/\\/g, "/");
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
/**
|
|
211
239
|
* @param {Response} response
|
|
212
240
|
* @returns {Promise<unknown>}
|
|
@@ -44,6 +44,7 @@ export type ServerApiCall = {
|
|
|
44
44
|
at: number;
|
|
45
45
|
error: string | null;
|
|
46
46
|
page: string | null;
|
|
47
|
+
component: string | null;
|
|
47
48
|
details: unknown;
|
|
48
49
|
};
|
|
49
50
|
export type ApiFailure = {
|
|
@@ -54,6 +55,7 @@ export type ApiFailure = {
|
|
|
54
55
|
bytes: number;
|
|
55
56
|
error: string | null;
|
|
56
57
|
page: string | null;
|
|
58
|
+
component: string | null;
|
|
57
59
|
details: unknown;
|
|
58
60
|
};
|
|
59
61
|
/**
|
|
@@ -65,6 +67,7 @@ export type ApiFailure = {
|
|
|
65
67
|
* bytes: number,
|
|
66
68
|
* error: string | null,
|
|
67
69
|
* page: string | null,
|
|
70
|
+
* component: string | null,
|
|
68
71
|
* details: unknown,
|
|
69
72
|
* }} ApiFailure
|
|
70
73
|
*/
|