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 (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.
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.
@@ -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
- 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.
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
@@ -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 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.
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.13",
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: inferIsland(),
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
- * Çağrı yığınında `islands/ad` izi varsa ya da bir island mount edilirken
523
- * yakalanmışsa ada bağlar.
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 inferIsland() {
549
+ function inferComponent() {
528
550
  const mounting = globalThis.__JSKELET_MOUNTING_ISLAND;
529
551
  if (typeof mounting === "string" && mounting) return mounting;
530
552
 
531
- const stack = new Error().stack ?? "";
532
- const match = stack.match(/islands[/\\]([A-Za-z0-9_-]+)/);
533
- return match?.[1] ?? null;
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
- * island: string | null,
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
- island: call.island,
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
- island: null,
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 method, host, timing and response details.`
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">${escapeHtml(call.side)}${call.island ? ` · ${escapeHtml(call.island)}` : ""}</span>
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();
@@ -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
  */