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 +10 -4
- package/docs/07-yapilandirma.md +1 -1
- package/docs/08-build.md +9 -2
- package/docs/09-dev-araclari.md +3 -3
- package/docs/en/07-configuration.md +1 -1
- package/docs/en/08-build.md +9 -2
- package/docs/en/09-dev-tools.md +4 -3
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +98 -15
- package/src/server/assets.js +24 -6
- package/src/server/dev/report.js +28 -0
- package/src/views/helpers/tags.js +6 -1
- package/types/server/assets.d.ts +12 -1
- package/types/server/dev/report.d.ts +3 -0
- package/types/views/helpers/tags.d.ts +5 -0
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 (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -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
|
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
|
|
@@ -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). |
|
package/docs/en/08-build.md
CHANGED
|
@@ -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
|
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.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:
|
|
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/assets.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
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>}
|
|
@@ -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)) {
|
package/types/server/assets.d.ts
CHANGED
|
@@ -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}
|