jskelet 0.7.1 → 0.7.3
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 +41 -0
- package/docs/02-mimari.md +11 -3
- package/docs/04-render-ve-sablonlar.md +39 -9
- package/docs/05-islands.md +8 -4
- package/docs/06-cache.md +18 -16
- package/docs/07-yapilandirma.md +8 -7
- package/docs/08-build.md +5 -2
- package/docs/en/02-architecture.md +10 -3
- package/docs/en/04-rendering.md +44 -10
- package/docs/en/05-islands.md +8 -4
- package/docs/en/06-caching.md +18 -16
- package/docs/en/07-configuration.md +8 -6
- package/docs/en/08-build.md +7 -4
- package/package.json +1 -1
- package/src/build/build.mjs +23 -8
- package/src/build/tasks/css.mjs +2 -1
- package/src/build/tasks/precompress.mjs +51 -11
- package/src/client/registry.js +13 -3
- package/src/compile/compile-all.js +14 -8
- package/src/config/defaults.js +9 -5
- package/src/config/index.js +3 -3
- package/src/http/request-cache.js +19 -1
- package/src/index.js +2 -0
- package/src/server/cache-blob.js +33 -0
- package/src/server/create-app.js +5 -0
- package/src/server/data-cache.js +77 -11
- package/src/server/disk-cache.js +2 -2
- package/src/server/head-hints.js +21 -9
- package/src/server/html-cache.js +169 -39
- package/src/server/image-optimizer.js +72 -28
- package/src/server/redis.js +2 -2
- package/src/server/render.js +165 -53
- package/src/views/helpers/tags.js +38 -9
- package/types/client/registry.d.ts +2 -0
- package/types/config/defaults.d.ts +8 -4
- package/types/config/index.d.ts +1 -0
- package/types/index.d.ts +1 -1
- package/types/server/cache-blob.d.ts +8 -0
- package/types/server/data-cache.d.ts +1 -0
- package/types/server/html-cache.d.ts +4 -5
- package/types/server/render.d.ts +19 -0
- package/types/views/helpers/tags.d.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -26,9 +26,46 @@ one is listed under a **Breaking** heading.
|
|
|
26
26
|
variant URLs once at load time.
|
|
27
27
|
- `scripts/measure-ssr.mjs` microbench for fixture `renderView` /
|
|
28
28
|
`renderPage` and live cache MISS/HIT timings against a running app.
|
|
29
|
+
- HTML cache awaits brotli on write so the MISS response can reuse it;
|
|
30
|
+
`rememberHtmlEncoding` updates the byte budget by delta instead of
|
|
31
|
+
rescanning the store. Short negative cache skips empty L2 (Redis/disk)
|
|
32
|
+
lookups after a miss.
|
|
33
|
+
- Data cache early-refreshes near TTL (same lead formula as HTML) and skips
|
|
34
|
+
repeated empty L2 reads the same way; `produceMs` is stored on entries.
|
|
35
|
+
- Image optimizer coalesces concurrent work for the same `url+w+q` and caches
|
|
36
|
+
DNS safety checks for 60s.
|
|
37
|
+
- Disk/Redis L2 decode uses async brotli (no sync decompress on the event
|
|
38
|
+
loop). `cache()` skips `JSON.stringify` for a single primitive argument.
|
|
39
|
+
- `preconnectHints()` adds `build.assets.prefix` origin automatically. Build
|
|
40
|
+
runs Fonts and Icon sprite in parallel; precompress writes `.br`/`.gz` in
|
|
41
|
+
parallel per file.
|
|
42
|
+
- Cold SSR without prewarm: `warmRenderEngine()` runs before listen; page
|
|
43
|
+
`.jsk` templates load lazily (layout + `components/*` stay eager); production
|
|
44
|
+
`ensureTemplatesCompiled` returns on manifest without walking sources.
|
|
45
|
+
- L2 read races a 50ms budget then produces locally; Redis `storeEncoded`
|
|
46
|
+
defaults to on.
|
|
47
|
+
- `renderPage` runs the body with `page.metadata` in parallel with the
|
|
48
|
+
`metadata` hook + `layoutContext` chain.
|
|
49
|
+
- `sendHtml` always negotiates brotli/gzip for HTML (including
|
|
50
|
+
`fragment` / `private`), instead of relying on compression middleware.
|
|
51
|
+
- Transient upstream `notFound()` defaults to an immediate 503 with
|
|
52
|
+
background retry (`cache().transientRetry.background`; set `false` for the
|
|
53
|
+
old sync loop). `warmRenderEngine` / `resetRenderEngine` are exported.
|
|
54
|
+
|
|
55
|
+
### Breaking
|
|
56
|
+
|
|
57
|
+
- Redis `storeEncoded` defaults to `true` (was `false`). Opt out with
|
|
58
|
+
`cache().redis.storeEncoded: false` if L2 size must stay minimal.
|
|
59
|
+
- Transient retry no longer delays the visitor by default
|
|
60
|
+
(`background: true`). Set `cache().transientRetry.background: false` to
|
|
61
|
+
wait for sync retries before responding.
|
|
29
62
|
|
|
30
63
|
### Fixed
|
|
31
64
|
|
|
65
|
+
- `hydrate()` mounts zero-box hosts (`hidden` / no layout box) through
|
|
66
|
+
`mountWhenReady()` instead of calling `mountIsland()` immediately, so
|
|
67
|
+
`data-island-idle` on a hidden host still waits for `load` plus idle time.
|
|
68
|
+
`data-island-eager` is unchanged (mounts right away).
|
|
32
69
|
- S3 PutObject uses path-style URLs for bucket names that contain dots
|
|
33
70
|
(e.g. `static.investvio.dev`). Virtual-hosted
|
|
34
71
|
`bucket.s3.region.amazonaws.com` fails TLS because the cert only covers
|
|
@@ -36,6 +73,10 @@ one is listed under a **Breaking** heading.
|
|
|
36
73
|
|
|
37
74
|
### Added
|
|
38
75
|
|
|
76
|
+
- `stylesheets({ blocking: false })` and `<Stylesheets :blocking="false" />`
|
|
77
|
+
emit each stylesheet with `media="print"` and an `onload` that switches it
|
|
78
|
+
to `all`, plus a `<noscript>` fallback, so the links do not block rendering.
|
|
79
|
+
The default stays render-blocking.
|
|
39
80
|
- Dev tools **Server** tab can clear the HTML cache for the open page, every
|
|
40
81
|
HTML entry, or the data cache. Live stats include HTML/data entry counts;
|
|
41
82
|
new endpoints under `devBasePath`: `POST /cache/clear-html`,
|
package/docs/02-mimari.md
CHANGED
|
@@ -155,9 +155,17 @@ kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasınd
|
|
|
155
155
|
ziyarette zaten `immutable` önbellekten geliyor.
|
|
156
156
|
|
|
157
157
|
Sayfaya özel kurallar için `styles/pages/*.css` + controller `styles: [...]`
|
|
158
|
-
eklenebilir ([08-build.md](./08-build.md)); bunlar da
|
|
159
|
-
ama yalnızca ilgili sayfada. Tailwind utility'leri
|
|
160
|
-
sayfa sheet'inde tam `@import "tailwindcss"` utility
|
|
158
|
+
eklenebilir ([08-build.md](./08-build.md)); bunlar da varsayılan olarak
|
|
159
|
+
render-blocking basılır ama yalnızca ilgili sayfada. Tailwind utility'leri
|
|
160
|
+
global sheet'te kalır — sayfa sheet'inde tam `@import "tailwindcss"` utility
|
|
161
|
+
çıktısını tekrarlar.
|
|
162
|
+
|
|
163
|
+
Erken boyama isteyen bir layout bu varsayılandan çıkabilir:
|
|
164
|
+
`stylesheets({ blocking: false })` veya şablonda
|
|
165
|
+
`<Stylesheets :blocking="false" />`. Link `media="print"` ile iner, `onload`
|
|
166
|
+
onu `all` yapar; JS kapalıyken `<noscript>` içindeki normal link uygulanır.
|
|
167
|
+
Bu, yukarıdaki CLS takasının tersidir: ilk boyama erken gelir, sheet gelince
|
|
168
|
+
düzen kayabilir.
|
|
161
169
|
|
|
162
170
|
Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
|
|
163
171
|
yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
|
|
@@ -15,17 +15,18 @@ route(controller)
|
|
|
15
15
|
└─ produce()
|
|
16
16
|
├─ controller(ctx) → sayfa tanımı
|
|
17
17
|
└─ renderPage(page)
|
|
18
|
-
├─ hooks.metadata(page) + page.metadata → metadata
|
|
19
18
|
├─ Promise.all([
|
|
20
|
-
│ renderView(page.view, { …data, metadata }),
|
|
21
|
-
│ hooks.
|
|
19
|
+
│ renderView(page.view, { …data, page.metadata }), → body
|
|
20
|
+
│ hooks.metadata(page) + page.metadata → metadata
|
|
21
|
+
│ └─ hooks.layoutContext({ pathname, metadata }) → context
|
|
22
22
|
│ ])
|
|
23
23
|
└─ layout (.jsk derlenmiş veya .ejs) → tam HTML
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
Gövde, `hooks.metadata` + `layoutContext` zinciriyle **paralel** üretilir.
|
|
27
|
+
Şablon locals'ında `metadata` önce controller'ın `page.metadata`'sıdır; layout
|
|
28
|
+
birleşik (hook + sayfa) metadata'yı alır. Navigasyon çoğu projede upstream'den
|
|
29
|
+
gelir — gövdeyi metadata zincirinin bitmesini bekletmek her MISS'e gecikme ekler.
|
|
29
30
|
|
|
30
31
|
## `.jsk` — build-time derlenmiş şablonlar
|
|
31
32
|
|
|
@@ -112,7 +113,7 @@ script ve JSON-LD döngüleri için yerleşikler:
|
|
|
112
113
|
|
|
113
114
|
| Etiket | Props | Çıktı |
|
|
114
115
|
| --- | --- | --- |
|
|
115
|
-
| `Stylesheets` | `styles` | `app.css` + sayfa sheet’leri (`data-jskelet-css`) |
|
|
116
|
+
| `Stylesheets` | `styles`, `blocking` | `app.css` + sayfa sheet’leri (`data-jskelet-css`). `blocking` varsayılan `true` |
|
|
116
117
|
| `BodyScripts` | `entries`, `devtools`, `devBasePath` | `main.js`, entry’ler, isteğe bağlı overlay |
|
|
117
118
|
| `JsonLd` | `items` (`structuredData`) | `application/ld+json` script’leri |
|
|
118
119
|
|
|
@@ -180,9 +181,12 @@ Dikkat edilecek noktalar:
|
|
|
180
181
|
|
|
181
182
|
- **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
|
|
182
183
|
geciktirmek doğrudan LCP'ye yazılır.
|
|
183
|
-
- **Global `app.css` render-blocking** ve gerekçesi
|
|
184
|
+
- **Global `app.css` varsayılan olarak render-blocking** ve gerekçesi
|
|
184
185
|
[02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
|
|
185
|
-
sheet'leri de aynı şekilde basılır.
|
|
186
|
+
sheet'leri de aynı şekilde basılır. `<Stylesheets :blocking="false" />`
|
|
187
|
+
(`stylesheets({ blocking: false })`) linkleri `media="print"` + `onload`
|
|
188
|
+
ile basar; JS kapalıyken `<noscript>` yedeği uygulanır. Boyama beklenmez,
|
|
189
|
+
sheet gelince düzen kayabilir. Build çalışmadıysa `hasAsset` false olur
|
|
186
190
|
ve etiket hiç basılmaz.
|
|
187
191
|
- **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
|
|
188
192
|
istememesini sağlar (`Stylesheets` / `BodyScripts` içinde).
|
|
@@ -431,6 +435,32 @@ icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
|
|
|
431
435
|
çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
|
|
432
436
|
sessizce boşluk kalır ([08-build.md](./08-build.md)).
|
|
433
437
|
|
|
438
|
+
### `stylesheets(props)`
|
|
439
|
+
|
|
440
|
+
Layout `<head>` içindeki global `app.css` ve controller `styles` dizisi.
|
|
441
|
+
Doğrudan çağırmak yerine yerleşik etiket kullanılır:
|
|
442
|
+
|
|
443
|
+
```jsk
|
|
444
|
+
<Stylesheets :styles="styles" />
|
|
445
|
+
<Stylesheets :styles="styles" :blocking="false" />
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```js
|
|
449
|
+
stylesheets({ styles: ["home.css"] });
|
|
450
|
+
stylesheets({ styles: ["home.css"], blocking: false });
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
- `styles` manifest anahtarlarıdır (`"home.css"` → `styles/pages/home.css`).
|
|
454
|
+
`hasAsset` false olanlar basılmaz.
|
|
455
|
+
- `blocking` varsayılan `true`: `<link rel="stylesheet" data-jskelet-css="…">`.
|
|
456
|
+
Yalnızca boolean `false` beklemeyi kapatır. Çıktı `media="print"` ve
|
|
457
|
+
`onload="this.media='all'"` olur; eşleşmeyen media boyamayı bekletmez.
|
|
458
|
+
`<noscript>` içindeki ikinci link, JS kapalıyken stillerin print'te
|
|
459
|
+
kalmamasını sağlar. Inline `onload` çalışmayan bir CSP'de (`script-src`
|
|
460
|
+
içinde `'unsafe-inline'` veya `script-src-attr` yok) sheet `print`'te kalır.
|
|
461
|
+
- `data-jskelet-css` dev hot-swap'ın doğru linki seçmesi içindir; her iki
|
|
462
|
+
modda da yazılır.
|
|
463
|
+
|
|
434
464
|
### `preloadImage(props)`
|
|
435
465
|
|
|
436
466
|
```js
|
package/docs/05-islands.md
CHANGED
|
@@ -108,10 +108,14 @@ ve doğrudan boş zamana kaydırılır.
|
|
|
108
108
|
`hidden` bir drawer ya da dialog'un düzen kutusu yoktur ve
|
|
109
109
|
`IntersectionObserver` onu **asla** bildirmez. Bu yüzden `hydrate()` ölçümleri
|
|
110
110
|
tek seferde okur (`getClientRects().length > 0`) ve kutusu olmayan elementleri
|
|
111
|
-
gözlemciye vermek yerine
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
gözlemciye vermek yerine `mountWhenReady()` ile bağlar — yani
|
|
112
|
+
`data-island-idle` gizli host'ta da geçerlidir (`load` + boş zaman); idle
|
|
113
|
+
olmayanlar boş zamana kaydırılır. Ölçümlerin tek seferde okunması bilinçli:
|
|
114
|
+
araya yazma girmediği için tek reflow olur. `data-island-eager` hâlâ hemen
|
|
115
|
+
bağlanır.
|
|
116
|
+
|
|
117
|
+
Pratik sonucu: bir modal'ı `hidden` başlatabilirsiniz, island'ı yine bağlanır;
|
|
118
|
+
ağır bir gizli host için `data-island-idle` LCP ile yarışmayı keser.
|
|
115
119
|
|
|
116
120
|
## `client/` dizini
|
|
117
121
|
|
package/docs/06-cache.md
CHANGED
|
@@ -560,39 +560,40 @@ geçici bir kota sorunu TTL boyunca "bu sayfa yok" cevabına dönüşür. Arama
|
|
|
560
560
|
için bu kalıcı bir kayıp.
|
|
561
561
|
|
|
562
562
|
Framework bu durumu ayırır: render sırasında **geçici** bir upstream hatası
|
|
563
|
-
varsa `notFound()` 404 olarak servis edilmez.
|
|
563
|
+
varsa `notFound()` 404 olarak servis edilmez. Varsayılan (`background: true`):
|
|
564
564
|
|
|
565
|
-
1.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
taşır, sonraki istek yine gerçek içeriği üretebilir.
|
|
565
|
+
1. Ziyaretçiye hemen önbelleğe girmeyen bir `503` + `Retry-After` gider.
|
|
566
|
+
2. Aynı yol arka planda yeniden denenir (varsayılan bir kez, 300 ms sonra).
|
|
567
|
+
Deneme kendi upstream ve istek içi cache bağlamında koşar; ilk turun hatası
|
|
568
|
+
da memoize edilmiş boş cevabı da ikinci turu etkilemez. Başarılı olursa
|
|
569
|
+
sonraki istek L1/L2 HIT alır.
|
|
570
|
+
3. `background: false` eski senkron döngüyü açar: önce bekle + dene, başarısızsa
|
|
571
|
+
503; başarılıysa ziyaretçi gerçek içeriği görür.
|
|
573
572
|
|
|
574
573
|
| Render sırasında | `notFound()` sonucu |
|
|
575
574
|
| --- | --- |
|
|
576
|
-
| Geçici hata
|
|
575
|
+
| Geçici hata + `background: true` (varsayılan) | Hemen `503`, `Retry-After: 30`, `no-store`; retry arka planda |
|
|
576
|
+
| Geçici hata + `background: false` | Tekrar dene → başarılıysa sayfa; değilse `503` |
|
|
577
577
|
| Tekrar denemede upstream sağlam cevap verip "yok" dedi | Normal `404` |
|
|
578
578
|
| Kalıcı hata var (`404`, `403`…) ya da hata yok | Normal `404`, tekrar denenmez |
|
|
579
579
|
|
|
580
580
|
Log satırları:
|
|
581
581
|
|
|
582
582
|
```
|
|
583
|
-
[render] /haber/x returned notFound() while upstream is failing (429 /api/...), retrying (1
|
|
583
|
+
[render] /haber/x returned notFound() while upstream is failing (429 /api/...), retrying in background (1×)
|
|
584
584
|
[render] /haber/x could not be produced, upstream is still failing (429 /api/...), serving an uncached 503 instead of a 404
|
|
585
585
|
```
|
|
586
586
|
|
|
587
587
|
Yani **var olan bir sayfa hiçbir koşulda 404'e dönüşmez**: ya gerçek içerik
|
|
588
|
-
gelir, ya önbelleğe girmeyen bir 503.
|
|
588
|
+
gelir (senkron retry veya sonraki istek), ya önbelleğe girmeyen bir 503.
|
|
589
|
+
Hiçbir şey "yok" olarak dondurulmaz.
|
|
589
590
|
|
|
590
591
|
Tekrar denemenin maliyeti upstream'e binen ikinci bir istek turudur; bu yüzden
|
|
591
592
|
varsayılan tek deneme. Ayar `cache().transientRetry`:
|
|
592
593
|
|
|
593
594
|
```js
|
|
594
595
|
cache: {
|
|
595
|
-
transientRetry: { attempts: 2, delayMs: 500 },
|
|
596
|
+
transientRetry: { attempts: 2, delayMs: 500, background: true },
|
|
596
597
|
}
|
|
597
598
|
```
|
|
598
599
|
|
|
@@ -886,9 +887,10 @@ bir sonraki yazışta siler. `clearHtmlCache()` ve invalidation dosyayı da dü
|
|
|
886
887
|
bile geçerli olan bu kural paylaşımlı kademede daha da kritik: sızması, bir
|
|
887
888
|
kullanıcının HTML'ini tüm kümeye servis etmek olur. `degraded` render ve 200
|
|
888
889
|
dışındaki durum kodları da paylaşılmaz.
|
|
889
|
-
- **Sıkıştırılmış gövdeler varsayılan olarak
|
|
890
|
-
|
|
891
|
-
|
|
890
|
+
- **Sıkıştırılmış gövdeler varsayılan olarak paylaşılır.** `storeEncoded`
|
|
891
|
+
(varsayılan `true`) soğuk replica'nın L2 HIT'te yeniden brotli ödemesini
|
|
892
|
+
önler; girdi başına boyutu iki-üç katına çıkarır. L2 boyutunu kısıtlamak
|
|
893
|
+
için `storeEncoded: false` verin.
|
|
892
894
|
- **Yumuşak invalidation Redis kopyasını siler.** Bayatlatmanın Redis karşılığı
|
|
893
895
|
her anahtar için oku-değiştir-yaz turu demek ve bir webhook binlerce anahtarı
|
|
894
896
|
birden düşürüyor. Silmenin bedeli, o yolu hiç görmemiş bir kopyanın bir kez
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -772,7 +772,7 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
772
772
|
**Tip:**
|
|
773
773
|
`() => { html?: Record<string, number>, staleWhileRevalidate?: number, query?: Record<string, string[] | true>, vary?: { host?: boolean, headers?: string[], fn?: (req) => string | null }, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: false | object }` —
|
|
774
774
|
**Varsayılan:**
|
|
775
|
-
`{ html: {}, staleWhileRevalidate: 60, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, upstream: { rate: 0 }, redis: { enabled: false }, prewarm: { enabled: true, mode: "classic", max: 400, intervalSeconds: 0, origins: [] } }` (veya `prewarm: false` → `mode: "off"`)
|
|
775
|
+
`{ html: {}, staleWhileRevalidate: 60, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300, background: true }, upstream: { rate: 0 }, redis: { enabled: false, storeEncoded: true }, prewarm: { enabled: true, mode: "classic", max: 400, intervalSeconds: 0, origins: [] } }` (veya `prewarm: false` → `mode: "off"`)
|
|
776
776
|
|
|
777
777
|
### `cache().html`
|
|
778
778
|
|
|
@@ -902,13 +902,14 @@ kaydedilecek bir şey yok; kapatmak bağlam kurma maliyetini de kaldırır.
|
|
|
902
902
|
|
|
903
903
|
### `cache().transientRetry`
|
|
904
904
|
|
|
905
|
-
**Tip:** `{ attempts?: number, delayMs?: number } | false` —
|
|
906
|
-
**Varsayılan:** `{ attempts: 1, delayMs: 300 }`
|
|
905
|
+
**Tip:** `{ attempts?: number, delayMs?: number, background?: boolean } | false` —
|
|
906
|
+
**Varsayılan:** `{ attempts: 1, delayMs: 300, background: true }`
|
|
907
907
|
|
|
908
908
|
Geçici bir upstream hatası yüzünden `notFound()` çağrılan sayfa kaç kez daha
|
|
909
|
-
denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi
|
|
910
|
-
|
|
911
|
-
|
|
909
|
+
denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi. `background: true`
|
|
910
|
+
(varsayılan) ziyaretçiye hemen 503 verir ve retry'yi arka planda koşturur;
|
|
911
|
+
`false` eski senkron döngüyü açar (önce bekle + dene, sonra yanıt). `false` ya
|
|
912
|
+
da `attempts: 0` tekrarı kapatır. Ayrıntı: [06-cache.md](./06-cache.md).
|
|
912
913
|
|
|
913
914
|
### `cache().upstream`
|
|
914
915
|
|
|
@@ -959,7 +960,7 @@ kurulmadıysa ya da bağlanılamıyorsa uyarı basılır ve site bellek içi ön
|
|
|
959
960
|
| `keyPrefix` | `string` | `"_jskelet"` | Anahtar düzeninin kökü |
|
|
960
961
|
| `html` | `boolean` | `true` | HTML gövdeleri paylaşılsın mı |
|
|
961
962
|
| `data` | `boolean` | `true` | `withDataCache` girdileri paylaşılsın mı |
|
|
962
|
-
| `storeEncoded` | `boolean` | `
|
|
963
|
+
| `storeEncoded` | `boolean` | `true` | Brotli/gzip gövdeleri de paylaşılsın mı; girdi başına boyutu iki-üç katına çıkarır |
|
|
963
964
|
| `events` | `boolean` | `true` | pub/sub üzerinden invalidation yayını |
|
|
964
965
|
| `commandTimeoutMs` | `number` | `200` | Tek bir komutun en fazla bekletebileceği süre |
|
|
965
966
|
|
package/docs/08-build.md
CHANGED
|
@@ -141,8 +141,10 @@ minifikasyon → `writeAsset("app.css", …)`.
|
|
|
141
141
|
şekilde yavaşlatıyor.
|
|
142
142
|
- **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
|
|
143
143
|
yalnızca birkaç kB daha büyük olur.
|
|
144
|
-
- Global çıktı `app.css`'tir ve layout onu her sayfada
|
|
145
|
-
yükler.
|
|
144
|
+
- Global çıktı `app.css`'tir ve layout onu her sayfada varsayılan olarak
|
|
145
|
+
render-blocking yükler. `<Stylesheets :blocking="false" />` bu beklemeyi
|
|
146
|
+
kapatır ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)). Ayrı bir
|
|
147
|
+
"critical CSS" üretilmemesinin ölçüm gerekçesi
|
|
146
148
|
[02-mimari.md](./02-mimari.md)'de.
|
|
147
149
|
|
|
148
150
|
### Sayfa stylesheet'leri (`styles/pages/`)
|
|
@@ -166,6 +168,7 @@ kalmalı — dosyada tam `@import "tailwindcss"` utility çıktısını tekrarla
|
|
|
166
168
|
|
|
167
169
|
Layout `app.css`ten sonra `styles` dizisindeki her sheet için
|
|
168
170
|
`<link data-jskelet-css="…">` basar; `hasAsset` false ise etiket yok.
|
|
171
|
+
`blocking: false` aynı linklere `media="print"` ve `onload` ekler.
|
|
169
172
|
|
|
170
173
|
### `@source` direktifleri zorunludur
|
|
171
174
|
|
|
@@ -160,9 +160,16 @@ both faster and free of CLS; on the second visit it already comes from the
|
|
|
160
160
|
|
|
161
161
|
Page-specific rules can use `styles/pages/*.css` plus controller
|
|
162
162
|
`styles: [...]` ([08-build.md](./08-build.md)); those are also render-blocking
|
|
163
|
-
but only on the pages that ask for them. Keep Tailwind utilities
|
|
164
|
-
sheet — a full `@import "tailwindcss"` in a page sheet
|
|
165
|
-
output.
|
|
163
|
+
by default, but only on the pages that ask for them. Keep Tailwind utilities
|
|
164
|
+
in the global sheet — a full `@import "tailwindcss"` in a page sheet
|
|
165
|
+
duplicates utility output.
|
|
166
|
+
|
|
167
|
+
A layout that wants an earlier first paint can leave this default:
|
|
168
|
+
`stylesheets({ blocking: false })`, or `<Stylesheets :blocking="false" />`
|
|
169
|
+
in the template. The link downloads with `media="print"`, `onload` switches
|
|
170
|
+
it to `all`, and a plain link inside `<noscript>` applies when JS is off.
|
|
171
|
+
That is the reverse of the CLS tradeoff above: first paint arrives sooner,
|
|
172
|
+
and the layout can shift when the sheet arrives.
|
|
166
173
|
|
|
167
174
|
The same logic applies to icons: instead of a separate request per icon, an SVG
|
|
168
175
|
sprite is produced at build time from only the symbols actually used in the
|
package/docs/en/04-rendering.md
CHANGED
|
@@ -16,17 +16,19 @@ route(controller)
|
|
|
16
16
|
└─ produce()
|
|
17
17
|
├─ controller(ctx) → page definition
|
|
18
18
|
└─ renderPage(page)
|
|
19
|
-
├─ hooks.metadata(page) + page.metadata → metadata
|
|
20
19
|
├─ Promise.all([
|
|
21
|
-
│ renderView(page.view, { …data, metadata }),
|
|
22
|
-
│ hooks.
|
|
20
|
+
│ renderView(page.view, { …data, page.metadata }), → body
|
|
21
|
+
│ hooks.metadata(page) + page.metadata → metadata
|
|
22
|
+
│ └─ hooks.layoutContext({ pathname, metadata }) → context
|
|
23
23
|
│ ])
|
|
24
24
|
└─ layout (.jsk compiled or .ejs) → full HTML
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
The body runs **in parallel** with the `hooks.metadata` + `layoutContext`
|
|
28
|
+
chain. Template locals see the controller's `page.metadata` first; the layout
|
|
29
|
+
receives the merged (hook + page) metadata. Navigation often comes from
|
|
30
|
+
upstream — waiting for the metadata chain before starting the body adds latency
|
|
31
|
+
to every MISS.
|
|
30
32
|
|
|
31
33
|
## `.jsk` — build-time compiled templates
|
|
32
34
|
|
|
@@ -178,10 +180,14 @@ Points to watch:
|
|
|
178
180
|
|
|
179
181
|
- **`extraHead` comes first.** Delaying resource hints (`preconnect`, LCP
|
|
180
182
|
`preload`) writes straight into LCP.
|
|
181
|
-
- **`<Stylesheets />` emits global `app.css`
|
|
182
|
-
`styles: [...]`, with the reasoning in
|
|
183
|
-
[02-architecture.md](./02-architecture.md).
|
|
184
|
-
|
|
183
|
+
- **`<Stylesheets />` emits global `app.css` render-blocking by default**,
|
|
184
|
+
plus controller `styles: [...]`, with the reasoning in
|
|
185
|
+
[02-architecture.md](./02-architecture.md).
|
|
186
|
+
`<Stylesheets :blocking="false" />` (`stylesheets({ blocking: false })`)
|
|
187
|
+
emits the links with `media="print"` plus `onload`, and a `<noscript>`
|
|
188
|
+
fallback when JS is off. Painting is not held, and the layout can shift
|
|
189
|
+
when the sheet arrives. If the build has not run, `hasAsset` is false
|
|
190
|
+
inside the tag and nothing is emitted.
|
|
185
191
|
- **`<BodyScripts />` emits `main.js`, page `entries`, and the
|
|
186
192
|
development-only overlay.** The overlay script exists only when
|
|
187
193
|
`NODE_ENV=development`; it is absent from production output.
|
|
@@ -434,6 +440,34 @@ icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
|
|
|
434
440
|
points at a missing symbol, the screen is silently left blank
|
|
435
441
|
([08-build.md](./08-build.md)).
|
|
436
442
|
|
|
443
|
+
### `stylesheets(props)`
|
|
444
|
+
|
|
445
|
+
Global `app.css` and the controller `styles` list for the layout `<head>`.
|
|
446
|
+
The built-in tag is the usual call site:
|
|
447
|
+
|
|
448
|
+
```jsk
|
|
449
|
+
<Stylesheets :styles="styles" />
|
|
450
|
+
<Stylesheets :styles="styles" :blocking="false" />
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
stylesheets({ styles: ["home.css"] });
|
|
455
|
+
stylesheets({ styles: ["home.css"], blocking: false });
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
- `styles` are manifest keys (`"home.css"` → `styles/pages/home.css`). Names
|
|
459
|
+
for which `hasAsset` is false are omitted.
|
|
460
|
+
- `blocking` defaults to `true`:
|
|
461
|
+
`<link rel="stylesheet" data-jskelet-css="…">`. Only the boolean `false`
|
|
462
|
+
turns waiting off. The output is `media="print"` and
|
|
463
|
+
`onload="this.media='all'"`; a non-matching media does not hold painting.
|
|
464
|
+
A second link inside `<noscript>` keeps styles from staying on `print`
|
|
465
|
+
when JS is off. Under a CSP that blocks inline handlers (no
|
|
466
|
+
`'unsafe-inline'` in `script-src`, and no `script-src-attr`) the sheet
|
|
467
|
+
stays on `print`.
|
|
468
|
+
- `data-jskelet-css` is how dev hot-swap picks the right link; it is written
|
|
469
|
+
in both modes.
|
|
470
|
+
|
|
437
471
|
### `preloadImage(props)`
|
|
438
472
|
|
|
439
473
|
```js
|
package/docs/en/05-islands.md
CHANGED
|
@@ -111,12 +111,16 @@ skipped and it is deferred straight to idle time.
|
|
|
111
111
|
|
|
112
112
|
A `hidden` drawer or dialog has no layout box, and `IntersectionObserver`
|
|
113
113
|
**never** reports it. That is why `hydrate()` reads its measurements in one go
|
|
114
|
-
(`getClientRects().length > 0`) and mounts elements
|
|
115
|
-
instead of handing them to the observer
|
|
116
|
-
|
|
114
|
+
(`getClientRects().length > 0`) and mounts zero-box elements via
|
|
115
|
+
`mountWhenReady()` instead of handing them to the observer — so
|
|
116
|
+
`data-island-idle` still applies on a hidden host (`load` plus idle time);
|
|
117
|
+
non-idle hosts are deferred to idle time. Reading the measurements in one go
|
|
118
|
+
is deliberate: since no write comes in between there is only a single reflow.
|
|
119
|
+
`data-island-eager` still mounts immediately.
|
|
117
120
|
|
|
118
121
|
The practical consequence: you can start a modal as `hidden` and its island
|
|
119
|
-
will still mount
|
|
122
|
+
will still mount; use `data-island-idle` on a heavy hidden host so it does not
|
|
123
|
+
compete with LCP.
|
|
120
124
|
|
|
121
125
|
## The `client/` directory
|
|
122
126
|
|
package/docs/en/06-caching.md
CHANGED
|
@@ -570,39 +570,41 @@ cache, a temporary quota problem becomes a "this page does not exist" answer for
|
|
|
570
570
|
the whole TTL. For a search engine that is a permanent loss.
|
|
571
571
|
|
|
572
572
|
The framework separates the two cases: if a **transient** upstream failure
|
|
573
|
-
happened during the render, `notFound()` is not served as a 404.
|
|
573
|
+
happened during the render, `notFound()` is not served as a 404. Default
|
|
574
|
+
(`background: true`):
|
|
574
575
|
|
|
575
|
-
1. The
|
|
576
|
+
1. The visitor gets an immediate uncached `503` + `Retry-After`.
|
|
577
|
+
2. The same path is retried in the background (once by default, after 300 ms).
|
|
576
578
|
The retry runs in its own upstream and per-request cache scope, so neither
|
|
577
|
-
the first round's failure nor its memoized empty answers affect it.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
3. If the retries are exhausted the response is a `503` — not cached, carrying
|
|
582
|
-
`Retry-After`, and the next request can still produce the real content.
|
|
579
|
+
the first round's failure nor its memoized empty answers affect it. On
|
|
580
|
+
success the next request is an L1/L2 HIT.
|
|
581
|
+
3. `background: false` restores the old sync loop: wait + retry first; on
|
|
582
|
+
success the visitor sees real content, otherwise a 503.
|
|
583
583
|
|
|
584
584
|
| During the render | Result of `notFound()` |
|
|
585
585
|
| --- | --- |
|
|
586
|
-
|
|
|
586
|
+
| Transient failure + `background: true` (default) | Immediate `503`, `Retry-After: 30`, `no-store`; retry in background |
|
|
587
|
+
| Transient failure + `background: false` | Retry → the page if it succeeds; otherwise `503` |
|
|
587
588
|
| The retry got a clean answer saying "not there" | A normal `404` |
|
|
588
589
|
| A permanent failure (`404`, `403`…) or no failure | A normal `404`, no retry |
|
|
589
590
|
|
|
590
591
|
The log lines:
|
|
591
592
|
|
|
592
593
|
```
|
|
593
|
-
[render] /news/x returned notFound() while upstream is failing (429 /api/...), retrying (1
|
|
594
|
+
[render] /news/x returned notFound() while upstream is failing (429 /api/...), retrying in background (1×)
|
|
594
595
|
[render] /news/x could not be produced, upstream is still failing (429 /api/...), serving an uncached 503 instead of a 404
|
|
595
596
|
```
|
|
596
597
|
|
|
597
|
-
So **an existing page never turns into a 404**: either the real content arrives
|
|
598
|
-
or an uncached 503 does. Nothing is frozen as
|
|
598
|
+
So **an existing page never turns into a 404**: either the real content arrives
|
|
599
|
+
(sync retry or the next request), or an uncached 503 does. Nothing is frozen as
|
|
600
|
+
"missing".
|
|
599
601
|
|
|
600
602
|
The cost of a retry is a second round of requests on upstream, which is why the
|
|
601
603
|
default is a single attempt. The setting is `cache().transientRetry`:
|
|
602
604
|
|
|
603
605
|
```js
|
|
604
606
|
cache: {
|
|
605
|
-
transientRetry: { attempts: 2, delayMs: 500 },
|
|
607
|
+
transientRetry: { attempts: 2, delayMs: 500, background: true },
|
|
606
608
|
}
|
|
607
609
|
```
|
|
608
610
|
|
|
@@ -905,9 +907,9 @@ file too.
|
|
|
905
907
|
rule already holds in a single process, but it matters far more in a shared
|
|
906
908
|
tier: a leak would mean serving one user's HTML to the whole cluster.
|
|
907
909
|
`degraded` renders and non-200 status codes are not shared either.
|
|
908
|
-
- **Compressed bodies
|
|
909
|
-
|
|
910
|
-
|
|
910
|
+
- **Compressed bodies are shared by default.** `storeEncoded` (default `true`)
|
|
911
|
+
keeps a cold replica from paying brotli again on an L2 HIT; it doubles or
|
|
912
|
+
triples the size per entry. Set `storeEncoded: false` to keep L2 lean.
|
|
911
913
|
- **A soft invalidation deletes the Redis copy.** Staling in Redis would mean a
|
|
912
914
|
read-modify-write round per key, and a webhook drops thousands of keys at
|
|
913
915
|
once. The cost of deleting is one render on a replica that never saw that
|
|
@@ -786,7 +786,7 @@ Details: [03-routing.md](./03-routing.md).
|
|
|
786
786
|
**Type:**
|
|
787
787
|
`() => { html?: Record<string, number>, staleWhileRevalidate?: number, query?: Record<string, string[] | true>, vary?: { host?: boolean, headers?: string[], fn?: (req) => string | null }, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: false | object }` —
|
|
788
788
|
**Default:**
|
|
789
|
-
`{ html: {}, staleWhileRevalidate: 60, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, upstream: { rate: 0 }, redis: { enabled: false }, prewarm: { enabled: true, mode: "classic", max: 400, intervalSeconds: 0, origins: [] } }` (or `prewarm: false` → `mode: "off"`)
|
|
789
|
+
`{ html: {}, staleWhileRevalidate: 60, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300, background: true }, upstream: { rate: 0 }, redis: { enabled: false, storeEncoded: true }, prewarm: { enabled: true, mode: "classic", max: 400, intervalSeconds: 0, origins: [] } }` (or `prewarm: false` → `mode: "off"`)
|
|
790
790
|
|
|
791
791
|
### `cache().html`
|
|
792
792
|
|
|
@@ -920,13 +920,15 @@ setting up the context.
|
|
|
920
920
|
|
|
921
921
|
### `cache().transientRetry`
|
|
922
922
|
|
|
923
|
-
**Type:** `{ attempts?: number, delayMs?: number } | false` —
|
|
924
|
-
**Default:** `{ attempts: 1, delayMs: 300 }`
|
|
923
|
+
**Type:** `{ attempts?: number, delayMs?: number, background?: boolean } | false` —
|
|
924
|
+
**Default:** `{ attempts: 1, delayMs: 300, background: true }`
|
|
925
925
|
|
|
926
926
|
How many extra times a page is tried when `notFound()` was called because of a
|
|
927
927
|
transient upstream failure. The point is that an existing page never turns into
|
|
928
|
-
a 404
|
|
929
|
-
|
|
928
|
+
a 404. `background: true` (default) returns 503 immediately and runs the retry
|
|
929
|
+
in the background; `false` restores the old sync loop (wait + retry, then
|
|
930
|
+
respond). `false` or `attempts: 0` disables the retry. Details:
|
|
931
|
+
[06-caching.md](./06-caching.md).
|
|
930
932
|
|
|
931
933
|
### `cache().upstream`
|
|
932
934
|
|
|
@@ -977,7 +979,7 @@ the site keeps running on the in-process cache.
|
|
|
977
979
|
| `keyPrefix` | `string` | `"_jskelet"` | Root of the key layout |
|
|
978
980
|
| `html` | `boolean` | `true` | Whether HTML bodies are shared |
|
|
979
981
|
| `data` | `boolean` | `true` | Whether `withDataCache` entries are shared |
|
|
980
|
-
| `storeEncoded` | `boolean` | `
|
|
982
|
+
| `storeEncoded` | `boolean` | `true` | Whether brotli/gzip bodies are shared too; doubles or triples the size per entry |
|
|
981
983
|
| `events` | `boolean` | `true` | Invalidation broadcast over pub/sub |
|
|
982
984
|
| `commandTimeoutMs` | `number` | `200` | At most how long a single command may block |
|
|
983
985
|
|
package/docs/en/08-build.md
CHANGED
|
@@ -145,9 +145,11 @@ The pipeline: PostCSS + `@tailwindcss/postcss` → minification with lightningcs
|
|
|
145
145
|
noticeably.
|
|
146
146
|
- **lightningcss is optional:** without it, Tailwind's own output is used, and it
|
|
147
147
|
is only a few kB bigger.
|
|
148
|
-
- The global output is `app.css` and the layout loads it render-blocking on
|
|
149
|
-
page
|
|
150
|
-
|
|
148
|
+
- The global output is `app.css` and the layout loads it render-blocking on
|
|
149
|
+
every page by default. `<Stylesheets :blocking="false" />` turns that wait
|
|
150
|
+
off ([04-rendering.md](./04-rendering.md)). The measurement-based reasoning
|
|
151
|
+
for not producing a separate "critical CSS" is in
|
|
152
|
+
[02-architecture.md](./02-architecture.md).
|
|
151
153
|
|
|
152
154
|
### Page stylesheets (`styles/pages/`)
|
|
153
155
|
|
|
@@ -170,7 +172,8 @@ Page CSS is for page-specific rules. Keep Tailwind utilities in the global
|
|
|
170
172
|
sheet — a full `@import "tailwindcss"` in a page file duplicates utility output.
|
|
171
173
|
|
|
172
174
|
After `app.css`, the layout emits a `<link data-jskelet-css="…">` for each name
|
|
173
|
-
in `styles`; if `hasAsset` is false the tag is omitted.
|
|
175
|
+
in `styles`; if `hasAsset` is false the tag is omitted. `blocking: false`
|
|
176
|
+
adds `media="print"` and `onload` to those same links.
|
|
174
177
|
|
|
175
178
|
### `@source` directives are mandatory
|
|
176
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
package/src/build/build.mjs
CHANGED
|
@@ -68,14 +68,29 @@ async function task(label, run) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
// Fonts ve icon sprite birbirine bağlı değil — paralel.
|
|
72
|
+
{
|
|
73
|
+
/** @type {Promise<Record<string, string> | void>[]} */
|
|
74
|
+
const parallel = [];
|
|
75
|
+
if (config.fonts?.length) {
|
|
76
|
+
parallel.push(
|
|
77
|
+
(async () => {
|
|
78
|
+
const { copyFonts } = await import("./tasks/fonts.mjs");
|
|
79
|
+
return task("Fonts", () => copyFonts(config));
|
|
80
|
+
})(),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (config.icons !== false) {
|
|
84
|
+
parallel.push(
|
|
85
|
+
(async () => {
|
|
86
|
+
const { buildIconSprite } = await import("./tasks/icons.mjs");
|
|
87
|
+
return task("Icon sprite", () => buildIconSprite(config));
|
|
88
|
+
})(),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (const part of await Promise.all(parallel)) {
|
|
92
|
+
if (part) Object.assign(manifest, part);
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
if (fs.existsSync(config.dirs.styles)) {
|