jskelet 0.6.13 → 0.6.15

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
@@ -17,12 +17,18 @@ one is listed under a **Breaking** heading.
17
17
 
18
18
  ### Added
19
19
 
20
+ - `asset(name, { origin: "document" })` skips `build.assets.prefix` and
21
+ returns the site-origin `/assets/…` path. `icon()` always resolves the
22
+ sprite this way so SVG `<use href>` keeps working when other assets are
23
+ on a CDN (the origin still serves `public/assets/`).
20
24
  - Dev overlay **API** tab (between Performance and Server): outbound
21
25
  `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.
26
+ rollup, duration, status, and a detail pane on click (component,
27
+ URL, timing, size, response body when captured). Browser calls are
28
+ attributed to the island/module (mount, stack, or last interaction);
29
+ SSR calls to the first `lib/` / `routes/` / `controllers/` / `views/`
30
+ frame. The live stats channel includes recent server API calls so the
31
+ tab updates without a refresh.
26
32
  - Optional CDN asset publishing via `build.assets`: `prefix` rewrites
27
33
  `asset()` / optimised image URLs (manifest stays relative on disk);
28
34
  `upload.bucket` / `keyPrefix` / `region` / `endpoint` come from config only.
@@ -629,7 +629,7 @@ değişmez** — asset'ler origin'den servis edilir.
629
629
 
630
630
  | Alan | Tip | Varsayılan | Anlamı |
631
631
  | --- | --- | --- | --- |
632
- | `assets.prefix` | `string` | `""` | CDN origin; `asset()` URL öneki (sonunda `/` yok). Yalnızca config. Manifest diskte göreli kalır. |
632
+ | `assets.prefix` | `string` | `""` | CDN origin; `asset()` URL öneki (sonunda `/` yok). Yalnızca config. Manifest diskte göreli kalır. `asset(name, { origin: "document" })` ve `icon()` sprite'ı öneksiz kalır. |
633
633
  | `assets.upload.enabled` | `boolean` | `false` | `true` veya config'te `bucket` doluysa açılır; açık `false` ezer. |
634
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). |
package/docs/08-build.md CHANGED
@@ -80,6 +80,10 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
80
80
  - `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
81
81
  `build.assets.prefix` doluysa bu göreli yolun önüne CDN origin eklenir
82
82
  (manifest diskte göreli kalır).
83
+ - `asset(name, { origin: "document" })` prefix'i atlar; site origin'indeki
84
+ `/assets/…` yolunu döner. SVG `<use href>` çapraz origin sprite
85
+ yüklemediği için `icon()` sprite'ı her zaman böyle çözer — origin hâlâ
86
+ `public/assets/` servis eder.
83
87
  - `hasAsset(name)` manifest'te olup olmadığını söyler.
84
88
 
85
89
  ### CDN prefix ve S3 upload
@@ -91,7 +95,8 @@ env → ECS task role → EC2 IMDS zincirinden. Açılınca:
91
95
  altındaki dosyaları S3'e PutObject ile kopyalar.
92
96
  2. Çalışma anında `asset()` / optimize görseller `build.assets.prefix` ile
93
97
  absolute CDN URL basar. Entry CDN'deyse esbuild chunk'ları göreli yolla aynı
94
- host'a gider.
98
+ host'a gider. `icon()` sprite'ı ve `asset(name, { origin: "document" })`
99
+ site origin'inde kalır.
95
100
 
96
101
  Bucket adı noktalı olabilir (`static.ornek.com`); `https://…` şeması
97
102
  `prefix`'e yazılır. Config sözlüğü: [07-yapilandirma.md](./07-yapilandirma.md#build).
@@ -457,7 +462,9 @@ karşılaşmaması.
457
462
  - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
458
463
  dizinde yazılmışlar.
459
464
  - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
460
- uyarısına bakın.
465
+ uyarısına bakın. CDN prefix açıkken sprite bilerek site origin'inde kalır
466
+ (`asset("sprite.svg", { origin: "document" })`); origin `public/assets/`
467
+ servis etmiyorsa `<use>` boş kalır.
461
468
  - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
462
469
  da build atlanmış) veya bir build hatası var.
463
470
  - **Dev'de sayfa aniden stilsiz kaldı.** Manifest ile diskteki dosya
@@ -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
@@ -642,7 +642,7 @@ changes** — assets stay on the origin.
642
642
 
643
643
  | Field | Type | Default | Meaning |
644
644
  | --- | --- | --- | --- |
