jskelet 0.6.15 → 0.6.17
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 +12 -0
- package/docs/06-cache.md +33 -8
- package/docs/07-yapilandirma.md +25 -10
- package/docs/08-build.md +19 -6
- package/docs/en/06-caching.md +33 -7
- package/docs/en/07-configuration.md +24 -9
- package/docs/en/08-build.md +18 -5
- package/package.json +1 -1
- package/src/build/rewrite-css-urls.mjs +40 -0
- package/src/build/tasks/assets-upload.mjs +34 -1
- package/src/build/tasks/css.mjs +20 -2
- package/src/config/defaults.js +20 -5
- package/src/config/index.js +53 -2
- package/src/server/logs/s3-put.js +148 -16
- package/src/server/prewarm.js +5 -1
- package/types/config/defaults.d.ts +19 -4
- package/types/config/index.d.ts +4 -0
- package/types/server/logs/s3-put.d.ts +34 -0
- package/types/server/prewarm.d.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,18 @@ one is listed under a **Breaking** heading.
|
|
|
17
17
|
|
|
18
18
|
### Added
|
|
19
19
|
|
|
20
|
+
- On-demand HTML cache via `cache().prewarm: false`: visited pages still
|
|
21
|
+
write to L1 and Redis/disk L2 with the usual `html` TTLs, but classic
|
|
22
|
+
prewarm, `onVisit` link warming, and early-expiry HTTP refresh stay off
|
|
23
|
+
(`PREWARM=1` does not override). Mutually exclusive with
|
|
24
|
+
`hooks.prewarmPaths`. Resolved `prewarm.mode` is `"off"` | `"classic"` |
|
|
25
|
+
`"onVisit"`.
|
|
26
|
+
- When `build.assets.prefix` is set, the CSS build rewrites root-absolute
|
|
27
|
+
`url()` paths under `static.prefixes` (e.g. `/fonts/…`) to the CDN
|
|
28
|
+
prefix so they match the S3 key (`…/js/fonts/…`). Default upload
|
|
29
|
+
`include` is `["assets", "fonts"]`; prefix also forces `fonts` into
|
|
30
|
+
`include`. Font uploads run PutBucketCors with
|
|
31
|
+
`upload.corsOrigin` (default `"*"`, set `null` to skip).
|
|
20
32
|
- `asset(name, { origin: "document" })` skips `build.assets.prefix` and
|
|
21
33
|
returns the site-origin `/assets/…` path. `icon()` always resolves the
|
|
22
34
|
sprite this way so SVG `<use href>` keeps working when other assets are
|
package/docs/06-cache.md
CHANGED
|
@@ -1173,10 +1173,34 @@ loopback yalnızca o host'un anahtarını ısıtır — locale sitelerinde
|
|
|
1173
1173
|
`origins: ["http://localhost", "http://tr.localhost"]` gibi çoklu origin
|
|
1174
1174
|
gerekir.
|
|
1175
1175
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1176
|
+
Üç yüzey **karşılıklı dışlayıcıdır**: klasik (`hooks.prewarmPaths` + alanlar),
|
|
1177
|
+
`onVisit` ve on-demand (`prewarm: false`). Birini seçmek diğerini config
|
|
1178
|
+
yüklenirken reddeder.
|
|
1179
|
+
|
|
1180
|
+
### Mod: on-demand — `prewarm: false`
|
|
1181
|
+
|
|
1182
|
+
Arka plan ısıtması yok; yalnızca gerçek trafikle gelen sayfa önbelleğe yazılır.
|
|
1183
|
+
TTL `cache().html` (veya route `revalidate`) ile verilir. Redis açıksa L2'ye,
|
|
1184
|
+
değilse `.jskelet/cache/<buildId>/` disk L2'ye yazılır — ziyaret yazımı zaten
|
|
1185
|
+
böyle çalışır; bu mod klasik turu, `onVisit` link crawl'ını ve TTL öncesi
|
|
1186
|
+
early-expiry HTTP rewarm'ı kapatır. `PREWARM=1` bunu ezmez.
|
|
1187
|
+
|
|
1188
|
+
```js
|
|
1189
|
+
// jskelet.config.mjs
|
|
1190
|
+
export default {
|
|
1191
|
+
cache() {
|
|
1192
|
+
return {
|
|
1193
|
+
html: { "/": 60, "/haber/:slug": 300 },
|
|
1194
|
+
redis: { enabled: true, url: process.env.REDIS_URL },
|
|
1195
|
+
prewarm: false,
|
|
1196
|
+
};
|
|
1197
|
+
},
|
|
1198
|
+
};
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
`hooks.prewarmPaths` ile birlikte **yasaktır**. `prewarm: { enabled: false }`
|
|
1202
|
+
farklıdır: klasik turu keser ama early-expiry drain açık kalır; tamamen pasif
|
|
1203
|
+
mod için `prewarm: false` kullanın.
|
|
1180
1204
|
|
|
1181
1205
|
### Mod: `onVisit` — ziyaret edilen sayfanın linkleri
|
|
1182
1206
|
|
|
@@ -1442,12 +1466,13 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
|
|
|
1442
1466
|
`cache().vary.host: true` yoksa ilk locale'in HTML'i diğer host'a servis
|
|
1443
1467
|
edilir. Prewarm yalnızca `127.0.0.1` ile ısınıyorsa `prewarm.origins` ile
|
|
1444
1468
|
locale host'larını ekleyin.
|
|
1445
|
-
- **Isıtma hiç çalışmıyor.**
|
|
1446
|
-
|
|
1469
|
+
- **Isıtma hiç çalışmıyor.** `prewarm: false` (on-demand) bilinçli olarak
|
|
1470
|
+
hiçbir arka plan ısıtması kurmaz. Klasik modda `hooks.prewarmPaths` tanımlı
|
|
1471
|
+
değil, `PREWARM=0` ayarlı ya da `cache().prewarm.enabled === false`. `onVisit`
|
|
1447
1472
|
modunda `listen` sonrası logda `onVisit mode` satırını ve public cache'li
|
|
1448
1473
|
bir sayfa gezildiğini doğrulayın.
|
|
1449
|
-
- **Config `onVisit` + `max` / `prewarmPaths` ile düşüyor.**
|
|
1450
|
-
dışlayıcı; yalnızca birini kullanın.
|
|
1474
|
+
- **Config `onVisit` / `prewarm: false` + `max` / `prewarmPaths` ile düşüyor.**
|
|
1475
|
+
Üç yüzey karşılıklı dışlayıcı; yalnızca birini kullanın.
|
|
1451
1476
|
- **Isıtma turu API'yi 429'a sokuyor.** `rps` verilmemiş. `concurrency`
|
|
1452
1477
|
düşürmek yeterli değil; kotayı koruyan ayar toplam hız. Kalıcı çözüm veri
|
|
1453
1478
|
önbelleği: ikinci turdan sonra ısıtma upstream'e gitmez.
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -635,7 +635,8 @@ değişmez** — asset'ler origin'den servis edilir.
|
|
|
635
635
|
| `assets.upload.keyPrefix` | `string` | `""` | Nesne anahtarı öneki; bucket yolunda verilmişse o kazanır (`prefix` takma adı da kabul). |
|
|
636
636
|
| `assets.upload.region` | `string` | `us-east-1` | Config; custom `endpoint` varken varsayılan `auto`. |
|
|
637
637
|
| `assets.upload.endpoint` | `string \| null` | `null` | S3-uyumlu API (R2 / MinIO); config. |
|
|
638
|
-
| `assets.upload.include` | `string[]` | `["assets"]` | `public/` altı dizinler
|
|
638
|
+
| `assets.upload.include` | `string[]` | `["assets", "fonts"]` | `public/` altı dizinler. `assets.prefix` doluysa `fonts` yoksa eklenir. |
|
|
639
|
+
| `assets.upload.corsOrigin` | `string \| null` | `"*"` | Font upload'ta PutBucketCors `AllowedOrigin`; `null` kapatır (önceki CORS'un üzerine yazar). |
|
|
639
640
|
| `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
|
|
640
641
|
|
|
641
642
|
**Credential config'e yazılmaz.** Zincir: `AWS_ACCESS_KEY_ID` /
|
|
@@ -769,9 +770,9 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
769
770
|
## `cache()`
|
|
770
771
|
|
|
771
772
|
**Tip:**
|
|
772
|
-
`() => { 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?: object }` —
|
|
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 }` —
|
|
773
774
|
**Varsayılan:**
|
|
774
|
-
`{ 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, max: 400, intervalSeconds: 0, origins: [] } }`
|
|
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
776
|
|
|
776
777
|
### `cache().html`
|
|
777
778
|
|
|
@@ -1083,14 +1084,28 @@ tam cevabı olmadığı — [06-cache.md](./06-cache.md) içinde.
|
|
|
1083
1084
|
|
|
1084
1085
|
### `cache().prewarm`
|
|
1085
1086
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1087
|
+
Üç yüzey: **`false`** (on-demand — yalnızca ziyaret yazımı), **klasik** (liste
|
|
1088
|
+
+ açılış turu) veya **`onVisit`** (ziyaret edilen sayfadaki linkler). Birlikte
|
|
1089
|
+
verilemez — config yüklenirken hata. Çözümlenen `prewarm.mode`: `"off"` |
|
|
1090
|
+
`"classic"` | `"onVisit"`.
|
|
1091
|
+
|
|
1092
|
+
#### On-demand — `prewarm: false`
|
|
1093
|
+
|
|
1094
|
+
Arka plan ısıtması yok (klasik tur, `onVisit`, early-expiry HTTP rewarm).
|
|
1095
|
+
Ziyaret edilen sayfa `html` TTL'i ile L1 + Redis/disk L2'ye yazılır.
|
|
1096
|
+
`PREWARM=1` bunu ezmez. `hooks.prewarmPaths` ile **yasak**.
|
|
1097
|
+
`{ enabled: false }` klasik turu keser ama early-expiry açık kalır — tamamen
|
|
1098
|
+
pasif mod için `false` kullanın.
|
|
1099
|
+
|
|
1100
|
+
```js
|
|
1101
|
+
prewarm: false,
|
|
1102
|
+
```
|
|
1088
1103
|
|
|
1089
1104
|
#### Klasik alanlar
|
|
1090
1105
|
|
|
1091
1106
|
| Alan | Tip | Varsayılan | Anlamı |
|
|
1092
1107
|
| --- | --- | --- | --- |
|
|
1093
|
-
| `enabled` | `boolean` | `true` | `false` ise
|
|
1108
|
+
| `enabled` | `boolean` | `true` | `false` ise klasik tur yapılmaz (`PREWARM=1` ile ezilebilir); early-expiry drain açık kalır |
|
|
1094
1109
|
| `max` | `number` | `400` | Bir turda en fazla kaç yol ısıtılır |
|
|
1095
1110
|
| `concurrency` | `number` | prod 4, dev 1 | Paralel işçi sayısı |
|
|
1096
1111
|
| `rps` | `number` | prod `0`, dev 4 | Saniyedeki en fazla ısıtma isteği; `0` sınırsız. Upstream kotasını koruyan ayar bu. Dev'deki varsayılan fren, ısıtmanın sayfa isteklerini bekletmemesi için. |
|
|
@@ -1134,8 +1149,8 @@ prewarm: {
|
|
|
1134
1149
|
}
|
|
1135
1150
|
```
|
|
1136
1151
|
|
|
1137
|
-
`hooks.prewarmPaths` ve klasik alanlar (`max`, `priority`, …) `onVisit`
|
|
1138
|
-
**yasaktır**. Ayrıntı: [06-cache.md](./06-cache.md).
|
|
1152
|
+
`hooks.prewarmPaths` ve klasik alanlar (`max`, `priority`, …) `onVisit` veya
|
|
1153
|
+
`prewarm: false` ile **yasaktır**. Ayrıntı: [06-cache.md](./06-cache.md).
|
|
1139
1154
|
|
|
1140
1155
|
Sayısal alanların her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env
|
|
1141
1156
|
önceliklidir. Ayrıntı: [06-cache.md](./06-cache.md).
|
|
@@ -1153,7 +1168,7 @@ varsayılanına döner ve uyarır — sayfa düşmez.
|
|
|
1153
1168
|
| `layoutContext` | `({ pathname, metadata }) => object` | Layout local'leri; `lang`, `structuredData`, `extraHead`, `bodyClass` özel yorumlanır | [04](./04-render-ve-sablonlar.md) |
|
|
1154
1169
|
| `notFound` | `() => object \| null` | 404 sayfa tanımı; `null` ise framework'ün hata sayfası | [03](./03-routing.md) |
|
|
1155
1170
|
| `error` | `({ status, error }) => object \| string \| null` | 404 dışındaki hata sayfaları (ve `notFound` yoksa 404); sayfa tanımı ya da doğrudan HTML | [03](./03-routing.md) |
|
|
1156
|
-
| `prewarmPaths` | `() => string[]` | Klasik ısıtmada ısıtılacak yollar; tanımlı değilse klasik tur kurulmaz. `onVisit` ile
|
|
1171
|
+
| `prewarmPaths` | `() => string[]` | Klasik ısıtmada ısıtılacak yollar; tanımlı değilse klasik tur kurulmaz. `onVisit` ve `prewarm: false` ile **yasak** | [06](./06-cache.md) |
|
|
1157
1172
|
|
|
1158
1173
|
```js
|
|
1159
1174
|
hooks: {
|
|
@@ -1246,7 +1261,7 @@ basılmaz.
|
|
|
1246
1261
|
| `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache yüzeyi | — | API token. Verilene kadar CDN purge'ü ve edge analitiği kapalıdır; config'teki `apiToken`'ı ezer. Token hiçbir cevapta dönmez. [06](./06-cache.md) |
|
|
1247
1262
|
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
|
|
1248
1263
|
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
|
|
1249
|
-
| `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
|
|
1264
|
+
| `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar. `prewarm: false` (on-demand) ezilmez |
|
|
1250
1265
|
| `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
|
|
1251
1266
|
| `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 1 | Paralel işçi sayısı |
|
|
1252
1267
|
| `PREWARM_RPS` | `prewarm` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız |
|
package/docs/08-build.md
CHANGED
|
@@ -91,12 +91,21 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
|
|
|
91
91
|
Varsayılan kapalıdır. Prefix / bucket / keyPrefix config'te; credential
|
|
92
92
|
env → ECS task role → EC2 IMDS zincirinden. Açılınca:
|
|
93
93
|
|
|
94
|
-
1. Production build `public/assets/`
|
|
95
|
-
altındaki dosyaları S3'e PutObject ile kopyalar.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
1. Production build `public/assets/` ve `public/fonts/` (varsayılan `include`)
|
|
95
|
+
altındaki dosyaları S3'e PutObject ile kopyalar. `assets.prefix` doluysa
|
|
96
|
+
`include`'da `fonts` yoksa otomatik eklenir.
|
|
97
|
+
2. CSS derlemesi `static.prefixes` kök-mutlak `url()` yollarını
|
|
98
|
+
(`/fonts/…`, `/assets/…`) `prefix` ile yeniden yazar — aksi hâlde CDN'deki
|
|
99
|
+
stylesheet `url("/fonts/x")` yolunu host köküne çözer ve `keyPrefix`
|
|
100
|
+
kaybolur. `@font-face` CDN CSS içinde kalabilir; font ile CSS aynı host'ta
|
|
101
|
+
olduğu için ek CORS gerekmez.
|
|
102
|
+
3. Font yüklendiğinde ve `upload.corsOrigin` doluysa (varsayılan `"*"`)
|
|
103
|
+
PutBucketCors bir GET/HEAD kuralı yazar — bucket'taki önceki CORS'un
|
|
104
|
+
**üzerine** yazar; kapatmak için `corsOrigin: null`. HTML'den çapraz
|
|
105
|
+
origin font için bu (veya CloudFront response header) gerekir.
|
|
106
|
+
4. Çalışma anında `asset()` / optimize görseller `build.assets.prefix` ile
|
|
107
|
+
absolute CDN URL basar. `icon()` sprite'ı ve
|
|
108
|
+
`asset(name, { origin: "document" })` site origin'inde kalır.
|
|
100
109
|
|
|
101
110
|
Bucket adı noktalı olabilir (`static.ornek.com`); `https://…` şeması
|
|
102
111
|
`prefix`'e yazılır. Config sözlüğü: [07-yapilandirma.md](./07-yapilandirma.md#build).
|
|
@@ -297,6 +306,10 @@ Kullanımı stylesheet'te elle yazılır:
|
|
|
297
306
|
`.woff2` uzantısı ve `/fonts/` öneki varsayılan `static` kurallarında olduğu için
|
|
298
307
|
bu dosyalara otomatik olarak `immutable` cache yazılır.
|
|
299
308
|
|
|
309
|
+
`build.assets.prefix` açıksa build, CSS'teki `url("/fonts/…")` yollarını CDN
|
|
310
|
+
prefix'iyle yeniden yazar ve fontları S3'e de basar — layout'ta ayrı
|
|
311
|
+
`@font-face` tutmaya gerek kalmaz.
|
|
312
|
+
|
|
300
313
|
## İkon sprite
|
|
301
314
|
|
|
302
315
|
**Yalnızca kaynakta kullanılan** ikonlar için bir `<symbol>` seti üretir. Tüm
|
package/docs/en/06-caching.md
CHANGED
|
@@ -1175,10 +1175,35 @@ traffic. With `vary.host`, the default loopback only warms that host's key —
|
|
|
1175
1175
|
locale sites need multiple origins such as
|
|
1176
1176
|
`origins: ["http://localhost", "http://tr.localhost"]`.
|
|
1177
1177
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1178
|
+
Three surfaces are **mutually exclusive**: classic (`hooks.prewarmPaths` +
|
|
1179
|
+
fields), `onVisit`, and on-demand (`prewarm: false`). Choosing one rejects the
|
|
1180
|
+
others at config load.
|
|
1181
|
+
|
|
1182
|
+
### Mode: on-demand — `prewarm: false`
|
|
1183
|
+
|
|
1184
|
+
No background warming; only pages hit by real traffic are written to the cache.
|
|
1185
|
+
TTL comes from `cache().html` (or the route `revalidate`). With Redis on, the
|
|
1186
|
+
entry goes to L2 Redis; otherwise to the `.jskelet/cache/<buildId>/` disk L2 —
|
|
1187
|
+
visit writes already work that way; this mode turns off the classic pass,
|
|
1188
|
+
`onVisit` link crawling, and early-expiry HTTP refresh before TTL. `PREWARM=1`
|
|
1189
|
+
does not override it.
|
|
1190
|
+
|
|
1191
|
+
```js
|
|
1192
|
+
// jskelet.config.mjs
|
|
1193
|
+
export default {
|
|
1194
|
+
cache() {
|
|
1195
|
+
return {
|
|
1196
|
+
html: { "/": 60, "/news/:slug": 300 },
|
|
1197
|
+
redis: { enabled: true, url: process.env.REDIS_URL },
|
|
1198
|
+
prewarm: false,
|
|
1199
|
+
};
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
**Forbidden** together with `hooks.prewarmPaths`. `prewarm: { enabled: false }`
|
|
1205
|
+
is different: it stops the classic pass but leaves the early-expiry drain on —
|
|
1206
|
+
use `prewarm: false` for fully passive mode.
|
|
1182
1207
|
|
|
1183
1208
|
### Mode: `onVisit` — links from the page just visited
|
|
1184
1209
|
|
|
@@ -1449,12 +1474,13 @@ filled the cache.
|
|
|
1449
1474
|
without `cache().vary.host: true` the first locale's HTML is served to the
|
|
1450
1475
|
other host. If prewarm only hits `127.0.0.1`, add the locale hosts via
|
|
1451
1476
|
`prewarm.origins`.
|
|
1452
|
-
- **The warm-up never runs.**
|
|
1477
|
+
- **The warm-up never runs.** `prewarm: false` (on-demand) intentionally
|
|
1478
|
+
installs no background warming. In classic mode `hooks.prewarmPaths` is not
|
|
1453
1479
|
defined, `PREWARM=0` is set, or `cache().prewarm.enabled === false`. In
|
|
1454
1480
|
`onVisit` mode check the `onVisit mode` log line after `listen` and that a
|
|
1455
1481
|
public cacheable page was visited.
|
|
1456
|
-
- **Config fails with `onVisit` + `max` / `prewarmPaths`.**
|
|
1457
|
-
mutually exclusive; use only one.
|
|
1482
|
+
- **Config fails with `onVisit` / `prewarm: false` + `max` / `prewarmPaths`.**
|
|
1483
|
+
The three surfaces are mutually exclusive; use only one.
|
|
1458
1484
|
- **The warm-up round pushes the API into 429.** No `rps` was given. Lowering
|
|
1459
1485
|
`concurrency` is not enough; the setting that protects the quota is the total
|
|
1460
1486
|
rate. The lasting fix is the data cache: after the second round the warm-up
|
|
@@ -648,7 +648,8 @@ changes** — assets stay on the origin.
|
|
|
648
648
|
| `assets.upload.keyPrefix` | `string` | `""` | Object key prefix; a path on the bucket wins (`prefix` is accepted as an alias). |
|
|
649
649
|
| `assets.upload.region` | `string` | `us-east-1` | Config; defaults to `auto` when `endpoint` is set. |
|
|
650
650
|
| `assets.upload.endpoint` | `string \| null` | `null` | S3-compatible API (R2 / MinIO); config. |
|
|
651
|
-
| `assets.upload.include` | `string[]` | `["assets"]` | Directories under `public
|
|
651
|
+
| `assets.upload.include` | `string[]` | `["assets", "fonts"]` | Directories under `public/`. When `assets.prefix` is set, `fonts` is added if missing. |
|
|
652
|
+
| `assets.upload.corsOrigin` | `string \| null` | `"*"` | PutBucketCors `AllowedOrigin` when fonts upload; `null` disables (replaces prior bucket CORS). |
|
|
652
653
|
| `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
|
|
653
654
|
|
|
654
655
|
**Credentials are not written in the config.** Chain: `AWS_ACCESS_KEY_ID` /
|
|
@@ -783,9 +784,9 @@ Details: [03-routing.md](./03-routing.md).
|
|
|
783
784
|
## `cache()`
|
|
784
785
|
|
|
785
786
|
**Type:**
|
|
786
|
-
`() => { 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?: object }` —
|
|
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 }` —
|
|
787
788
|
**Default:**
|
|
788
|
-
`{ 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, max: 400, intervalSeconds: 0, origins: [] } }`
|
|
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
790
|
|
|
790
791
|
### `cache().html`
|
|
791
792
|
|
|
@@ -1105,14 +1106,28 @@ edges hold this page" has no exact answer — is in
|
|
|
1105
1106
|
|
|
1106
1107
|
### `cache().prewarm`
|
|
1107
1108
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1109
|
+
Three surfaces: **`false`** (on-demand — visit writes only), **classic** (list
|
|
1110
|
+
+ startup pass), or **`onVisit`** (links from the page just visited). They
|
|
1111
|
+
cannot be combined — config load throws. Resolved `prewarm.mode`: `"off"` |
|
|
1112
|
+
`"classic"` | `"onVisit"`.
|
|
1113
|
+
|
|
1114
|
+
#### On-demand — `prewarm: false`
|
|
1115
|
+
|
|
1116
|
+
No background warming (classic pass, `onVisit`, early-expiry HTTP refresh).
|
|
1117
|
+
Visited pages still write to L1 + Redis/disk L2 with the `html` TTLs.
|
|
1118
|
+
`PREWARM=1` does not override. **Forbidden** with `hooks.prewarmPaths`.
|
|
1119
|
+
`{ enabled: false }` stops the classic pass but leaves early-expiry on — use
|
|
1120
|
+
`false` for fully passive mode.
|
|
1121
|
+
|
|
1122
|
+
```js
|
|
1123
|
+
prewarm: false,
|
|
1124
|
+
```
|
|
1110
1125
|
|
|
1111
1126
|
#### Classic fields
|
|
1112
1127
|
|
|
1113
1128
|
| Field | Type | Default | Meaning |
|
|
1114
1129
|
| --- | --- | --- | --- |
|
|
1115
|
-
| `enabled` | `boolean` | `true` | If `false`, no
|
|
1130
|
+
| `enabled` | `boolean` | `true` | If `false`, no classic pass (`PREWARM=1` can override); early-expiry drain stays on |
|
|
1116
1131
|
| `max` | `number` | `400` | At most how many paths are prewarmed per pass |
|
|
1117
1132
|
| `concurrency` | `number` | prod 4, dev 1 | Number of parallel workers |
|
|
1118
1133
|
| `rps` | `number` | prod `0`, dev 4 | At most how many prewarm requests per second; `0` is unlimited. This is the setting that protects the upstream quota. The default brake in dev keeps prewarming from holding page requests up. |
|
|
@@ -1157,7 +1172,7 @@ prewarm: {
|
|
|
1157
1172
|
```
|
|
1158
1173
|
|
|
1159
1174
|
`hooks.prewarmPaths` and classic fields (`max`, `priority`, …) are **forbidden**
|
|
1160
|
-
with `onVisit`. Details: [06-caching.md](./06-caching.md).
|
|
1175
|
+
with `onVisit` or `prewarm: false`. Details: [06-caching.md](./06-caching.md).
|
|
1161
1176
|
|
|
1162
1177
|
Each numeric field can be overridden by an environment variable of the same
|
|
1163
1178
|
name; env takes precedence. Details: [06-caching.md](./06-caching.md).
|
|
@@ -1175,7 +1190,7 @@ its own default and warns — the page does not go down.
|
|
|
1175
1190
|
| `layoutContext` | `({ pathname, metadata }) => object` | Layout locals; `lang`, `structuredData`, `extraHead` and `bodyClass` get special treatment | [04](./04-rendering.md) |
|
|
1176
1191
|
| `notFound` | `() => object \| null` | 404 page definition; if `null`, the framework's error page | [03](./03-routing.md) |
|
|
1177
1192
|
| `error` | `({ status, error }) => object \| string \| null` | Error pages other than 404 (and 404 when there is no `notFound`); a page definition or HTML directly | [03](./03-routing.md) |
|
|
1178
|
-
| `prewarmPaths` | `() => string[]` | Paths for classic prewarm; if omitted, the classic pass is never set up. **Forbidden** with `onVisit` | [06](./06-caching.md) |
|
|
1193
|
+
| `prewarmPaths` | `() => string[]` | Paths for classic prewarm; if omitted, the classic pass is never set up. **Forbidden** with `onVisit` and `prewarm: false` | [06](./06-caching.md) |
|
|
1179
1194
|
|
|
1180
1195
|
```js
|
|
1181
1196
|
hooks: {
|
|
@@ -1269,7 +1284,7 @@ and no warning is printed.
|
|
|
1269
1284
|
| `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache surface | — | API token. Until it is set, CDN purging and edge analytics stay off; it overrides `apiToken` in the config. The token is never returned in a response. [06](./06-caching.md) |
|
|
1270
1285
|
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache surface | — | Zone identifier. No Cloudflare endpoint is called unless it is set alongside the token |
|
|
1271
1286
|
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache surface | — | The root for purge URLs. Required when the panel is opened over an internal address |
|
|
1272
|
-
| `PREWARM` | `startPrewarm` | — | `0` turns prewarming off; `1` overrides `enabled: false` in the config and turns it on |
|
|
1287
|
+
| `PREWARM` | `startPrewarm` | — | `0` turns prewarming off; `1` overrides `enabled: false` in the config and turns it on. Does not override `prewarm: false` (on-demand) |
|
|
1273
1288
|
| `PREWARM_MAX` | `prewarm` | `400` | At most how many paths are prewarmed |
|
|
1274
1289
|
| `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 1 | Number of parallel workers |
|
|
1275
1290
|
| `PREWARM_RPS` | `prewarm` | `0` | At most how many prewarm requests per second; `0` is unlimited |
|
package/docs/en/08-build.md
CHANGED
|
@@ -94,11 +94,20 @@ They are passed to templates automatically; in server code,
|
|
|
94
94
|
Off by default. Prefix / bucket / keyPrefix come from config; credentials
|
|
95
95
|
resolve from env → ECS task role → EC2 IMDS. When enabled:
|
|
96
96
|
|
|
97
|
-
1. A production build PutObjects
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
1. A production build PutObjects `public/assets/` and `public/fonts/`
|
|
98
|
+
(default `include`) to S3. When `assets.prefix` is set, `fonts` is added
|
|
99
|
+
to `include` if missing.
|
|
100
|
+
2. The CSS step rewrites root-absolute `url()` paths under `static.prefixes`
|
|
101
|
+
(`/fonts/…`, `/assets/…`) with `prefix` — otherwise a stylesheet on the CDN
|
|
102
|
+
resolves `url("/fonts/x")` against the host root and drops `keyPrefix`.
|
|
103
|
+
`@font-face` can stay in the CDN CSS; font and CSS share a host, so no
|
|
104
|
+
extra CORS is needed for that path.
|
|
105
|
+
3. When fonts are uploaded and `upload.corsOrigin` is set (default `"*"`),
|
|
106
|
+
PutBucketCors writes a GET/HEAD rule — it **replaces** the bucket's
|
|
107
|
+
previous CORS config; set `corsOrigin: null` to skip. Cross-origin fonts
|
|
108
|
+
from HTML still need this (or a CloudFront response-header policy).
|
|
109
|
+
4. At runtime `asset()` / optimised images emit absolute CDN URLs via
|
|
110
|
+
`build.assets.prefix`. The `icon()` sprite and
|
|
102
111
|
`asset(name, { origin: "document" })` stay on the document origin.
|
|
103
112
|
|
|
104
113
|
Bucket names may include dots (`static.example.com`); put `https://…` in
|
|
@@ -305,6 +314,10 @@ Usage is written by hand in the stylesheet:
|
|
|
305
314
|
Because the `.woff2` extension and the `/fonts/` prefix are in the default
|
|
306
315
|
`static` rules, these files automatically get an `immutable` cache.
|
|
307
316
|
|
|
317
|
+
When `build.assets.prefix` is set, the build rewrites `url("/fonts/…")` in CSS
|
|
318
|
+
to the CDN prefix and uploads the font files too — you do not need a separate
|
|
319
|
+
`@font-face` block in the layout.
|
|
320
|
+
|
|
308
321
|
## Icon sprite
|
|
309
322
|
|
|
310
323
|
Produces a `<symbol>` set for **only the icons actually used in the source**.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
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",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN prefix varken CSS `url(/fonts/…)` yollarını upload path'iyle hizalar.
|
|
3
|
+
*
|
|
4
|
+
* CDN'deki `app.css` kök-mutlak `/fonts/x.woff2` görünce tarayıcı bunu
|
|
5
|
+
* `https://cdn.example.com/fonts/x` diye çözer — `keyPrefix` (`/js` vb.)
|
|
6
|
+
* kaybolur ve 403 gelir. Prefix + aynı göreli yol (`…/js/fonts/x`) doğru
|
|
7
|
+
* nesneye gider; font ile CSS aynı host'ta olduğu için `@font-face` CORS
|
|
8
|
+
* istemez. HTML'den CDN font yüklemek ayrı: bucket/CloudFront CORS gerekir.
|
|
9
|
+
*/
|
|
10
|
+
import { applyAssetPrefix } from "../server/assets.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} css
|
|
14
|
+
* @param {{ prefix: string, prefixes?: string[] }} options
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function rewriteCssStaticUrls(css, { prefix, prefixes = [] }) {
|
|
18
|
+
if (!prefix || !css) return css;
|
|
19
|
+
|
|
20
|
+
const roots = [...prefixes]
|
|
21
|
+
.filter((entry) => typeof entry === "string" && entry.startsWith("/"))
|
|
22
|
+
.map((entry) => (entry.endsWith("/") ? entry : `${entry}/`))
|
|
23
|
+
.sort((a, b) => b.length - a.length);
|
|
24
|
+
|
|
25
|
+
if (!roots.length) return css;
|
|
26
|
+
|
|
27
|
+
return css.replace(
|
|
28
|
+
/url\(\s*(['"]?)(\/[^)"'\s]+)\1\s*\)/gi,
|
|
29
|
+
(match, quote, urlPath) => {
|
|
30
|
+
const pathOnly = urlPath.split(/[?#]/, 1)[0];
|
|
31
|
+
if (!roots.some((root) => pathOnly === root.slice(0, -1) || pathOnly.startsWith(root))) {
|
|
32
|
+
return match;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rewritten = applyAssetPrefix(prefix, urlPath);
|
|
36
|
+
const q = quote || '"';
|
|
37
|
+
return `url(${q}${rewritten}${q})`;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -16,12 +16,22 @@ import {
|
|
|
16
16
|
resolveS3Credentials,
|
|
17
17
|
s3CredentialsMissingReason,
|
|
18
18
|
} from "../../server/logs/s3-credentials.js";
|
|
19
|
-
import { putObject } from "../../server/logs/s3-put.js";
|
|
19
|
+
import { putBucketCors, putObject } from "../../server/logs/s3-put.js";
|
|
20
20
|
import * as log from "../../log.mjs";
|
|
21
21
|
import { paths } from "../paths.mjs";
|
|
22
22
|
|
|
23
23
|
const CONCURRENCY = 8;
|
|
24
24
|
|
|
25
|
+
/** Font MIME — CDN'den HTML çapraz origin yüklerken CORS gerekir. */
|
|
26
|
+
const FONT_MIME = new Set([
|
|
27
|
+
"font/woff",
|
|
28
|
+
"font/woff2",
|
|
29
|
+
"font/ttf",
|
|
30
|
+
"font/otf",
|
|
31
|
+
"application/font-woff",
|
|
32
|
+
"application/font-woff2",
|
|
33
|
+
]);
|
|
34
|
+
|
|
25
35
|
/** @type {Record<string, string>} */
|
|
26
36
|
const MIME = {
|
|
27
37
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -116,6 +126,7 @@ export function assetsUploadSkipReason(config) {
|
|
|
116
126
|
/**
|
|
117
127
|
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
118
128
|
* @param {{ put?: typeof putObject,
|
|
129
|
+
* putCors?: typeof putBucketCors,
|
|
119
130
|
* resolveCredentials?: typeof resolveS3Credentials }} [options]
|
|
120
131
|
* @returns {Promise<void>}
|
|
121
132
|
*/
|
|
@@ -135,6 +146,7 @@ export async function uploadAssets(config, options = {}) {
|
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
const put = options.put ?? putObject;
|
|
149
|
+
const putCors = options.putCors ?? putBucketCors;
|
|
138
150
|
// Normalize bazen atlanır (test / elle config); önek `/` ile bitsin.
|
|
139
151
|
let keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
|
|
140
152
|
if (keyPrefix && !keyPrefix.endsWith("/")) keyPrefix += "/";
|
|
@@ -162,6 +174,27 @@ export async function uploadAssets(config, options = {}) {
|
|
|
162
174
|
return;
|
|
163
175
|
}
|
|
164
176
|
|
|
177
|
+
const uploadsFonts = jobs.some((job) =>
|
|
178
|
+
FONT_MIME.has(contentTypeFor(job.file)),
|
|
179
|
+
);
|
|
180
|
+
if (uploadsFonts && upload.corsOrigin) {
|
|
181
|
+
try {
|
|
182
|
+
await putCors({
|
|
183
|
+
bucket: upload.bucket,
|
|
184
|
+
region: upload.region,
|
|
185
|
+
endpoint: upload.endpoint,
|
|
186
|
+
credentials,
|
|
187
|
+
allowedOrigin: upload.corsOrigin,
|
|
188
|
+
});
|
|
189
|
+
log.detail(`cors ${upload.corsOrigin}`);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
console.warn(
|
|
193
|
+
`[assets] PutBucketCors failed (${message}) — set CDN/bucket CORS manually for cross-origin fonts`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
165
198
|
let uploaded = 0;
|
|
166
199
|
let index = 0;
|
|
167
200
|
|
package/src/build/tasks/css.mjs
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { paths, writeAsset, writeManifest } from "../paths.mjs";
|
|
27
|
+
import { rewriteCssStaticUrls } from "../rewrite-css-urls.mjs";
|
|
27
28
|
import { importFromApp, tryImportFromApp } from "../resolve-peer.mjs";
|
|
28
29
|
import * as log from "../../log.mjs";
|
|
29
30
|
|
|
@@ -85,6 +86,23 @@ function listPageStyles(dir) {
|
|
|
85
86
|
.map((name) => path.join(dir, name));
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* `build.assets.prefix` doluysa `static.prefixes` kök-mutlak `url()` yollarını
|
|
91
|
+
* CDN'e çevirir (`/fonts/…` → `https://cdn…/js/fonts/…`).
|
|
92
|
+
*
|
|
93
|
+
* @param {string} css
|
|
94
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function applyCdnStaticUrls(css, config) {
|
|
98
|
+
const prefix = config.build?.assets?.prefix ?? "";
|
|
99
|
+
if (!prefix) return css;
|
|
100
|
+
return rewriteCssStaticUrls(css, {
|
|
101
|
+
prefix,
|
|
102
|
+
prefixes: config.static?.prefixes ?? ["/assets/", "/fonts/"],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
/**
|
|
89
107
|
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
90
108
|
* @param {{ watch?: boolean }} [options]
|
|
@@ -101,7 +119,7 @@ export async function buildCss(config, { watch = false } = {}) {
|
|
|
101
119
|
const urls = {};
|
|
102
120
|
let bytes = 0;
|
|
103
121
|
|
|
104
|
-
const appCss = await compile(input);
|
|
122
|
+
const appCss = applyCdnStaticUrls(await compile(input), config);
|
|
105
123
|
const appUrl = writeAsset("app.css", appCss);
|
|
106
124
|
urls["app.css"] = appUrl;
|
|
107
125
|
bytes += Buffer.byteLength(appCss);
|
|
@@ -111,7 +129,7 @@ export async function buildCss(config, { watch = false } = {}) {
|
|
|
111
129
|
|
|
112
130
|
for (const file of listPageStyles(pagesDir)) {
|
|
113
131
|
const name = path.basename(file);
|
|
114
|
-
const css = await compile(file);
|
|
132
|
+
const css = applyCdnStaticUrls(await compile(file), config);
|
|
115
133
|
const url = writeAsset(name, css);
|
|
116
134
|
urls[name] = url;
|
|
117
135
|
bytes += Buffer.byteLength(css);
|
package/src/config/defaults.js
CHANGED
|
@@ -51,12 +51,17 @@ export const DEFAULT_DEV_GATE_BYPASS = [
|
|
|
51
51
|
];
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Klasik (liste tabanlı) prewarm ayarları. `onVisit`
|
|
55
|
-
* kullanılamaz — ya açılış/`prewarmPaths` turu, ya
|
|
56
|
-
* linkleri ısıtma
|
|
54
|
+
* Klasik (liste tabanlı) prewarm ayarları. `onVisit` ve `prewarm: false`
|
|
55
|
+
* (on-demand) ile birlikte kullanılamaz — ya açılış/`prewarmPaths` turu, ya
|
|
56
|
+
* ziyaret edilen sayfadaki linkleri ısıtma, ya yalnızca ziyaret yazımı.
|
|
57
|
+
*
|
|
58
|
+
* `mode` çözümleme sonucu: `"classic"` | `"onVisit"` | `"off"`.
|
|
59
|
+
* `prewarm: false` → `"off"` (erken TTL ısıtması dahil hiçbir arka plan yok).
|
|
57
60
|
*/
|
|
58
61
|
export const DEFAULT_PREWARM = {
|
|
59
62
|
enabled: true,
|
|
63
|
+
/** @type {"classic" | "onVisit" | "off"} */
|
|
64
|
+
mode: "classic",
|
|
60
65
|
max: 400,
|
|
61
66
|
intervalSeconds: 0,
|
|
62
67
|
/** Paralel ısıtma isteği; dev'de sunucu tek süreç olduğu için düşürülür. */
|
|
@@ -508,9 +513,19 @@ export const DEFAULT_BUILD = {
|
|
|
508
513
|
keyPrefix: "",
|
|
509
514
|
region: "us-east-1",
|
|
510
515
|
endpoint: null,
|
|
511
|
-
/**
|
|
512
|
-
|
|
516
|
+
/**
|
|
517
|
+
* `public/` altı dizin adları. `fonts` varsayılan: CDN CSS'teki
|
|
518
|
+
* `/fonts/…` rewrite'ı upload nesnesiyle hizalansın.
|
|
519
|
+
*/
|
|
520
|
+
include: /** @type {string[]} */ (["assets", "fonts"]),
|
|
513
521
|
cacheControl: "public, max-age=31536000, immutable",
|
|
522
|
+
/**
|
|
523
|
+
* Upload sırasında PutBucketCors ile yazılan `AllowedOrigin`.
|
|
524
|
+
* `null` / `false` → CORS adımı atlanır. Bucket'taki önceki CORS
|
|
525
|
+
* kuralının üzerine yazar — paylaşımlı kovada bilinçli açın.
|
|
526
|
+
* CDN CSS + aynı host font için genelde gerekmez.
|
|
527
|
+
*/
|
|
528
|
+
corsOrigin: /** @type {string | null} */ ("*"),
|
|
514
529
|
},
|
|
515
530
|
},
|
|
516
531
|
};
|
package/src/config/index.js
CHANGED
|
@@ -184,6 +184,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
184
184
|
* @property {string | null} endpoint
|
|
185
185
|
* @property {string[]} include
|
|
186
186
|
* @property {string} cacheControl
|
|
187
|
+
* @property {string | null} corsOrigin Font/CDN PutBucketCors `AllowedOrigin`; `null` kapatır.
|
|
187
188
|
*
|
|
188
189
|
* @typedef {object} BuildAssetsConfig
|
|
189
190
|
* @property {string} prefix CDN origin (`build.assets.prefix`); sonunda `/` yok.
|
|
@@ -939,18 +940,38 @@ function normalizeOnVisit(raw) {
|
|
|
939
940
|
}
|
|
940
941
|
|
|
941
942
|
/**
|
|
942
|
-
* Klasik liste
|
|
943
|
-
*
|
|
943
|
+
* Klasik liste ısıtması, `onVisit` ve `prewarm: false` (on-demand) karşılıklı
|
|
944
|
+
* dışlayıcıdır. Yanlış moda sessizce düşülmesin diye yüklemede hata verir.
|
|
944
945
|
*
|
|
945
946
|
* @param {unknown} raw
|
|
946
947
|
* @returns {Record<string, unknown>}
|
|
947
948
|
*/
|
|
948
949
|
function normalizePrewarm(raw) {
|
|
950
|
+
// On-demand: yalnızca ziyaret yazımı. Klasik / onVisit / early-expiry yok.
|
|
951
|
+
if (raw === false) {
|
|
952
|
+
return {
|
|
953
|
+
...DEFAULT_PREWARM,
|
|
954
|
+
enabled: false,
|
|
955
|
+
mode: "off",
|
|
956
|
+
onVisit: { ...DEFAULT_PREWARM_ON_VISIT, enabled: false },
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (raw != null && (typeof raw !== "object" || Array.isArray(raw))) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
"[config] cache().prewarm must be false, or an object " +
|
|
963
|
+
"(classic settings or { onVisit })",
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
949
967
|
const source =
|
|
950
968
|
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
951
969
|
? /** @type {Record<string, unknown>} */ ({ ...raw })
|
|
952
970
|
: {};
|
|
953
971
|
|
|
972
|
+
// `mode` çözümleme alanı; kullanıcı yazarsa yok sayılır.
|
|
973
|
+
delete source.mode;
|
|
974
|
+
|
|
954
975
|
const onVisit = normalizeOnVisit(source.onVisit);
|
|
955
976
|
|
|
956
977
|
if (onVisit.enabled) {
|
|
@@ -976,12 +997,15 @@ function normalizePrewarm(raw) {
|
|
|
976
997
|
return {
|
|
977
998
|
...DEFAULT_PREWARM,
|
|
978
999
|
enabled: true,
|
|
1000
|
+
mode: "onVisit",
|
|
979
1001
|
onVisit,
|
|
980
1002
|
};
|
|
981
1003
|
}
|
|
982
1004
|
|
|
983
1005
|
// Klasik mod: `onVisit: false` yazılmış olabilir; diğer alanlar varsayılanlarla
|
|
984
1006
|
// birleşir. `onVisit` anahtarı çözülmüş nesnede her zaman durur.
|
|
1007
|
+
// `enabled: false` klasik turu keser ama early-expiry drain açık kalır —
|
|
1008
|
+
// tamamen kapatmak için `prewarm: false` (`mode: "off"`) kullanılır.
|
|
985
1009
|
const classic = { ...source };
|
|
986
1010
|
delete classic.onVisit;
|
|
987
1011
|
|
|
@@ -993,6 +1017,7 @@ function normalizePrewarm(raw) {
|
|
|
993
1017
|
return {
|
|
994
1018
|
...DEFAULT_PREWARM,
|
|
995
1019
|
...classic,
|
|
1020
|
+
mode: "classic",
|
|
996
1021
|
onVisit,
|
|
997
1022
|
};
|
|
998
1023
|
}
|
|
@@ -1264,11 +1289,25 @@ export function normalizeBuildAssets(raw) {
|
|
|
1264
1289
|
}
|
|
1265
1290
|
}
|
|
1266
1291
|
|
|
1292
|
+
// CDN CSS `/fonts/…` → prefix'li URL yazar; fontlar upload'ta yoksa 403.
|
|
1293
|
+
// Kullanıcı `include: ["assets"]` yazsa bile prefix varken `fonts` eklenir.
|
|
1294
|
+
if (prefix && !include.includes("fonts")) {
|
|
1295
|
+
include = [...include, "fonts"];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1267
1298
|
const cacheControl =
|
|
1268
1299
|
typeof uploadRaw.cacheControl === "string" && uploadRaw.cacheControl.trim()
|
|
1269
1300
|
? uploadRaw.cacheControl.trim()
|
|
1270
1301
|
: DEFAULT_BUILD.assets.upload.cacheControl;
|
|
1271
1302
|
|
|
1303
|
+
/** @type {string | null} */
|
|
1304
|
+
let corsOrigin = DEFAULT_BUILD.assets.upload.corsOrigin;
|
|
1305
|
+
if (uploadRaw.corsOrigin === null || uploadRaw.corsOrigin === false) {
|
|
1306
|
+
corsOrigin = null;
|
|
1307
|
+
} else if (typeof uploadRaw.corsOrigin === "string") {
|
|
1308
|
+
corsOrigin = uploadRaw.corsOrigin.trim() || null;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1272
1311
|
// Config'te bucket varsa `enabled` unutulmuş olsa bile aç. Açık `false` ezer.
|
|
1273
1312
|
const enabled =
|
|
1274
1313
|
uploadRaw.enabled === true ||
|
|
@@ -1284,6 +1323,7 @@ export function normalizeBuildAssets(raw) {
|
|
|
1284
1323
|
endpoint,
|
|
1285
1324
|
include,
|
|
1286
1325
|
cacheControl,
|
|
1326
|
+
corsOrigin,
|
|
1287
1327
|
},
|
|
1288
1328
|
};
|
|
1289
1329
|
}
|
|
@@ -1682,6 +1722,17 @@ export async function loadConfig(options = {}) {
|
|
|
1682
1722
|
);
|
|
1683
1723
|
}
|
|
1684
1724
|
|
|
1725
|
+
if (
|
|
1726
|
+
config.prewarm?.mode === "off" &&
|
|
1727
|
+
typeof config.hooks?.prewarmPaths === "function"
|
|
1728
|
+
) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
"[config] hooks.prewarmPaths() cannot be used with cache().prewarm: false. " +
|
|
1731
|
+
"On-demand mode only caches pages as they are visited; classic mode uses " +
|
|
1732
|
+
"prewarmPaths. Choose one.",
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1685
1736
|
// Dev'de build ve sunucu ayrı alt süreçler; üçü de aynı özeti basınca satır
|
|
1686
1737
|
// banner'ın ve build bloğunun arasına üç kez giriyor. Özeti dış süreç basar.
|
|
1687
1738
|
if (loaded && !process.env.JSKELET_CHILD) {
|
|
@@ -14,6 +14,9 @@ import crypto from "node:crypto";
|
|
|
14
14
|
* @typedef {{ bucket: string, key: string, body: string | Buffer,
|
|
15
15
|
* region: string, endpoint?: string | null, credentials: AwsCredentials,
|
|
16
16
|
* contentType?: string, cacheControl?: string | null, now?: Date }} PutObjectInput
|
|
17
|
+
*
|
|
18
|
+
* @typedef {{ bucket: string, region: string, endpoint?: string | null,
|
|
19
|
+
* credentials: AwsCredentials, allowedOrigin: string, now?: Date }} PutBucketCorsInput
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
/**
|
|
@@ -131,6 +134,24 @@ export function useS3PathStyle(bucket, endpoint) {
|
|
|
131
134
|
return bucket.includes(".");
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @param {{ bucket: string, region: string, endpoint?: string | null }} input
|
|
139
|
+
* @returns {{ host: string, base: string, pathStyle: boolean }}
|
|
140
|
+
*/
|
|
141
|
+
function s3EndpointParts(input) {
|
|
142
|
+
const pathStyle = useS3PathStyle(input.bucket, input.endpoint);
|
|
143
|
+
if (input.endpoint) {
|
|
144
|
+
const base = String(input.endpoint).replace(/\/+$/, "");
|
|
145
|
+
return { host: new URL(input.endpoint).host, base, pathStyle: true };
|
|
146
|
+
}
|
|
147
|
+
if (pathStyle) {
|
|
148
|
+
const host = `s3.${input.region}.amazonaws.com`;
|
|
149
|
+
return { host, base: `https://${host}`, pathStyle: true };
|
|
150
|
+
}
|
|
151
|
+
const host = `${input.bucket}.s3.${input.region}.amazonaws.com`;
|
|
152
|
+
return { host, base: `https://${host}`, pathStyle: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
/**
|
|
135
156
|
* PutObject isteğini imzalar; ağ çağrısı yapmaz — test edilebilir olsun diye.
|
|
136
157
|
*
|
|
@@ -145,22 +166,7 @@ export function buildSignedPutObject(input) {
|
|
|
145
166
|
const payloadHash = sha256Hex(body);
|
|
146
167
|
|
|
147
168
|
const keyPath = encodeS3Path(input.key.replace(/^\/+/, ""));
|
|
148
|
-
const pathStyle =
|
|
149
|
-
|
|
150
|
-
/** @type {string} */
|
|
151
|
-
let host;
|
|
152
|
-
/** @type {string} */
|
|
153
|
-
let base;
|
|
154
|
-
if (input.endpoint) {
|
|
155
|
-
base = String(input.endpoint).replace(/\/+$/, "");
|
|
156
|
-
host = new URL(input.endpoint).host;
|
|
157
|
-
} else if (pathStyle) {
|
|
158
|
-
host = `s3.${input.region}.amazonaws.com`;
|
|
159
|
-
base = `https://${host}`;
|
|
160
|
-
} else {
|
|
161
|
-
host = `${input.bucket}.s3.${input.region}.amazonaws.com`;
|
|
162
|
-
base = `https://${host}`;
|
|
163
|
-
}
|
|
169
|
+
const { host, base, pathStyle } = s3EndpointParts(input);
|
|
164
170
|
|
|
165
171
|
const canonicalUri = pathStyle
|
|
166
172
|
? `/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
@@ -219,6 +225,132 @@ export function buildSignedPutObject(input) {
|
|
|
219
225
|
return { url, headers, body };
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
/**
|
|
229
|
+
* PutBucketCors gövdesi — tek kural, GET/HEAD.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} allowedOrigin
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
export function corsConfigurationXml(allowedOrigin) {
|
|
235
|
+
const origin = String(allowedOrigin)
|
|
236
|
+
.replace(/&/g, "&")
|
|
237
|
+
.replace(/</g, "<")
|
|
238
|
+
.replace(/>/g, ">")
|
|
239
|
+
.replace(/"/g, """);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
`<?xml version="1.0" encoding="UTF-8"?>` +
|
|
243
|
+
`<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">` +
|
|
244
|
+
`<CORSRule>` +
|
|
245
|
+
`<AllowedOrigin>${origin}</AllowedOrigin>` +
|
|
246
|
+
`<AllowedMethod>GET</AllowedMethod>` +
|
|
247
|
+
`<AllowedMethod>HEAD</AllowedMethod>` +
|
|
248
|
+
`<AllowedHeader>*</AllowedHeader>` +
|
|
249
|
+
`<ExposeHeader>ETag</ExposeHeader>` +
|
|
250
|
+
`<ExposeHeader>Content-Type</ExposeHeader>` +
|
|
251
|
+
`<MaxAgeSeconds>86400</MaxAgeSeconds>` +
|
|
252
|
+
`</CORSRule>` +
|
|
253
|
+
`</CORSConfiguration>`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* PutBucketCors isteğini imzalar (`PUT /?cors` veya path-style `/bucket?cors`).
|
|
259
|
+
*
|
|
260
|
+
* @param {PutBucketCorsInput} input
|
|
261
|
+
* @returns {{ url: string, headers: Record<string, string>, body: Buffer }}
|
|
262
|
+
*/
|
|
263
|
+
export function buildSignedPutBucketCors(input) {
|
|
264
|
+
const body = Buffer.from(corsConfigurationXml(input.allowedOrigin), "utf8");
|
|
265
|
+
const now = input.now ?? new Date();
|
|
266
|
+
const { amzDate, dateStamp } = formatAmzDates(now);
|
|
267
|
+
const payloadHash = sha256Hex(body);
|
|
268
|
+
const { host, base, pathStyle } = s3EndpointParts(input);
|
|
269
|
+
|
|
270
|
+
// AWS: cors alt kaynağının canonical query'si `cors=` (boş değer).
|
|
271
|
+
const canonicalQuerystring = "cors=";
|
|
272
|
+
const canonicalUri = pathStyle ? `/${encodeS3Path(input.bucket)}` : "/";
|
|
273
|
+
|
|
274
|
+
/** @type {Record<string, string>} */
|
|
275
|
+
const headers = {
|
|
276
|
+
host,
|
|
277
|
+
"content-type": "application/xml",
|
|
278
|
+
"content-length": String(body.length),
|
|
279
|
+
"x-amz-content-sha256": payloadHash,
|
|
280
|
+
"x-amz-date": amzDate,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (input.credentials.sessionToken) {
|
|
284
|
+
headers["x-amz-security-token"] = input.credentials.sessionToken;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { canonicalRequest, signedHeaders } = buildCanonicalRequest({
|
|
288
|
+
method: "PUT",
|
|
289
|
+
canonicalUri,
|
|
290
|
+
canonicalQuerystring,
|
|
291
|
+
headers,
|
|
292
|
+
payloadHash,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const stringToSign = buildStringToSign({
|
|
296
|
+
amzDate,
|
|
297
|
+
dateStamp,
|
|
298
|
+
region: input.region,
|
|
299
|
+
service: "s3",
|
|
300
|
+
canonicalRequest,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const signingKey = deriveSigningKey(
|
|
304
|
+
input.credentials.secretAccessKey,
|
|
305
|
+
dateStamp,
|
|
306
|
+
input.region,
|
|
307
|
+
"s3",
|
|
308
|
+
);
|
|
309
|
+
const signature = hmac("sha256", signingKey, stringToSign).toString("hex");
|
|
310
|
+
const credentialScope = `${dateStamp}/${input.region}/s3/aws4_request`;
|
|
311
|
+
|
|
312
|
+
headers.authorization =
|
|
313
|
+
`AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}, ` +
|
|
314
|
+
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
315
|
+
|
|
316
|
+
const url = pathStyle
|
|
317
|
+
? `${base}/${encodeS3Path(input.bucket)}?cors`
|
|
318
|
+
: `${base}/?cors`;
|
|
319
|
+
|
|
320
|
+
return { url, headers, body };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Bucket CORS yazar. Mevcut kuralların **üzerine** yazar; paylaşımlı
|
|
325
|
+
* kovada bilinçli kullanılmalı. İzin yoksa çağıran uyarır, upload sürer.
|
|
326
|
+
*
|
|
327
|
+
* @param {PutBucketCorsInput} input
|
|
328
|
+
* @returns {Promise<void>}
|
|
329
|
+
*/
|
|
330
|
+
export async function putBucketCors(input) {
|
|
331
|
+
const signed = buildSignedPutBucketCors(input);
|
|
332
|
+
/** @type {Response} */
|
|
333
|
+
let response;
|
|
334
|
+
try {
|
|
335
|
+
response = await fetch(signed.url, {
|
|
336
|
+
method: "PUT",
|
|
337
|
+
headers: signed.headers,
|
|
338
|
+
body: signed.body,
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const detail =
|
|
342
|
+
error instanceof Error ? error.message : String(error);
|
|
343
|
+
throw new Error(`S3 PutBucketCors fetch failed (${signed.url}): ${detail}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
const text = await response.text().catch(() => "");
|
|
348
|
+
throw new Error(
|
|
349
|
+
`S3 PutBucketCors ${response.status}: ${text.slice(0, 200) || response.statusText}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
222
354
|
/**
|
|
223
355
|
* @param {PutObjectInput} input
|
|
224
356
|
* @returns {Promise<void>}
|
package/src/server/prewarm.js
CHANGED
|
@@ -928,6 +928,8 @@ async function drainVisitWarm() {
|
|
|
928
928
|
* Açılışta ısıtmayı tetikler. `listen` geri çağrısından çağrılır.
|
|
929
929
|
* `onVisit` modunda klasik zamanlayıcı yok; yine de erken-TTL invalidation
|
|
930
930
|
* drain'i çalışır — soft-bayatlayan sweeper'ın kuyruğu boşalmasın.
|
|
931
|
+
* `prewarm: false` (`mode: "off"`) on-demand: ziyaret yazımı kalır, hiçbir
|
|
932
|
+
* arka plan ısıtması kurulmaz; `PREWARM=1` bunu ezmez.
|
|
931
933
|
*
|
|
932
934
|
* @param {{ port: number }} options
|
|
933
935
|
* @returns {void}
|
|
@@ -935,12 +937,14 @@ async function drainVisitWarm() {
|
|
|
935
937
|
export function startPrewarm({ port }) {
|
|
936
938
|
const config = getConfig();
|
|
937
939
|
if (process.env.PREWARM === "0") return;
|
|
940
|
+
// On-demand: L1/L2 ziyaretle dolar; classic / onVisit / early-expiry yok.
|
|
941
|
+
if (config.prewarm?.mode === "off") return;
|
|
938
942
|
|
|
939
943
|
const origins = resolvePrewarmOrigins(port);
|
|
940
944
|
const origin = origins[0];
|
|
941
945
|
|
|
942
946
|
// Klasik `prewarmPaths` olmasa da TTL öncesi soft-bayatlayan girdiler
|
|
943
|
-
// HTTP ile ısıtılsın. `PREWARM=0` yukarıda her şeyi keser.
|
|
947
|
+
// HTTP ile ısıtılsın. `PREWARM=0` / `mode: "off"` yukarıda her şeyi keser.
|
|
944
948
|
startEarlyExpirySweep();
|
|
945
949
|
startExpiryWarmDrain(origins);
|
|
946
950
|
|
|
@@ -40,12 +40,17 @@ export declare const DEFAULT_STATIC: {
|
|
|
40
40
|
/** Dev gate'in hiçbir koşulda kapatmadığı yollar. */
|
|
41
41
|
export declare const DEFAULT_DEV_GATE_BYPASS: string[];
|
|
42
42
|
/**
|
|
43
|
-
* Klasik (liste tabanlı) prewarm ayarları. `onVisit`
|
|
44
|
-
* kullanılamaz — ya açılış/`prewarmPaths` turu, ya
|
|
45
|
-
* linkleri ısıtma
|
|
43
|
+
* Klasik (liste tabanlı) prewarm ayarları. `onVisit` ve `prewarm: false`
|
|
44
|
+
* (on-demand) ile birlikte kullanılamaz — ya açılış/`prewarmPaths` turu, ya
|
|
45
|
+
* ziyaret edilen sayfadaki linkleri ısıtma, ya yalnızca ziyaret yazımı.
|
|
46
|
+
*
|
|
47
|
+
* `mode` çözümleme sonucu: `"classic"` | `"onVisit"` | `"off"`.
|
|
48
|
+
* `prewarm: false` → `"off"` (erken TTL ısıtması dahil hiçbir arka plan yok).
|
|
46
49
|
*/
|
|
47
50
|
export declare const DEFAULT_PREWARM: {
|
|
48
51
|
enabled: boolean;
|
|
52
|
+
/** @type {"classic" | "onVisit" | "off"} */
|
|
53
|
+
mode: "classic" | "onVisit" | "off";
|
|
49
54
|
max: number;
|
|
50
55
|
intervalSeconds: number;
|
|
51
56
|
/** Paralel ısıtma isteği; dev'de sunucu tek süreç olduğu için düşürülür. */
|
|
@@ -458,9 +463,19 @@ export declare const DEFAULT_BUILD: {
|
|
|
458
463
|
keyPrefix: string;
|
|
459
464
|
region: string;
|
|
460
465
|
endpoint: null;
|
|
461
|
-
/**
|
|
466
|
+
/**
|
|
467
|
+
* `public/` altı dizin adları. `fonts` varsayılan: CDN CSS'teki
|
|
468
|
+
* `/fonts/…` rewrite'ı upload nesnesiyle hizalansın.
|
|
469
|
+
*/
|
|
462
470
|
include: string[];
|
|
463
471
|
cacheControl: string;
|
|
472
|
+
/**
|
|
473
|
+
* Upload sırasında PutBucketCors ile yazılan `AllowedOrigin`.
|
|
474
|
+
* `null` / `false` → CORS adımı atlanır. Bucket'taki önceki CORS
|
|
475
|
+
* kuralının üzerine yazar — paylaşımlı kovada bilinçli açın.
|
|
476
|
+
* CDN CSS + aynı host font için genelde gerekmez.
|
|
477
|
+
*/
|
|
478
|
+
corsOrigin: string | null;
|
|
464
479
|
};
|
|
465
480
|
};
|
|
466
481
|
};
|
package/types/config/index.d.ts
CHANGED
|
@@ -240,6 +240,10 @@ export type BuildAssetsUploadConfig = {
|
|
|
240
240
|
endpoint: string | null;
|
|
241
241
|
include: string[];
|
|
242
242
|
cacheControl: string;
|
|
243
|
+
/**
|
|
244
|
+
* Font/CDN PutBucketCors `AllowedOrigin`; `null` kapatır.
|
|
245
|
+
*/
|
|
246
|
+
corsOrigin: string | null;
|
|
243
247
|
};
|
|
244
248
|
export type BuildAssetsConfig = {
|
|
245
249
|
/**
|
|
@@ -14,6 +14,14 @@ export type PutObjectInput = {
|
|
|
14
14
|
cacheControl?: string | null;
|
|
15
15
|
now?: Date;
|
|
16
16
|
};
|
|
17
|
+
export type PutBucketCorsInput = {
|
|
18
|
+
bucket: string;
|
|
19
|
+
region: string;
|
|
20
|
+
endpoint?: string | null;
|
|
21
|
+
credentials: AwsCredentials;
|
|
22
|
+
allowedOrigin: string;
|
|
23
|
+
now?: Date;
|
|
24
|
+
};
|
|
17
25
|
/**
|
|
18
26
|
* @param {string | Buffer} data
|
|
19
27
|
* @returns {string}
|
|
@@ -91,6 +99,32 @@ export declare function buildSignedPutObject(input: PutObjectInput): {
|
|
|
91
99
|
headers: Record<string, string>;
|
|
92
100
|
body: Buffer;
|
|
93
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* PutBucketCors gövdesi — tek kural, GET/HEAD.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} allowedOrigin
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export declare function corsConfigurationXml(allowedOrigin: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* PutBucketCors isteğini imzalar (`PUT /?cors` veya path-style `/bucket?cors`).
|
|
111
|
+
*
|
|
112
|
+
* @param {PutBucketCorsInput} input
|
|
113
|
+
* @returns {{ url: string, headers: Record<string, string>, body: Buffer }}
|
|
114
|
+
*/
|
|
115
|
+
export declare function buildSignedPutBucketCors(input: PutBucketCorsInput): {
|
|
116
|
+
url: string;
|
|
117
|
+
headers: Record<string, string>;
|
|
118
|
+
body: Buffer;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Bucket CORS yazar. Mevcut kuralların **üzerine** yazar; paylaşımlı
|
|
122
|
+
* kovada bilinçli kullanılmalı. İzin yoksa çağıran uyarır, upload sürer.
|
|
123
|
+
*
|
|
124
|
+
* @param {PutBucketCorsInput} input
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
export declare function putBucketCors(input: PutBucketCorsInput): Promise<void>;
|
|
94
128
|
/**
|
|
95
129
|
* @param {PutObjectInput} input
|
|
96
130
|
* @returns {Promise<void>}
|
|
@@ -122,6 +122,8 @@ export declare function noteVisitWarm(html: string, context: {
|
|
|
122
122
|
* Açılışta ısıtmayı tetikler. `listen` geri çağrısından çağrılır.
|
|
123
123
|
* `onVisit` modunda klasik zamanlayıcı yok; yine de erken-TTL invalidation
|
|
124
124
|
* drain'i çalışır — soft-bayatlayan sweeper'ın kuyruğu boşalmasın.
|
|
125
|
+
* `prewarm: false` (`mode: "off"`) on-demand: ziyaret yazımı kalır, hiçbir
|
|
126
|
+
* arka plan ısıtması kurulmaz; `PREWARM=1` bunu ezmez.
|
|
125
127
|
*
|
|
126
128
|
* @param {{ port: number }} options
|
|
127
129
|
* @returns {void}
|