645
- | `assets.prefix` | `string` | `""` | CDN origin; prefix for `asset()` URLs (no trailing `/`). Config only. The on-disk manifest stays relative. |
645
+ | `assets.prefix` | `string` | `""` | CDN origin; prefix for `asset()` URLs (no trailing `/`). Config only. The on-disk manifest stays relative. `asset(name, { origin: "document" })` and the `icon()` sprite stay unprefixed. |
646
646
  | `assets.upload.enabled` | `boolean` | `false` | Turns on when `true` or when `bucket` is set in config; explicit `false` wins. |
647
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). |
@@ -83,6 +83,10 @@ They are passed to templates automatically; in server code,
83
83
  - `asset(name)` returns the hashed URL if it is in the manifest, otherwise
84
84
  `/assets/<name>`. When `build.assets.prefix` is set, that relative path is
85
85
  prefixed with the CDN origin (the on-disk manifest stays relative).
86
+ - `asset(name, { origin: "document" })` skips the prefix and returns the
87
+ site-origin `/assets/…` path. SVG `<use href>` cannot load a cross-origin
88
+ sprite, so `icon()` always resolves the sprite this way — the origin still
89
+ serves `public/assets/`.
86
90
  - `hasAsset(name)` tells you whether it is in the manifest.
87
91
 
88
92
  ### CDN prefix and S3 upload
@@ -94,7 +98,8 @@ resolve from env → ECS task role → EC2 IMDS. When enabled:
94
98
  directories in `include`) to S3.
95
99
  2. At runtime `asset()` / optimised images emit absolute CDN URLs via
96
100
  `build.assets.prefix`. When the entry script is on the CDN, esbuild chunks
97
- resolve relatively on the same host.
101
+ resolve relatively on the same host. The `icon()` sprite and
102
+ `asset(name, { origin: "document" })` stay on the document origin.
98
103
 
99
104
  Bucket names may include dots (`static.example.com`); put `https://…` in
100
105
  `prefix`. Config reference: [07-configuration.md](./07-configuration.md#build).
@@ -473,7 +478,9 @@ directly does not end up facing an unstyled page.
473
478
  - **Some Tailwind classes do not work.** They were written in a directory whose
474
479
  `@source` directive is missing.
475
480
  - **An icon looks empty.** That symbol is not in the sprite; in dev, look for the
476
- `[icon] missing from sprite` warning.
481
+ `[icon] missing from sprite` warning. With a CDN prefix the sprite stays on
482
+ the document origin on purpose (`asset("sprite.svg", { origin: "document" })`);
483
+ if the origin does not serve `public/assets/`, `<use>` stays blank.
477
484
  - **The islands never open.** `main.js` is not in the manifest (the entry
478
485
  directory is empty or the build was skipped) or there is a build error.
479
486
  - **The page suddenly went unstyled in dev.** The manifest and the file on disk
@@ -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.15",
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();
@@ -9,6 +9,11 @@
9
9
  * `build.assets.prefix` doluysa HTML'e basılan URL'lere CDN origin eklenir;
10
10
  * manifest diskte göreli kalır. Dosya sistemi okumaları (`getSpriteIds`)
11
11
  * göreli yolu kullanır.
12
+ *
13
+ * Bazı varlıklar bilinçli olarak site origin'inde kalır: SVG `<use href>`
14
+ * çapraz origin sprite yüklemez. `asset(name, { origin: "document" })`
15
+ * prefix'i atlar; `icon()` sprite için bunu kullanır. Origin hâlâ
16
+ * `public/assets/` servis eder.
12
17
  */
13
18
  import fs from "node:fs";
14
19
  import path from "node:path";
@@ -17,7 +22,7 @@ import { getConfig } from "../config/index.js";
17
22
 
18
23
  const isDev = process.env.NODE_ENV === "development";
19
24
 
20
- /** @type {{ manifest: Record<string, string> } | null} */
25
+ /** @type {{ manifest: Record<string, string>, generated: string } | null} */
21
26
  let cached = null;
22
27
  let warned = false;
23
28
 
@@ -38,13 +43,15 @@ function load() {
38
43
  }
39
44
  }
40
45
 
41
- return { manifest };
46
+ return { manifest, generated };
42
47
  }
43
48
 
44
49
  function state() {
45
50
  // Dev'de her istekte yeniden okunur (watch build hash'leri değiştirir);
46
- // prod'da bir kez.
47
- if (isDev || !cached) cached = load();
51
+ // prod'da bir kez. `generated` yolu değiştiyse (test / başka kök) de
52
+ // yenilenir — aksi hâlde eski manifest takılı kalır.
53
+ const generated = getConfig().dirs.generated;
54
+ if (isDev || !cached || cached.generated !== generated) cached = load();
48
55
  return cached;
49
56
  }
50
57
 
@@ -82,11 +89,22 @@ function assetPath(name) {
82
89
  }
83
90
 
84
91
  /**
92
+ * @typedef {{ origin?: "cdn" | "document" }} AssetOptions
93
+ */
94
+
95
+ /**
96
+ * Manifest'teki hash'li (veya yedek) URL.
97
+ *
85
98
  * @param {string} name Örn. "app.css", "main.js", "sprite.svg"
99
+ * @param {AssetOptions} [options]
100
+ * `origin: "document"` → `build.assets.prefix` uygulanmaz; site
101
+ * origin'indeki `/assets/…` yolu döner (varsayılan `"cdn"`).
86
102
  * @returns {string}
87
103
  */
88
- export function asset(name) {
89
- return applyAssetPrefix(assetPrefix(), assetPath(name));
104
+ export function asset(name, options = {}) {
105
+ const relative = assetPath(name);
106
+ if (options.origin === "document") return relative;
107
+ return applyAssetPrefix(assetPrefix(), relative);
90
108
  }
91
109
 
92
110
  /**
@@ -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>}
@@ -195,6 +195,11 @@ export function preloadImage({ href, imagesrcset, imagesizes }) {
195
195
  /**
196
196
  * Phosphor ikonları — build zamanı üretilen SVG sprite'tan `<use>`.
197
197
  * `weight` sprite id'sine dahildir (regular/bold/fill).
198
+ *
199
+ * Sprite URL'si her zaman site origin'indedir (`asset(..., { origin:
200
+ * "document" })`). `<use href>` çapraz origin SVG yüklemez; CDN prefix
201
+ * açıkken bile `public/assets/sprite.<hash>.svg` aynı host'tan gelir.
202
+ *
198
203
  * @param {{ name: string, size?: number | string, weight?: string,
199
204
  * class?: string, attrs?: Record<string, unknown> }} props
200
205
  * @returns {string}
@@ -208,7 +213,7 @@ export function icon(props) {
208
213
  attrs: extra = {},
209
214
  } = props;
210
215
 
211
- const spritePath = asset("sprite.svg");
216
+ const spritePath = asset("sprite.svg", { origin: "document" });
212
217
  const id = `${toKebab(name)}-${weight}`;
213
218
 
214
219
  if (isDev && !warnedIcons.has(id)) {
@@ -6,11 +6,22 @@
6
6
  * @returns {string}
7
7
  */
8
8
  export declare function applyAssetPrefix(prefix: string, url: string): string;
9
+ export type AssetOptions = {
10
+ origin?: "cdn" | "document";
11
+ };
12
+ /**
13
+ * @typedef {{ origin?: "cdn" | "document" }} AssetOptions
14
+ */
9
15
  /**
16
+ * Manifest'teki hash'li (veya yedek) URL.
17
+ *
10
18
  * @param {string} name Örn. "app.css", "main.js", "sprite.svg"
19
+ * @param {AssetOptions} [options]
20
+ * `origin: "document"` → `build.assets.prefix` uygulanmaz; site
21
+ * origin'indeki `/assets/…` yolu döner (varsayılan `"cdn"`).
11
22
  * @returns {string}
12
23
  */
13
- export declare function asset(name: string): string;
24
+ export declare function asset(name: string, options?: AssetOptions): string;
14
25
  /**
15
26
  * @param {string} name
16
27
  * @returns {boolean}
@@ -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
  */
@@ -60,6 +60,11 @@ export declare function preloadImage({ href, imagesrcset, imagesizes }: {
60
60
  /**
61
61
  * Phosphor ikonları — build zamanı üretilen SVG sprite'tan `<use>`.
62
62
  * `weight` sprite id'sine dahildir (regular/bold/fill).
63
+ *
64
+ * Sprite URL'si her zaman site origin'indedir (`asset(..., { origin:
65
+ * "document" })`). `<use href>` çapraz origin SVG yüklemez; CDN prefix
66
+ * açıkken bile `public/assets/sprite.<hash>.svg` aynı host'tan gelir.
67
+ *
63
68
  * @param {{ name: string, size?: number | string, weight?: string,
64
69
  * class?: string, attrs?: Record<string, unknown> }} props
65
70
  * @returns {string}